chore: move core modules into core directory

BREAKING CHANGE:
    This change moves the http module into angular2/, so its import
    path is now angular2/http instead of http/http.

    Many other modules have also been moved around inside of angular2,
    but the public API paths have not changed as of this commit.
This commit is contained in:
Jeff Cross
2015-08-20 14:28:09 -07:00
parent 56e88058f1
commit 38a5a2a955
260 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,509 @@
import {isPresent, isBlank, RegExpWrapper} from 'angular2/src/facade/lang';
import {Promise} from 'angular2/src/facade/async';
import {List, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
import {ASTWithSource} from 'angular2/src/change_detection/change_detection';
/**
* General notes:
*
* The methods for creating / destroying views in this API are used in the AppViewHydrator
* and RenderViewHydrator as well.
*
* We are already parsing expressions on the render side:
* - this makes the ElementBinders more compact
* (e.g. no need to distinguish interpolations from regular expressions from literals)
* - allows to retrieve which properties should be accessed from the event
* by looking at the expression
* - we need the parse at least for the `template` attribute to match
* directives in it
* - render compiler is not on the critical path as
* its output will be stored in precompiled templates.
*/
export class EventBinding {
constructor(public fullName: string, public source: ASTWithSource) {}
}
export enum PropertyBindingType {
PROPERTY,
ATTRIBUTE,
CLASS,
STYLE
}
export class ElementPropertyBinding {
constructor(public type: PropertyBindingType, public astWithSource: ASTWithSource,
public property: string, public unit: string = null) {}
}
export class RenderElementBinder {
index: number;
parentIndex: number;
distanceToParent: number;
directives: List<DirectiveBinder>;
nestedProtoView: ProtoViewDto;
propertyBindings: List<ElementPropertyBinding>;
variableBindings: Map<string, string>;
// Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element
// with a local name
eventBindings: List<EventBinding>;
readAttributes: Map<string, string>;
constructor({index, parentIndex, distanceToParent, directives, nestedProtoView, propertyBindings,
variableBindings, eventBindings, readAttributes}: {
index?: number,
parentIndex?: number,
distanceToParent?: number,
directives?: List<DirectiveBinder>,
nestedProtoView?: ProtoViewDto,
propertyBindings?: List<ElementPropertyBinding>,
variableBindings?: Map<string, string>,
eventBindings?: List<EventBinding>,
readAttributes?: Map<string, string>
} = {}) {
this.index = index;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
this.directives = directives;
this.nestedProtoView = nestedProtoView;
this.propertyBindings = propertyBindings;
this.variableBindings = variableBindings;
this.eventBindings = eventBindings;
this.readAttributes = readAttributes;
}
}
export class DirectiveBinder {
// Index into the array of directives in the View instance
directiveIndex: number;
propertyBindings: Map<string, ASTWithSource>;
// Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element
// with a local name
eventBindings: List<EventBinding>;
hostPropertyBindings: List<ElementPropertyBinding>;
constructor({directiveIndex, propertyBindings, eventBindings, hostPropertyBindings}: {
directiveIndex?: number,
propertyBindings?: Map<string, ASTWithSource>,
eventBindings?: List<EventBinding>,
hostPropertyBindings?: List<ElementPropertyBinding>
}) {
this.directiveIndex = directiveIndex;
this.propertyBindings = propertyBindings;
this.eventBindings = eventBindings;
this.hostPropertyBindings = hostPropertyBindings;
}
}
export enum ViewType {
// A view that contains the host element with bound component directive.
// Contains a COMPONENT view
HOST,
// The view of the component
// Can contain 0 to n EMBEDDED views
COMPONENT,
// A view that is embedded into another View via a <template> element
// inside of a COMPONENT view
EMBEDDED
}
export class ProtoViewDto {
render: RenderProtoViewRef;
elementBinders: List<RenderElementBinder>;
variableBindings: Map<string, string>;
type: ViewType;
textBindings: List<ASTWithSource>;
transitiveNgContentCount: number;
constructor({render, elementBinders, variableBindings, type, textBindings,
transitiveNgContentCount}: {
render?: RenderProtoViewRef,
elementBinders?: List<RenderElementBinder>,
variableBindings?: Map<string, string>,
type?: ViewType,
textBindings?: List<ASTWithSource>,
transitiveNgContentCount?: number
}) {
this.render = render;
this.elementBinders = elementBinders;
this.variableBindings = variableBindings;
this.type = type;
this.textBindings = textBindings;
this.transitiveNgContentCount = transitiveNgContentCount;
}
}
export class RenderDirectiveMetadata {
static get DIRECTIVE_TYPE() { return 0; }
static get COMPONENT_TYPE() { return 1; }
id: any;
selector: string;
compileChildren: boolean;
events: List<string>;
properties: List<string>;
readAttributes: List<string>;
type: number;
callOnDestroy: boolean;
callOnChange: boolean;
callOnCheck: boolean;
callOnInit: boolean;
callOnAllChangesDone: boolean;
changeDetection: string;
exportAs: string;
hostListeners: Map<string, string>;
hostProperties: Map<string, string>;
hostAttributes: Map<string, string>;
hostActions: Map<string, string>;
// group 1: "property" from "[property]"
// group 2: "event" from "(event)"
// group 3: "action" from "@action"
private static _hostRegExp = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\))|(?:@(.+)))$/g;
constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes,
hostActions, properties, readAttributes, type, callOnDestroy, callOnChange,
callOnCheck, callOnInit, callOnAllChangesDone, changeDetection, exportAs}: {
id?: string,
selector?: string,
compileChildren?: boolean,
events?: List<string>,
hostListeners?: Map<string, string>,
hostProperties?: Map<string, string>,
hostAttributes?: Map<string, string>,
hostActions?: Map<string, string>,
properties?: List<string>,
readAttributes?: List<string>,
type?: number,
callOnDestroy?: boolean,
callOnChange?: boolean,
callOnCheck?: boolean,
callOnInit?: boolean,
callOnAllChangesDone?: boolean,
changeDetection?: string,
exportAs?: string
}) {
this.id = id;
this.selector = selector;
this.compileChildren = isPresent(compileChildren) ? compileChildren : true;
this.events = events;
this.hostListeners = hostListeners;
this.hostAttributes = hostAttributes;
this.hostProperties = hostProperties;
this.hostActions = hostActions;
this.properties = properties;
this.readAttributes = readAttributes;
this.type = type;
this.callOnDestroy = callOnDestroy;
this.callOnChange = callOnChange;
this.callOnCheck = callOnCheck;
this.callOnInit = callOnInit;
this.callOnAllChangesDone = callOnAllChangesDone;
this.changeDetection = changeDetection;
this.exportAs = exportAs;
}
static create({id, selector, compileChildren, events, host, properties, readAttributes, type,
callOnDestroy, callOnChange, callOnCheck, callOnInit, callOnAllChangesDone,
changeDetection, exportAs}: {
id?: string,
selector?: string,
compileChildren?: boolean,
events?: List<string>,
host?: Map<string, string>,
properties?: List<string>,
readAttributes?: List<string>,
type?: number,
callOnDestroy?: boolean,
callOnChange?: boolean,
callOnCheck?: boolean,
callOnInit?: boolean,
callOnAllChangesDone?: boolean,
changeDetection?: string,
exportAs?: string
}): RenderDirectiveMetadata {
let hostListeners = new Map();
let hostProperties = new Map();
let hostAttributes = new Map();
let hostActions = new Map();
if (isPresent(host)) {
MapWrapper.forEach(host, (value: string, key: string) => {
var matches = RegExpWrapper.firstMatch(RenderDirectiveMetadata._hostRegExp, key);
if (isBlank(matches)) {
hostAttributes.set(key, value);
} else if (isPresent(matches[1])) {
hostProperties.set(matches[1], value);
} else if (isPresent(matches[2])) {
hostListeners.set(matches[2], value);
} else if (isPresent(matches[3])) {
hostActions.set(matches[3], value);
}
});
}
return new RenderDirectiveMetadata({
id: id,
selector: selector,
compileChildren: compileChildren,
events: events,
hostListeners: hostListeners,
hostProperties: hostProperties,
hostAttributes: hostAttributes,
hostActions: hostActions,
properties: properties,
readAttributes: readAttributes,
type: type,
callOnDestroy: callOnDestroy,
callOnChange: callOnChange,
callOnCheck: callOnCheck,
callOnInit: callOnInit,
callOnAllChangesDone: callOnAllChangesDone,
changeDetection: changeDetection,
exportAs: exportAs
});
}
}
// An opaque reference to a render proto ivew
export class RenderProtoViewRef {}
// An opaque reference to a part of a view
export class RenderFragmentRef {}
// An opaque reference to a view
export class RenderViewRef {}
/**
* How the template and styles of a view should be encapsulated.
*/
export enum ViewEncapsulation {
/**
* Emulate scoping of styles by preprocessing the style rules
* and adding additional attributes to elements. This is the default.
*/
EMULATED,
/**
* Uses the native mechanism of the renderer. For the DOM this means creating a ShadowRoot.
*/
NATIVE,
/**
* Don't scope the template nor the styles.
*/
NONE
}
export class ViewDefinition {
componentId: string;
templateAbsUrl: string;
template: string;
directives: List<RenderDirectiveMetadata>;
styleAbsUrls: List<string>;
styles: List<string>;
encapsulation: ViewEncapsulation;
constructor({componentId, templateAbsUrl, template, styleAbsUrls, styles, directives,
encapsulation}: {
componentId?: string,
templateAbsUrl?: string,
template?: string,
styleAbsUrls?: List<string>,
styles?: List<string>,
directives?: List<RenderDirectiveMetadata>,
encapsulation?: ViewEncapsulation
} = {}) {
this.componentId = componentId;
this.templateAbsUrl = templateAbsUrl;
this.template = template;
this.styleAbsUrls = styleAbsUrls;
this.styles = styles;
this.directives = directives;
this.encapsulation = isPresent(encapsulation) ? encapsulation : ViewEncapsulation.EMULATED;
}
}
export class RenderProtoViewMergeMapping {
constructor(public mergedProtoViewRef: RenderProtoViewRef,
// Number of fragments in the merged ProtoView.
// Fragments are stored in depth first order of nested ProtoViews.
public fragmentCount: number,
// Mapping from app element index to render element index.
// Mappings of nested ProtoViews are in depth first order, with all
// indices for one ProtoView in a consecuitve block.
public mappedElementIndices: number[],
// Number of bound render element.
// Note: This could be more than the original ones
// as we might have bound a new element for projecting bound text nodes.
public mappedElementCount: number,
// Mapping from app text index to render text index.
// Mappings of nested ProtoViews are in depth first order, with all
// indices for one ProtoView in a consecuitve block.
public mappedTextIndices: number[],
// Mapping from view index to app element index
public hostElementIndicesByViewIndex: number[],
// Number of contained views by view index
public nestedViewCountByViewIndex: number[]) {}
}
export class RenderCompiler {
/**
* Creats a ProtoViewDto that contains a single nested component with the given componentId.
*/
compileHost(directiveMetadata: RenderDirectiveMetadata): Promise<ProtoViewDto> { return null; }
/**
* Compiles a single DomProtoView. Non recursive so that
* we don't need to serialize all possible components over the wire,
* but only the needed ones based on previous calls.
*/
compile(view: ViewDefinition): Promise<ProtoViewDto> { return null; }
/**
* Merges ProtoViews.
* The first entry of the array is the protoview into which all the other entries of the array
* should be merged.
* If the array contains other arrays, they will be merged before processing the parent array.
* The array must contain an entry for every component and embedded ProtoView of the first entry.
* @param protoViewRefs List of ProtoViewRefs or nested
* @return the merge result
*/
mergeProtoViewsRecursively(
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping> {
return null;
}
}
export class RenderViewWithFragments {
constructor(public viewRef: RenderViewRef, public fragmentRefs: RenderFragmentRef[]) {}
}
/**
* Abstract reference to the element which can be marshaled across web-worker boundary.
*
* This interface is used by the Renderer API.
*/
export interface RenderElementRef {
/**
* Reference to the `RenderViewRef` where the `RenderElementRef` is inside of.
*/
renderView: RenderViewRef;
/**
* Index of the element inside the `RenderViewRef`.
*
* This is used internally by the Angular framework to locate elements.
*/
renderBoundElementIndex: number;
}
export class Renderer {
/**
* Creates a root host view that includes the given element.
* Note that the fragmentCount needs to be passed in so that we can create a result
* synchronously even when dealing with webworkers!
*
* @param {RenderProtoViewRef} hostProtoViewRef a RenderProtoViewRef of type
* ProtoViewDto.HOST_VIEW_TYPE
* @param {any} hostElementSelector css selector for the host element (will be queried against the
* main document)
* @return {RenderViewWithFragments} the created view including fragments
*/
createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number,
hostElementSelector: string): RenderViewWithFragments {
return null;
}
/**
* Creates a regular view out of the given ProtoView.
* Note that the fragmentCount needs to be passed in so that we can create a result
* synchronously even when dealing with webworkers!
*/
createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments {
return null;
}
/**
* Destroys the given view after it has been dehydrated and detached
*/
destroyView(viewRef: RenderViewRef) {}
/**
* Attaches a fragment after another fragment.
*/
attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef,
fragmentRef: RenderFragmentRef) {}
/**
* Attaches a fragment after an element.
*/
attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) {}
/**
* Detaches a fragment.
*/
detachFragment(fragmentRef: RenderFragmentRef) {}
/**
* Hydrates a view after it has been attached. Hydration/dehydration is used for reusing views
* inside of the view pool.
*/
hydrateView(viewRef: RenderViewRef) {}
/**
* Dehydrates a view after it has been attached. Hydration/dehydration is used for reusing views
* inside of the view pool.
*/
dehydrateView(viewRef: RenderViewRef) {}
/**
* Returns the native element at the given location.
* Attention: In a WebWorker scenario, this should always return null!
*/
getNativeElementSync(location: RenderElementRef): any { return null; }
/**
* Sets a property on an element.
*/
setElementProperty(location: RenderElementRef, propertyName: string, propertyValue: any) {}
/**
* Sets an attribute on an element.
*/
setElementAttribute(location: RenderElementRef, attributeName: string, attributeValue: string) {}
/**
* Sets a class on an element.
*/
setElementClass(location: RenderElementRef, className: string, isAdd: boolean) {}
/**
* Sets a style on an element.
*/
setElementStyle(location: RenderElementRef, styleName: string, styleValue: string) {}
/**
* Calls a method on an element.
*/
invokeElementMethod(location: RenderElementRef, methodName: string, args: List<any>) {}
/**
* Sets the value of a text node.
*/
setText(viewRef: RenderViewRef, textNodeIndex: number, text: string) {}
/**
* Sets the dispatcher for all events of the given view
*/
setEventDispatcher(viewRef: RenderViewRef, dispatcher: RenderEventDispatcher) {}
}
/**
* A dispatcher for all events happening in a view.
*/
export interface RenderEventDispatcher {
/**
* Called when an event was triggered for a on-* attribute on an element.
* @param {Map<string, any>} locals Locals to be used to evaluate the
* event expressions
*/
dispatchRenderEvent(elementIndex: number, eventName: string, locals: Map<string, any>);
}

View File

@ -0,0 +1,67 @@
import {isBlank} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {CompileElement} from './compile_element';
import {CompileStep} from './compile_step';
/**
* Controls the processing order of elements.
* Right now it only allows to add a parent element.
*/
export class CompileControl {
_currentStepIndex: number = 0;
_parent: CompileElement = null;
_results: any[] = null;
_additionalChildren: CompileElement[] = null;
_ignoreCurrentElement: boolean;
constructor(public _steps: List<CompileStep>) {}
// only public so that it can be used by compile_pipeline
internalProcess(results: any[], startStepIndex: number, parent: CompileElement,
current: CompileElement): CompileElement[] {
this._results = results;
var previousStepIndex = this._currentStepIndex;
var previousParent = this._parent;
this._ignoreCurrentElement = false;
for (var i = startStepIndex; i < this._steps.length && !this._ignoreCurrentElement; i++) {
var step = this._steps[i];
this._parent = parent;
this._currentStepIndex = i;
step.processElement(parent, current, this);
parent = this._parent;
}
if (!this._ignoreCurrentElement) {
results.push(current);
}
this._currentStepIndex = previousStepIndex;
this._parent = previousParent;
var localAdditionalChildren = this._additionalChildren;
this._additionalChildren = null;
return localAdditionalChildren;
}
addParent(newElement: CompileElement) {
this.internalProcess(this._results, this._currentStepIndex + 1, this._parent, newElement);
this._parent = newElement;
}
addChild(element: CompileElement) {
if (isBlank(this._additionalChildren)) {
this._additionalChildren = [];
}
this._additionalChildren.push(element);
}
/**
* Ignores the current element.
*
* When a step calls `ignoreCurrentElement`, no further steps are executed on the current
* element and no `CompileElement` is added to the result list.
*/
ignoreCurrentElement() { this._ignoreCurrentElement = true; }
}

View File

@ -0,0 +1,103 @@
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isBlank, isPresent, Type, StringJoiner, assertionsEnabled} from 'angular2/src/facade/lang';
import {ProtoViewBuilder, ElementBinderBuilder} from '../view/proto_view_builder';
/**
* Collects all data that is needed to process an element
* in the compile process. Fields are filled
* by the CompileSteps starting out with the pure HTMLElement.
*/
export class CompileElement {
_attrs: Map<string, string> = null;
_classList: List<string> = null;
isViewRoot: boolean = false;
// inherited down to children if they don't have an own protoView
inheritedProtoView: ProtoViewBuilder = null;
distanceToInheritedBinder: number = 0;
// inherited down to children if they don't have an own elementBinder
inheritedElementBinder: ElementBinderBuilder = null;
compileChildren: boolean = true;
elementDescription: string; // e.g. '<div [title]="foo">' : used to provide context in case of
// error
constructor(public element, compilationUnit: string = '') {
// description is calculated here as compilation steps may change the element
var tplDesc = assertionsEnabled() ? getElementDescription(element) : null;
if (compilationUnit !== '') {
this.elementDescription = compilationUnit;
if (isPresent(tplDesc)) this.elementDescription += ": " + tplDesc;
} else {
this.elementDescription = tplDesc;
}
}
isBound(): boolean {
return isPresent(this.inheritedElementBinder) && this.distanceToInheritedBinder === 0;
}
bindElement(): ElementBinderBuilder {
if (!this.isBound()) {
var parentBinder = this.inheritedElementBinder;
this.inheritedElementBinder =
this.inheritedProtoView.bindElement(this.element, this.elementDescription);
if (isPresent(parentBinder)) {
this.inheritedElementBinder.setParent(parentBinder, this.distanceToInheritedBinder);
}
this.distanceToInheritedBinder = 0;
}
return this.inheritedElementBinder;
}
attrs(): Map<string, string> {
if (isBlank(this._attrs)) {
this._attrs = DOM.attributeMap(this.element);
}
return this._attrs;
}
classList(): List<string> {
if (isBlank(this._classList)) {
this._classList = [];
var elClassList = DOM.classList(this.element);
for (var i = 0; i < elClassList.length; i++) {
this._classList.push(elClassList[i]);
}
}
return this._classList;
}
}
// return an HTML representation of an element start tag - without its content
// this is used to give contextual information in case of errors
function getElementDescription(domElement): string {
var buf = new StringJoiner();
var atts = DOM.attributeMap(domElement);
buf.add("<");
buf.add(DOM.tagName(domElement).toLowerCase());
// show id and class first to ease element identification
addDescriptionAttribute(buf, "id", atts.get("id"));
addDescriptionAttribute(buf, "class", atts.get("class"));
MapWrapper.forEach(atts, (attValue, attName) => {
if (attName !== "id" && attName !== "class") {
addDescriptionAttribute(buf, attName, attValue);
}
});
buf.add(">");
return buf.toString();
}
function addDescriptionAttribute(buffer: StringJoiner, attName: string, attValue) {
if (isPresent(attValue)) {
if (attValue.length === 0) {
buffer.add(' ' + attName);
} else {
buffer.add(' ' + attName + '="' + attValue + '"');
}
}
}

View File

@ -0,0 +1,64 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {CompileStep} from './compile_step';
import {ProtoViewBuilder} from '../view/proto_view_builder';
import {ProtoViewDto, ViewType, ViewDefinition} from '../../api';
/**
* CompilePipeline for executing CompileSteps recursively for
* all elements in a template.
*/
export class CompilePipeline {
_control: CompileControl;
constructor(public steps: List<CompileStep>) { this._control = new CompileControl(steps); }
processStyles(styles: string[]): string[] {
return styles.map(style => {
this.steps.forEach(step => { style = step.processStyle(style); });
return style;
});
}
processElements(rootElement: Element, protoViewType: ViewType,
viewDef: ViewDefinition): CompileElement[] {
var results: CompileElement[] = [];
var compilationCtxtDescription = viewDef.componentId;
var rootCompileElement = new CompileElement(rootElement, compilationCtxtDescription);
rootCompileElement.inheritedProtoView =
new ProtoViewBuilder(rootElement, protoViewType, viewDef.encapsulation);
rootCompileElement.isViewRoot = true;
this._processElement(results, null, rootCompileElement, compilationCtxtDescription);
return results;
}
_processElement(results: CompileElement[], parent: CompileElement, current: CompileElement,
compilationCtxtDescription: string = '') {
var additionalChildren = this._control.internalProcess(results, 0, parent, current);
if (current.compileChildren) {
var node = DOM.firstChild(DOM.templateAwareRoot(current.element));
while (isPresent(node)) {
// compiliation can potentially move the node, so we need to store the
// next sibling before recursing.
var nextNode = DOM.nextSibling(node);
if (DOM.isElementNode(node)) {
var childCompileElement = new CompileElement(node, compilationCtxtDescription);
childCompileElement.inheritedProtoView = current.inheritedProtoView;
childCompileElement.inheritedElementBinder = current.inheritedElementBinder;
childCompileElement.distanceToInheritedBinder = current.distanceToInheritedBinder + 1;
this._processElement(results, current, childCompileElement);
}
node = nextNode;
}
}
if (isPresent(additionalChildren)) {
for (var i = 0; i < additionalChildren.length; i++) {
this._processElement(results, current, additionalChildren[i]);
}
}
}
}

View File

@ -0,0 +1,13 @@
import {CompileElement} from './compile_element';
import * as compileControlModule from './compile_control';
/**
* One part of the compile process.
* Is guaranteed to be called in depth first order
*/
export interface CompileStep {
processElement(parent: CompileElement, current: CompileElement,
control: compileControlModule.CompileControl): void;
processStyle(style: string): string;
}

View File

@ -0,0 +1,28 @@
import {List} from 'angular2/src/facade/collection';
import {Parser} from 'angular2/src/change_detection/change_detection';
import {ViewDefinition} from '../../api';
import {CompileStep} from './compile_step';
import {PropertyBindingParser} from './property_binding_parser';
import {TextInterpolationParser} from './text_interpolation_parser';
import {DirectiveParser} from './directive_parser';
import {ViewSplitter} from './view_splitter';
import {StyleEncapsulator} from './style_encapsulator';
export class CompileStepFactory {
createSteps(view: ViewDefinition): List<CompileStep> { return null; }
}
export class DefaultStepFactory extends CompileStepFactory {
private _componentUIDsCache: Map<string, string> = new Map();
constructor(private _parser: Parser, private _appId: string) { super(); }
createSteps(view: ViewDefinition): List<CompileStep> {
return [
new ViewSplitter(this._parser),
new PropertyBindingParser(this._parser),
new DirectiveParser(this._parser, view.directives),
new TextInterpolationParser(this._parser),
new StyleEncapsulator(this._appId, view, this._componentUIDsCache)
];
}
}

View File

@ -0,0 +1,133 @@
import {Injectable} from 'angular2/di';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {BaseException, isPresent, isBlank} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
ViewDefinition,
ProtoViewDto,
ViewType,
RenderDirectiveMetadata,
RenderCompiler,
RenderProtoViewRef,
RenderProtoViewMergeMapping,
ViewEncapsulation
} from '../../api';
import {CompilePipeline} from './compile_pipeline';
import {ViewLoader, TemplateAndStyles} from 'angular2/src/render/dom/compiler/view_loader';
import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {Parser} from 'angular2/src/change_detection/change_detection';
import * as pvm from '../view/proto_view_merger';
import {CssSelector} from './selector';
import {DOCUMENT, APP_ID} from '../dom_tokens';
import {Inject} from 'angular2/di';
import {SharedStylesHost} from '../view/shared_styles_host';
import {prependAll} from '../util';
import {TemplateCloner} from '../template_cloner';
/**
* The compiler loads and translates the html templates of components into
* nested ProtoViews. To decompose its functionality it uses
* the CompilePipeline and the CompileSteps.
*/
export class DomCompiler extends RenderCompiler {
constructor(private _schemaRegistry: ElementSchemaRegistry,
private _templateCloner: TemplateCloner, private _stepFactory: CompileStepFactory,
private _viewLoader: ViewLoader, private _sharedStylesHost: SharedStylesHost) {
super();
}
compile(view: ViewDefinition): Promise<ProtoViewDto> {
var tplPromise = this._viewLoader.load(view);
return PromiseWrapper.then(
tplPromise, (tplAndStyles: TemplateAndStyles) =>
this._compileView(view, tplAndStyles, ViewType.COMPONENT),
(e) => {
throw new BaseException(`Failed to load the template for "${view.componentId}" : ${e}`);
return null;
});
}
compileHost(directiveMetadata: RenderDirectiveMetadata): Promise<ProtoViewDto> {
let hostViewDef = new ViewDefinition({
componentId: directiveMetadata.id,
templateAbsUrl: null, template: null,
styles: null,
styleAbsUrls: null,
directives: [directiveMetadata],
encapsulation: ViewEncapsulation.NONE
});
let selector = CssSelector.parse(directiveMetadata.selector)[0];
let hostTemplate = selector.getMatchingElementTemplate();
let templateAndStyles = new TemplateAndStyles(hostTemplate, []);
return this._compileView(hostViewDef, templateAndStyles, ViewType.HOST);
}
mergeProtoViewsRecursively(
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping> {
return PromiseWrapper.resolve(
pvm.mergeProtoViewsRecursively(this._templateCloner, protoViewRefs));
}
_compileView(viewDef: ViewDefinition, templateAndStyles: TemplateAndStyles,
protoViewType: ViewType): Promise<ProtoViewDto> {
if (viewDef.encapsulation === ViewEncapsulation.EMULATED &&
templateAndStyles.styles.length === 0) {
viewDef = this._normalizeViewEncapsulationIfThereAreNoStyles(viewDef);
}
var pipeline = new CompilePipeline(this._stepFactory.createSteps(viewDef));
var compiledStyles = pipeline.processStyles(templateAndStyles.styles);
var compileElements = pipeline.processElements(
this._createTemplateElm(templateAndStyles.template), protoViewType, viewDef);
if (viewDef.encapsulation === ViewEncapsulation.NATIVE) {
prependAll(DOM.content(compileElements[0].element),
compiledStyles.map(style => DOM.createStyleElement(style)));
} else {
this._sharedStylesHost.addStyles(compiledStyles);
}
return PromiseWrapper.resolve(
compileElements[0].inheritedProtoView.build(this._schemaRegistry, this._templateCloner));
}
_createTemplateElm(template: string) {
var templateElm = DOM.createTemplate(template);
var scriptTags = DOM.querySelectorAll(DOM.templateAwareRoot(templateElm), 'script');
for (var i = 0; i < scriptTags.length; i++) {
DOM.remove(scriptTags[i]);
}
return templateElm;
}
_normalizeViewEncapsulationIfThereAreNoStyles(viewDef: ViewDefinition): ViewDefinition {
if (viewDef.encapsulation === ViewEncapsulation.EMULATED) {
return new ViewDefinition({
componentId: viewDef.componentId,
templateAbsUrl: viewDef.templateAbsUrl, template: viewDef.template,
styleAbsUrls: viewDef.styleAbsUrls,
styles: viewDef.styles,
directives: viewDef.directives,
encapsulation: ViewEncapsulation.NONE
});
} else {
return viewDef;
}
}
}
@Injectable()
export class DefaultDomCompiler extends DomCompiler {
constructor(schemaRegistry: ElementSchemaRegistry, templateCloner: TemplateCloner, parser: Parser,
viewLoader: ViewLoader, sharedStylesHost: SharedStylesHost,
@Inject(APP_ID) appId: any) {
super(schemaRegistry, templateCloner, new DefaultStepFactory(parser, appId), viewLoader,
sharedStylesHost);
}
}

View File

@ -0,0 +1,172 @@
import {isPresent, isBlank, BaseException, StringWrapper} from 'angular2/src/facade/lang';
import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Parser} from 'angular2/src/change_detection/change_detection';
import {SelectorMatcher, CssSelector} from 'angular2/src/render/dom/compiler/selector';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {RenderDirectiveMetadata} from '../../api';
import {EventConfig, dashCaseToCamelCase, camelCaseToDashCase} from '../util';
import {DirectiveBuilder, ElementBinderBuilder} from '../view/proto_view_builder';
/**
* Parses the directives on a single element. Assumes ViewSplitter has already created
* <template> elements for template directives.
*/
export class DirectiveParser implements CompileStep {
_selectorMatcher: SelectorMatcher = new SelectorMatcher();
constructor(public _parser: Parser, public _directives: List<RenderDirectiveMetadata>) {
for (var i = 0; i < _directives.length; i++) {
var directive = _directives[i];
var selector = CssSelector.parse(directive.selector);
this._selectorMatcher.addSelectables(selector, i);
}
}
processStyle(style: string): string { return style; }
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
var attrs = current.attrs();
var classList = current.classList();
var cssSelector = new CssSelector();
var foundDirectiveIndices = [];
var elementBinder: ElementBinderBuilder = null;
cssSelector.setElement(DOM.nodeName(current.element));
for (var i = 0; i < classList.length; i++) {
cssSelector.addClassName(classList[i]);
}
MapWrapper.forEach(attrs,
(attrValue, attrName) => { cssSelector.addAttribute(attrName, attrValue); });
this._selectorMatcher.match(cssSelector, (selector, directiveIndex) => {
var directive = this._directives[directiveIndex];
elementBinder = current.bindElement();
if (directive.type === RenderDirectiveMetadata.COMPONENT_TYPE) {
this._ensureHasOnlyOneComponent(elementBinder, current.elementDescription);
// components need to go first, so it is easier to locate them in the result.
ListWrapper.insert(foundDirectiveIndices, 0, directiveIndex);
elementBinder.setComponentId(directive.id);
} else {
foundDirectiveIndices.push(directiveIndex);
}
});
ListWrapper.forEach(foundDirectiveIndices, (directiveIndex) => {
var dirMetadata = this._directives[directiveIndex];
var directiveBinderBuilder = elementBinder.bindDirective(directiveIndex);
current.compileChildren = current.compileChildren && dirMetadata.compileChildren;
if (isPresent(dirMetadata.properties)) {
ListWrapper.forEach(dirMetadata.properties, (bindConfig) => {
this._bindDirectiveProperty(bindConfig, current, directiveBinderBuilder);
});
}
if (isPresent(dirMetadata.hostListeners)) {
this._sortedKeysForEach(dirMetadata.hostListeners, (action, eventName) => {
this._bindDirectiveEvent(eventName, action, current, directiveBinderBuilder);
});
}
if (isPresent(dirMetadata.hostProperties)) {
this._sortedKeysForEach(dirMetadata.hostProperties, (expression, hostPropertyName) => {
this._bindHostProperty(hostPropertyName, expression, current, directiveBinderBuilder);
});
}
if (isPresent(dirMetadata.hostAttributes)) {
this._sortedKeysForEach(dirMetadata.hostAttributes, (hostAttrValue, hostAttrName) => {
this._addHostAttribute(hostAttrName, hostAttrValue, current);
});
}
if (isPresent(dirMetadata.readAttributes)) {
ListWrapper.forEach(dirMetadata.readAttributes,
(attrName) => { elementBinder.readAttribute(attrName); });
}
});
}
_sortedKeysForEach(map: Map<string, string>, fn: (value: string, key: string) => void): void {
var keys = MapWrapper.keys(map);
ListWrapper.sort(keys, (a, b) => {
// Ensure a stable sort.
var compareVal = StringWrapper.compare(a, b);
return compareVal == 0 ? -1 : compareVal;
});
ListWrapper.forEach(keys, (key) => { fn(MapWrapper.get(map, key), key); });
}
_ensureHasOnlyOneComponent(elementBinder: ElementBinderBuilder, elDescription: string): void {
if (isPresent(elementBinder.componentId)) {
throw new BaseException(
`Only one component directive is allowed per element - check ${elDescription}`);
}
}
_bindDirectiveProperty(bindConfig: string, compileElement: CompileElement,
directiveBinderBuilder: DirectiveBuilder) {
// Name of the property on the directive
let dirProperty: string;
// Name of the property on the element
let elProp: string;
let pipes: List<string>;
let assignIndex: number = bindConfig.indexOf(':');
if (assignIndex > -1) {
// canonical syntax: `dirProp: elProp | pipe0 | ... | pipeN`
dirProperty = StringWrapper.substring(bindConfig, 0, assignIndex).trim();
pipes = this._splitBindConfig(StringWrapper.substring(bindConfig, assignIndex + 1));
elProp = ListWrapper.removeAt(pipes, 0);
} else {
// shorthand syntax when the name of the property on the directive and on the element is the
// same, ie `property`
dirProperty = bindConfig;
elProp = bindConfig;
pipes = [];
}
elProp = dashCaseToCamelCase(elProp);
var bindingAst = compileElement.bindElement().propertyBindings.get(elProp);
if (isBlank(bindingAst)) {
var attributeValue = compileElement.attrs().get(camelCaseToDashCase(elProp));
if (isPresent(attributeValue)) {
bindingAst =
this._parser.wrapLiteralPrimitive(attributeValue, compileElement.elementDescription);
}
}
// Bindings are optional, so this binding only needs to be set up if an expression is given.
if (isPresent(bindingAst)) {
directiveBinderBuilder.bindProperty(dirProperty, bindingAst, elProp);
}
}
_bindDirectiveEvent(eventName, action, compileElement, directiveBinderBuilder) {
var ast = this._parser.parseAction(action, compileElement.elementDescription);
var parsedEvent = EventConfig.parse(eventName);
var targetName = parsedEvent.isLongForm ? parsedEvent.fieldName : null;
directiveBinderBuilder.bindEvent(parsedEvent.eventName, ast, targetName);
}
_bindHostProperty(hostPropertyName, expression, compileElement, directiveBinderBuilder) {
var ast = this._parser.parseSimpleBinding(
expression, `hostProperties of ${compileElement.elementDescription}`);
directiveBinderBuilder.bindHostProperty(hostPropertyName, ast);
}
_addHostAttribute(attrName, attrValue, compileElement) {
if (StringWrapper.equals(attrName, 'class')) {
ListWrapper.forEach(attrValue.split(' '),
(className) => { DOM.addClass(compileElement.element, className); });
} else if (!DOM.hasAttribute(compileElement.element, attrName)) {
DOM.setAttribute(compileElement.element, attrName, attrValue);
}
}
_splitBindConfig(bindConfig: string) {
return ListWrapper.map(bindConfig.split('|'), (s) => s.trim());
}
}

View File

@ -0,0 +1,113 @@
import {isPresent, RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
import {MapWrapper} from 'angular2/src/facade/collection';
import {Parser} from 'angular2/src/change_detection/change_detection';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {dashCaseToCamelCase} from '../util';
// Group 1 = "bind-"
// Group 2 = "var-" or "#"
// Group 3 = "on-"
// Group 4 = "onbubble-"
// Group 5 = "bindon-"
// Group 6 = the identifier after "bind-", "var-/#", or "on-"
// Group 7 = idenitifer inside [()]
// Group 8 = idenitifer inside []
// Group 9 = identifier inside ()
var BIND_NAME_REGEXP =
/^(?:(?:(?:(bind-)|(var-|#)|(on-)|(onbubble-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g;
/**
* Parses the property bindings on a single element.
*/
export class PropertyBindingParser implements CompileStep {
constructor(private _parser: Parser) {}
processStyle(style: string): string { return style; }
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
var attrs = current.attrs();
var newAttrs = new Map();
MapWrapper.forEach(attrs, (attrValue, attrName) => {
attrName = this._normalizeAttributeName(attrName);
var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
if (isPresent(bindParts)) {
if (isPresent(bindParts[1])) { // match: bind-prop
this._bindProperty(bindParts[6], attrValue, current, newAttrs);
} else if (isPresent(
bindParts[2])) { // match: var-name / var-name="iden" / #name / #name="iden"
var identifier = bindParts[6];
var value = attrValue == '' ? '\$implicit' : attrValue;
this._bindVariable(identifier, value, current, newAttrs);
} else if (isPresent(bindParts[3])) { // match: on-event
this._bindEvent(bindParts[6], attrValue, current, newAttrs);
} else if (isPresent(bindParts[4])) { // match: onbubble-event
this._bindEvent('^' + bindParts[6], attrValue, current, newAttrs);
} else if (isPresent(bindParts[5])) { // match: bindon-prop
this._bindProperty(bindParts[6], attrValue, current, newAttrs);
this._bindAssignmentEvent(bindParts[6], attrValue, current, newAttrs);
} else if (isPresent(bindParts[7])) { // match: [(expr)]
this._bindProperty(bindParts[7], attrValue, current, newAttrs);
this._bindAssignmentEvent(bindParts[7], attrValue, current, newAttrs);
} else if (isPresent(bindParts[8])) { // match: [expr]
this._bindProperty(bindParts[8], attrValue, current, newAttrs);
} else if (isPresent(bindParts[9])) { // match: (event)
this._bindEvent(bindParts[9], attrValue, current, newAttrs);
}
} else {
var expr = this._parser.parseInterpolation(attrValue, current.elementDescription);
if (isPresent(expr)) {
this._bindPropertyAst(attrName, expr, current, newAttrs);
}
}
});
MapWrapper.forEach(newAttrs, (attrValue, attrName) => { attrs.set(attrName, attrValue); });
}
_normalizeAttributeName(attrName: string): string {
return StringWrapper.startsWith(attrName, 'data-') ? StringWrapper.substring(attrName, 5) :
attrName;
}
_bindVariable(identifier, value, current: CompileElement, newAttrs: Map<any, any>) {
current.bindElement().bindVariable(dashCaseToCamelCase(identifier), value);
newAttrs.set(identifier, value);
}
_bindProperty(name, expression, current: CompileElement, newAttrs) {
this._bindPropertyAst(name, this._parser.parseBinding(expression, current.elementDescription),
current, newAttrs);
}
_bindPropertyAst(name, ast, current: CompileElement, newAttrs: Map<any, any>) {
var binder = current.bindElement();
binder.bindProperty(dashCaseToCamelCase(name), ast);
newAttrs.set(name, ast.source);
}
_bindAssignmentEvent(name, expression, current: CompileElement, newAttrs) {
this._bindEvent(name, `${expression}=$event`, current, newAttrs);
}
_bindEvent(name, expression, current: CompileElement, newAttrs) {
current.bindElement().bindEvent(
dashCaseToCamelCase(name),
this._parser.parseAction(expression, current.elementDescription));
// Don't detect directives for event names for now,
// so don't add the event name to the CompileElement.attrs
}
}

View File

@ -0,0 +1,387 @@
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {
isPresent,
isBlank,
RegExpWrapper,
RegExpMatcherWrapper,
StringWrapper,
BaseException
} from 'angular2/src/facade/lang';
const _EMPTY_ATTR_VALUE = '';
// TODO: Can't use `const` here as
// in Dart this is not transpiled into `final` yet...
var _SELECTOR_REGEXP = RegExpWrapper.create(
'(\\:not\\()|' + //":not("
'([-\\w]+)|' + // "tag"
'(?:\\.([-\\w]+))|' + // ".class"
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]"
'(\\))|' + // ")"
'(\\s*,\\s*)'); // ","
/**
* A css selector contains an element name,
* css classes and attribute/value pairs with the purpose
* of selecting subsets out of them.
*/
export class CssSelector {
element: string = null;
classNames: string[] = [];
attrs: string[] = [];
notSelectors: CssSelector[] = [];
static parse(selector: string): CssSelector[] {
var results: CssSelector[] = [];
var _addResult = (res: CssSelector[], cssSel) => {
if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) &&
ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) {
cssSel.element = "*";
}
res.push(cssSel);
};
var cssSelector = new CssSelector();
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
var match;
var current = cssSelector;
var inNot = false;
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
if (isPresent(match[1])) {
if (inNot) {
throw new BaseException('Nesting :not is not allowed in a selector');
}
inNot = true;
current = new CssSelector();
cssSelector.notSelectors.push(current);
}
if (isPresent(match[2])) {
current.setElement(match[2]);
}
if (isPresent(match[3])) {
current.addClassName(match[3]);
}
if (isPresent(match[4])) {
current.addAttribute(match[4], match[5]);
}
if (isPresent(match[6])) {
inNot = false;
current = cssSelector;
}
if (isPresent(match[7])) {
if (inNot) {
throw new BaseException('Multiple selectors in :not are not supported');
}
_addResult(results, cssSelector);
cssSelector = current = new CssSelector();
}
}
_addResult(results, cssSelector);
return results;
}
isElementSelector(): boolean {
return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) &&
ListWrapper.isEmpty(this.attrs) && this.notSelectors.length === 0;
}
setElement(element: string = null) {
if (isPresent(element)) {
element = element.toLowerCase();
}
this.element = element;
}
/** Gets a template string for an element that matches the selector. */
getMatchingElementTemplate(): string {
let tagName = isPresent(this.element) ? this.element : 'div';
let classAttr = this.classNames.length > 0 ? ` class="${this.classNames.join(' ')}"` : '';
let attrs = '';
for (let i = 0; i < this.attrs.length; i += 2) {
let attrName = this.attrs[i];
let attrValue = this.attrs[i + 1] !== '' ? `="${this.attrs[i + 1]}"` : '';
attrs += ` ${attrName}${attrValue}`;
}
return `<${tagName}${classAttr}${attrs}></${tagName}>`;
}
addAttribute(name: string, value: string = _EMPTY_ATTR_VALUE) {
this.attrs.push(name.toLowerCase());
if (isPresent(value)) {
value = value.toLowerCase();
} else {
value = _EMPTY_ATTR_VALUE;
}
this.attrs.push(value);
}
addClassName(name: string) { this.classNames.push(name.toLowerCase()); }
toString(): string {
var res = '';
if (isPresent(this.element)) {
res += this.element;
}
if (isPresent(this.classNames)) {
for (var i = 0; i < this.classNames.length; i++) {
res += '.' + this.classNames[i];
}
}
if (isPresent(this.attrs)) {
for (var i = 0; i < this.attrs.length;) {
var attrName = this.attrs[i++];
var attrValue = this.attrs[i++];
res += '[' + attrName;
if (attrValue.length > 0) {
res += '=' + attrValue;
}
res += ']';
}
}
ListWrapper.forEach(this.notSelectors,
(notSelector) => { res += ":not(" + notSelector.toString() + ")"; });
return res;
}
}
/**
* Reads a list of CssSelectors and allows to calculate which ones
* are contained in a given CssSelector.
*/
export class SelectorMatcher {
static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
var notMatcher = new SelectorMatcher();
notMatcher.addSelectables(notSelectors, null);
return notMatcher;
}
private _elementMap: Map<string, SelectorContext[]> = new Map();
private _elementPartialMap: Map<string, SelectorMatcher> = new Map();
private _classMap: Map<string, SelectorContext[]> = new Map();
private _classPartialMap: Map<string, SelectorMatcher> = new Map();
private _attrValueMap: Map<string, Map<string, SelectorContext[]>> = new Map();
private _attrValuePartialMap: Map<string, Map<string, SelectorMatcher>> = new Map();
private _listContexts: SelectorListContext[] = [];
addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) {
var listContext = null;
if (cssSelectors.length > 1) {
listContext = new SelectorListContext(cssSelectors);
this._listContexts.push(listContext);
}
for (var i = 0; i < cssSelectors.length; i++) {
this._addSelectable(cssSelectors[i], callbackCtxt, listContext);
}
}
/**
* Add an object that can be found later on by calling `match`.
* @param cssSelector A css selector
* @param callbackCtxt An opaque object that will be given to the callback of the `match` function
*/
private _addSelectable(cssSelector: CssSelector, callbackCtxt: any,
listContext: SelectorListContext) {
var matcher = this;
var element = cssSelector.element;
var classNames = cssSelector.classNames;
var attrs = cssSelector.attrs;
var selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
if (isPresent(element)) {
var isTerminal = attrs.length === 0 && classNames.length === 0;
if (isTerminal) {
this._addTerminal(matcher._elementMap, element, selectable);
} else {
matcher = this._addPartial(matcher._elementPartialMap, element);
}
}
if (isPresent(classNames)) {
for (var index = 0; index < classNames.length; index++) {
var isTerminal = attrs.length === 0 && index === classNames.length - 1;
var className = classNames[index];
if (isTerminal) {
this._addTerminal(matcher._classMap, className, selectable);
} else {
matcher = this._addPartial(matcher._classPartialMap, className);
}
}
}
if (isPresent(attrs)) {
for (var index = 0; index < attrs.length;) {
var isTerminal = index === attrs.length - 2;
var attrName = attrs[index++];
var attrValue = attrs[index++];
if (isTerminal) {
var terminalMap = matcher._attrValueMap;
var terminalValuesMap = terminalMap.get(attrName);
if (isBlank(terminalValuesMap)) {
terminalValuesMap = new Map();
terminalMap.set(attrName, terminalValuesMap);
}
this._addTerminal(terminalValuesMap, attrValue, selectable);
} else {
var parttialMap = matcher._attrValuePartialMap;
var partialValuesMap = parttialMap.get(attrName);
if (isBlank(partialValuesMap)) {
partialValuesMap = new Map();
parttialMap.set(attrName, partialValuesMap);
}
matcher = this._addPartial(partialValuesMap, attrValue);
}
}
}
}
private _addTerminal(map: Map<string, SelectorContext[]>, name: string,
selectable: SelectorContext) {
var terminalList = map.get(name);
if (isBlank(terminalList)) {
terminalList = [];
map.set(name, terminalList);
}
terminalList.push(selectable);
}
private _addPartial(map: Map<string, SelectorMatcher>, name: string): SelectorMatcher {
var matcher = map.get(name);
if (isBlank(matcher)) {
matcher = new SelectorMatcher();
map.set(name, matcher);
}
return matcher;
}
/**
* Find the objects that have been added via `addSelectable`
* whose css selector is contained in the given css selector.
* @param cssSelector A css selector
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
* @return boolean true if a match was found
*/
match(cssSelector: CssSelector, matchedCallback: (CssSelector, any) => void): boolean {
var result = false;
var element = cssSelector.element;
var classNames = cssSelector.classNames;
var attrs = cssSelector.attrs;
for (var i = 0; i < this._listContexts.length; i++) {
this._listContexts[i].alreadyMatched = false;
}
result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) ||
result;
if (isPresent(classNames)) {
for (var index = 0; index < classNames.length; index++) {
var className = classNames[index];
result =
this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
result =
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) ||
result;
}
}
if (isPresent(attrs)) {
for (var index = 0; index < attrs.length;) {
var attrName = attrs[index++];
var attrValue = attrs[index++];
var terminalValuesMap = this._attrValueMap.get(attrName);
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
result = this._matchTerminal(terminalValuesMap, _EMPTY_ATTR_VALUE, cssSelector,
matchedCallback) ||
result;
}
result = this._matchTerminal(terminalValuesMap, attrValue, cssSelector, matchedCallback) ||
result;
var partialValuesMap = this._attrValuePartialMap.get(attrName);
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
result = this._matchPartial(partialValuesMap, _EMPTY_ATTR_VALUE, cssSelector,
matchedCallback) ||
result;
}
result =
this._matchPartial(partialValuesMap, attrValue, cssSelector, matchedCallback) || result;
}
}
return result;
}
_matchTerminal(map: Map<string, SelectorContext[]>, name, cssSelector: CssSelector,
matchedCallback: (CssSelector, any) => void): boolean {
if (isBlank(map) || isBlank(name)) {
return false;
}
var selectables = map.get(name);
var starSelectables = map.get("*");
if (isPresent(starSelectables)) {
selectables = ListWrapper.concat(selectables, starSelectables);
}
if (isBlank(selectables)) {
return false;
}
var selectable;
var result = false;
for (var index = 0; index < selectables.length; index++) {
selectable = selectables[index];
result = selectable.finalize(cssSelector, matchedCallback) || result;
}
return result;
}
_matchPartial(map: Map<string, SelectorMatcher>, name, cssSelector: CssSelector,
matchedCallback /*: (CssSelector, any) => void*/): boolean {
if (isBlank(map) || isBlank(name)) {
return false;
}
var nestedSelector = map.get(name);
if (isBlank(nestedSelector)) {
return false;
}
// TODO(perf): get rid of recursion and measure again
// TODO(perf): don't pass the whole selector into the recursion,
// but only the not processed parts
return nestedSelector.match(cssSelector, matchedCallback);
}
}
export class SelectorListContext {
alreadyMatched: boolean = false;
constructor(public selectors: CssSelector[]) {}
}
// Store context to pass back selector and context when a selector is matched
export class SelectorContext {
notSelectors: CssSelector[];
constructor(public selector: CssSelector, public cbContext: any,
public listContext: SelectorListContext) {
this.notSelectors = selector.notSelectors;
}
finalize(cssSelector: CssSelector, callback: (CssSelector, any) => void): boolean {
var result = true;
if (this.notSelectors.length > 0 &&
(isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
var notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
result = !notMatcher.match(cssSelector, null);
}
if (result && isPresent(callback) &&
(isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
if (isPresent(this.listContext)) {
this.listContext.alreadyMatched = true;
}
callback(this.selector, this.cbContext);
}
return result;
}
}

View File

@ -0,0 +1,530 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {
StringWrapper,
RegExp,
RegExpWrapper,
RegExpMatcherWrapper,
isPresent,
isBlank,
BaseException
} from 'angular2/src/facade/lang';
/**
* This file is a port of shadowCSS from webcomponents.js to TypeScript.
*
* Please make sure to keep to edits in sync with the source file.
*
* Source:
* https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
*
* The original file level comment is reproduced below
*/
/*
This is a limited shim for ShadowDOM css styling.
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles
The intention here is to support only the styling features which can be
relatively simply implemented. The goal is to allow users to avoid the
most obvious pitfalls and do so without compromising performance significantly.
For ShadowDOM styling that's not covered here, a set of best practices
can be provided that should allow users to accomplish more complex styling.
The following is a list of specific ShadowDOM styling features and a brief
discussion of the approach used to shim.
Shimmed features:
* :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
element using the :host rule. To shim this feature, the :host styles are
reformatted and prefixed with a given scope name and promoted to a
document level stylesheet.
For example, given a scope name of .foo, a rule like this:
:host {
background: red;
}
}
becomes:
.foo {
background: red;
}
* encapsultion: Styles defined within ShadowDOM, apply only to
dom inside the ShadowDOM. Polymer uses one of two techniques to imlement
this feature.
By default, rules are prefixed with the host element tag name
as a descendant selector. This ensures styling does not leak out of the 'top'
of the element's ShadowDOM. For example,
div {
font-weight: bold;
}
becomes:
x-foo div {
font-weight: bold;
}
becomes:
Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
selectors are scoped by adding an attribute selector suffix to each
simple selector that contains the host element tag name. Each element
in the element's ShadowDOM template is also given the scope attribute.
Thus, these rules match only elements that have the scope attribute.
For example, given a scope name of x-foo, a rule like this:
div {
font-weight: bold;
}
becomes:
div[x-foo] {
font-weight: bold;
}
Note that elements that are dynamically added to a scope must have the scope
selector added to them manually.
* upper/lower bound encapsulation: Styles which are defined outside a
shadowRoot should not cross the ShadowDOM boundary and should not apply
inside a shadowRoot.
This styling behavior is not emulated. Some possible ways to do this that
were rejected due to complexity and/or performance concerns include: (1) reset
every possible property for every possible selector for a given scope name;
(2) re-implement css in javascript.
As an alternative, users should make sure to use selectors
specific to the scope in which they are working.
* ::distributed: This behavior is not emulated. It's often not necessary
to style the contents of a specific insertion point and instead, descendants
of the host element can be styled selectively. Users can also create an
extra node around an insertion point and style that node's contents
via descendent selectors. For example, with a shadowRoot like this:
<style>
::content(div) {
background: red;
}
</style>
<content></content>
could become:
<style>
/ *@polyfill .content-container div * /
::content(div) {
background: red;
}
</style>
<div class="content-container">
<content></content>
</div>
Note the use of @polyfill in the comment above a ShadowDOM specific style
declaration. This is a directive to the styling shim to use the selector
in comments in lieu of the next selector when running under polyfill.
*/
export class ShadowCss {
strictStyling: boolean = true;
constructor() {}
/*
* Shim a style element with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*/
shimStyle(style: string, selector: string, hostSelector: string = ''): string {
var cssText = DOM.getText(style);
return this.shimCssText(cssText, selector, hostSelector);
}
/*
* Shim some cssText with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*
* When strictStyling is true:
* - selector is the attribute added to all elements inside the host,
* - hostSelector is the attribute added to the host itself.
*/
shimCssText(cssText: string, selector: string, hostSelector: string = ''): string {
cssText = this._insertDirectives(cssText);
return this._scopeCssText(cssText, selector, hostSelector);
}
_insertDirectives(cssText: string): string {
cssText = this._insertPolyfillDirectivesInCssText(cssText);
return this._insertPolyfillRulesInCssText(cssText);
}
/*
* Process styles to convert native ShadowDOM rules that will trip
* up the css parser; we rely on decorating the stylesheet with inert rules.
*
* For example, we convert this rule:
*
* polyfill-next-selector { content: ':host menu-item'; }
* ::content menu-item {
*
* to this:
*
* scopeName menu-item {
*
**/
_insertPolyfillDirectivesInCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
return StringWrapper.replaceAllMapped(cssText, _cssContentNextSelectorRe,
function(m) { return m[1] + '{'; });
}
/*
* Process styles to add rules which will only apply under the polyfill
*
* For example, we convert this rule:
*
* polyfill-rule {
* content: ':host menu-item';
* ...
* }
*
* to this:
*
* scopeName menu-item {...}
*
**/
_insertPolyfillRulesInCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
return StringWrapper.replaceAllMapped(cssText, _cssContentRuleRe, function(m) {
var rule = m[0];
rule = StringWrapper.replace(rule, m[1], '');
rule = StringWrapper.replace(rule, m[2], '');
return m[3] + rule;
});
}
/* Ensure styles are scoped. Pseudo-scoping takes a rule like:
*
* .foo {... }
*
* and converts this to
*
* scopeName .foo { ... }
*/
_scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
var unscoped = this._extractUnscopedRulesFromCssText(cssText);
cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText);
cssText = this._convertShadowDOMSelectors(cssText);
if (isPresent(scopeSelector)) {
_withCssRules(cssText,
(rules) => { cssText = this._scopeRules(rules, scopeSelector, hostSelector); });
}
cssText = cssText + '\n' + unscoped;
return cssText.trim();
}
/*
* Process styles to add rules which will only apply under the polyfill
* and do not process via CSSOM. (CSSOM is destructive to rules on rare
* occasions, e.g. -webkit-calc on Safari.)
* For example, we convert this rule:
*
* @polyfill-unscoped-rule {
* content: 'menu-item';
* ... }
*
* to this:
*
* menu-item {...}
*
**/
_extractUnscopedRulesFromCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
var r = '', m;
var matcher = RegExpWrapper.matcher(_cssContentUnscopedRuleRe, cssText);
while (isPresent(m = RegExpMatcherWrapper.next(matcher))) {
var rule = m[0];
rule = StringWrapper.replace(rule, m[2], '');
rule = StringWrapper.replace(rule, m[1], m[3]);
r += rule + '\n\n';
}
return r;
}
/*
* convert a rule like :host(.foo) > .bar { }
*
* to
*
* scopeName.foo > .bar
*/
_convertColonHost(cssText: string): string {
return this._convertColonRule(cssText, _cssColonHostRe, this._colonHostPartReplacer);
}
/*
* convert a rule like :host-context(.foo) > .bar { }
*
* to
*
* scopeName.foo > .bar, .foo scopeName > .bar { }
*
* and
*
* :host-context(.foo:host) .bar { ... }
*
* to
*
* scopeName.foo .bar { ... }
*/
_convertColonHostContext(cssText: string): string {
return this._convertColonRule(cssText, _cssColonHostContextRe,
this._colonHostContextPartReplacer);
}
_convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
// p1 = :host, p2 = contents of (), p3 rest of rule
return StringWrapper.replaceAllMapped(cssText, regExp, function(m) {
if (isPresent(m[2])) {
var parts = m[2].split(','), r = [];
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
if (isBlank(p)) break;
p = p.trim();
r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
}
return r.join(',');
} else {
return _polyfillHostNoCombinator + m[3];
}
});
}
_colonHostContextPartReplacer(host: string, part: string, suffix: string): string {
if (StringWrapper.contains(part, _polyfillHost)) {
return this._colonHostPartReplacer(host, part, suffix);
} else {
return host + part + suffix + ', ' + part + ' ' + host + suffix;
}
}
_colonHostPartReplacer(host: string, part: string, suffix: string): string {
return host + StringWrapper.replace(part, _polyfillHost, '') + suffix;
}
/*
* Convert combinators like ::shadow and pseudo-elements like ::content
* by replacing with space.
*/
_convertShadowDOMSelectors(cssText: string): string {
for (var i = 0; i < _shadowDOMSelectorsRe.length; i++) {
cssText = StringWrapper.replaceAll(cssText, _shadowDOMSelectorsRe[i], ' ');
}
return cssText;
}
// change a selector like 'div' to 'name div'
_scopeRules(cssRules, scopeSelector: string, hostSelector: string): string {
var cssText = '';
if (isPresent(cssRules)) {
for (var i = 0; i < cssRules.length; i++) {
var rule = cssRules[i];
if (DOM.isStyleRule(rule) || DOM.isPageRule(rule)) {
cssText += this._scopeSelector(rule.selectorText, scopeSelector, hostSelector,
this.strictStyling) +
' {\n';
cssText += this._propertiesFromRule(rule) + '\n}\n\n';
} else if (DOM.isMediaRule(rule)) {
cssText += '@media ' + rule.media.mediaText + ' {\n';
cssText += this._scopeRules(rule.cssRules, scopeSelector, hostSelector);
cssText += '\n}\n\n';
} else {
// KEYFRAMES_RULE in IE throws when we query cssText
// when it contains a -webkit- property.
// if this happens, we fallback to constructing the rule
// from the CSSRuleSet
// https://connect.microsoft.com/IE/feedbackdetail/view/955703/accessing-csstext-of-a-keyframe-rule-that-contains-a-webkit-property-via-cssom-generates-exception
try {
if (isPresent(rule.cssText)) {
cssText += rule.cssText + '\n\n';
}
} catch (x) {
if (DOM.isKeyframesRule(rule) && isPresent(rule.cssRules)) {
cssText += this._ieSafeCssTextFromKeyFrameRule(rule);
}
}
}
}
}
return cssText;
}
_ieSafeCssTextFromKeyFrameRule(rule): string {
var cssText = '@keyframes ' + rule.name + ' {';
for (var i = 0; i < rule.cssRules.length; i++) {
var r = rule.cssRules[i];
cssText += ' ' + r.keyText + ' {' + r.style.cssText + '}';
}
cssText += ' }';
return cssText;
}
_scopeSelector(selector: string, scopeSelector: string, hostSelector: string,
strict: boolean): string {
var r = [], parts = selector.split(',');
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
p = p.trim();
if (this._selectorNeedsScoping(p, scopeSelector)) {
p = strict && !StringWrapper.contains(p, _polyfillHostNoCombinator) ?
this._applyStrictSelectorScope(p, scopeSelector) :
this._applySelectorScope(p, scopeSelector, hostSelector);
}
r.push(p);
}
return r.join(', ');
}
_selectorNeedsScoping(selector: string, scopeSelector: string): boolean {
var re = this._makeScopeMatcher(scopeSelector);
return !isPresent(RegExpWrapper.firstMatch(re, selector));
}
_makeScopeMatcher(scopeSelector: string): RegExp {
var lre = /\[/g;
var rre = /\]/g;
scopeSelector = StringWrapper.replaceAll(scopeSelector, lre, '\\[');
scopeSelector = StringWrapper.replaceAll(scopeSelector, rre, '\\]');
return RegExpWrapper.create('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
}
_applySelectorScope(selector: string, scopeSelector: string, hostSelector: string): string {
// Difference from webcomponentsjs: scopeSelector could not be an array
return this._applySimpleSelectorScope(selector, scopeSelector, hostSelector);
}
// scope via name and [is=name]
_applySimpleSelectorScope(selector: string, scopeSelector: string, hostSelector: string): string {
if (isPresent(RegExpWrapper.firstMatch(_polyfillHostRe, selector))) {
var replaceBy = this.strictStyling ? `[${hostSelector}]` : scopeSelector;
selector = StringWrapper.replace(selector, _polyfillHostNoCombinator, replaceBy);
return StringWrapper.replaceAll(selector, _polyfillHostRe, replaceBy + ' ');
} else {
return scopeSelector + ' ' + selector;
}
}
// return a selector with [name] suffix on each simple selector
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name]
_applyStrictSelectorScope(selector: string, scopeSelector: string): string {
var isRe = /\[is=([^\]]*)\]/g;
scopeSelector = StringWrapper.replaceAllMapped(scopeSelector, isRe, (m) => m[1]);
var splits = [' ', '>', '+', '~'], scoped = selector, attrName = '[' + scopeSelector + ']';
for (var i = 0; i < splits.length; i++) {
var sep = splits[i];
var parts = scoped.split(sep);
scoped = ListWrapper.map(parts, function(p) {
// remove :host since it should be unnecessary
var t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, '');
if (t.length > 0 && !ListWrapper.contains(splits, t) &&
!StringWrapper.contains(t, attrName)) {
var re = /([^:]*)(:*)(.*)/g;
var m = RegExpWrapper.firstMatch(re, t);
if (isPresent(m)) {
p = m[1] + attrName + m[2] + m[3];
}
}
return p;
}).join(sep);
}
return scoped;
}
_insertPolyfillHostInCssText(selector: string): string {
selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext);
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost);
return selector;
}
_propertiesFromRule(rule): string {
var cssText = rule.style.cssText;
// TODO(sorvell): Safari cssom incorrectly removes quotes from the content
// property. (https://bugs.webkit.org/show_bug.cgi?id=118045)
// don't replace attr rules
var attrRe = /['"]+|attr/g;
if (rule.style.content.length > 0 &&
!isPresent(RegExpWrapper.firstMatch(attrRe, rule.style.content))) {
var contentRe = /content:[^;]*;/g;
cssText =
StringWrapper.replaceAll(cssText, contentRe, 'content: \'' + rule.style.content + '\';');
}
// TODO(sorvell): we can workaround this issue here, but we need a list
// of troublesome properties to fix https://github.com/Polymer/platform/issues/53
//
// inherit rules can be omitted from cssText
// TODO(sorvell): remove when Blink bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=358273
// var style = rule.style;
// for (var i = 0; i < style.length; i++) {
// var name = style.item(i);
// var value = style.getPropertyValue(name);
// if (value == 'initial') {
// cssText += name + ': initial; ';
// }
//}
return cssText;
}
}
var _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim;
var _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim;
var _cssContentUnscopedRuleRe =
/(polyfill-unscoped-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim;
var _polyfillHost = '-shadowcsshost';
// note: :host-context pre-processed to -shadowcsshostcontext.
var _polyfillHostContext = '-shadowcsscontext';
var _parenSuffix = ')(?:\\((' +
'(?:\\([^)(]*\\)|[^)(]*)+?' +
')\\))?([^,{]*)';
var _cssColonHostRe = RegExpWrapper.create('(' + _polyfillHost + _parenSuffix, 'im');
var _cssColonHostContextRe = RegExpWrapper.create('(' + _polyfillHostContext + _parenSuffix, 'im');
var _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
var _shadowDOMSelectorsRe = [
/>>>/g,
/::shadow/g,
/::content/g,
// Deprecated selectors
// TODO(vicb): see https://github.com/angular/clang-format/issues/16
// clang-format off
/\/deep\//g, // former >>>
/\/shadow-deep\//g, // former /deep/
/\/shadow\//g, // former ::shadow
// clanf-format on
];
var _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
var _polyfillHostRe = RegExpWrapper.create(_polyfillHost, 'im');
var _colonHostRe = /:host/gim;
var _colonHostContextRe = /:host-context/gim;
function _cssToRules(cssText: string) {
return DOM.cssToRules(cssText);
}
function _withCssRules(cssText: string, callback: Function) {
// Difference from webcomponentjs: remove the workaround for an old bug in Chrome
if (isBlank(callback)) return;
var rules = _cssToRules(cssText);
callback(rules);
}

View File

@ -0,0 +1,73 @@
import {CompileStep} from '../compiler/compile_step';
import {CompileElement} from '../compiler/compile_element';
import {CompileControl} from '../compiler/compile_control';
import {ViewDefinition, ViewEncapsulation, ViewType} from '../../api';
import {NG_CONTENT_ELEMENT_NAME, isElementWithTag} from '../util';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {ShadowCss} from './shadow_css';
export class StyleEncapsulator implements CompileStep {
constructor(private _appId: string, private _view: ViewDefinition,
private _componentUIDsCache: Map<string, string>) {}
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
if (isElementWithTag(current.element, NG_CONTENT_ELEMENT_NAME)) {
current.inheritedProtoView.bindNgContent();
} else {
if (this._view.encapsulation === ViewEncapsulation.EMULATED) {
this._processEmulatedScopedElement(current, parent);
}
}
}
processStyle(style: string): string {
var encapsulation = this._view.encapsulation;
if (encapsulation === ViewEncapsulation.EMULATED) {
return this._shimCssForComponent(style, this._view.componentId);
} else {
return style;
}
}
_processEmulatedScopedElement(current: CompileElement, parent: CompileElement): void {
var element = current.element;
var hostComponentId = this._view.componentId;
var viewType = current.inheritedProtoView.type;
// Shim the element as a child of the compiled component
if (viewType !== ViewType.HOST && isPresent(hostComponentId)) {
var contentAttribute = getContentAttribute(this._getComponentId(hostComponentId));
DOM.setAttribute(element, contentAttribute, '');
// also shim the host
if (isBlank(parent) && viewType == ViewType.COMPONENT) {
var hostAttribute = getHostAttribute(this._getComponentId(hostComponentId));
current.inheritedProtoView.setHostAttribute(hostAttribute, '');
}
}
}
_shimCssForComponent(cssText: string, componentId: string): string {
var id = this._getComponentId(componentId);
var shadowCss = new ShadowCss();
return shadowCss.shimCssText(cssText, getContentAttribute(id), getHostAttribute(id));
}
_getComponentId(componentStringId: string): string {
var id = this._componentUIDsCache.get(componentStringId);
if (isBlank(id)) {
id = `${this._appId}-${this._componentUIDsCache.size}`;
this._componentUIDsCache.set(componentStringId, id);
}
return id;
}
}
// Return the attribute to be added to the component
function getHostAttribute(compId: string): string {
return `_nghost-${compId}`;
}
// Returns the attribute to be added on every single element nodes in the component
function getContentAttribute(compId: string): string {
return `_ngcontent-${compId}`;
}

View File

@ -0,0 +1,138 @@
import {Injectable} from 'angular2/di';
import {XHR} from 'angular2/src/render/xhr';
import {ListWrapper} from 'angular2/src/facade/collection';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {StyleUrlResolver} from './style_url_resolver';
import {
isBlank,
isPresent,
RegExp,
RegExpWrapper,
StringWrapper,
normalizeBlank,
isPromise
} from 'angular2/src/facade/lang';
import {
Promise,
PromiseWrapper,
} from 'angular2/src/facade/async';
/**
* Inline @import rules in the given CSS.
*
* When an @import rules is inlined, it's url are rewritten.
*/
@Injectable()
export class StyleInliner {
constructor(public _xhr: XHR, public _styleUrlResolver: StyleUrlResolver,
public _urlResolver: UrlResolver) {}
/**
* Inline the @imports rules in the given CSS text.
*
* The baseUrl is required to rewrite URLs in the inlined content.
*
* @param {string} cssText
* @param {string} baseUrl
* @returns {*} a Promise<string> when @import rules are present, a string otherwise
*/
inlineImports(cssText: string, baseUrl: string): Promise<string>| string {
return this._inlineImports(cssText, baseUrl, []);
}
_inlineImports(cssText: string, baseUrl: string, inlinedUrls: List<string>): Promise<string>|
string {
var partIndex = 0;
var parts = StringWrapper.split(cssText, _importRe);
if (parts.length === 1) {
// no @import rule found, return the original css
return cssText;
}
var promises = [];
while (partIndex < parts.length - 1) {
// prefix is the content before the @import rule
var prefix = parts[partIndex];
// rule is the parameter of the @import rule
var rule = parts[partIndex + 1];
var url = _extractUrl(rule);
if (isPresent(url)) {
url = this._urlResolver.resolve(baseUrl, url);
}
var mediaQuery = _extractMediaQuery(rule);
var promise;
if (isBlank(url)) {
promise = PromiseWrapper.resolve(`/* Invalid import rule: "@import ${rule};" */`);
} else if (ListWrapper.contains(inlinedUrls, url)) {
// The current import rule has already been inlined, return the prefix only
// Importing again might cause a circular dependency
promise = PromiseWrapper.resolve(prefix);
} else {
inlinedUrls.push(url);
promise = PromiseWrapper.then(this._xhr.get(url), (rawCss) => {
// resolve nested @import rules
var inlinedCss = this._inlineImports(rawCss, url, inlinedUrls);
if (isPromise(inlinedCss)) {
// wait until nested @import are inlined
return (<Promise<string>>inlinedCss)
.then((css) => prefix + this._transformImportedCss(css, mediaQuery, url) + '\n');
} else {
// there are no nested @import, return the css
return prefix + this._transformImportedCss(<string>inlinedCss, mediaQuery, url) + '\n';
}
}, (error) => `/* failed to import ${url} */\n`);
}
promises.push(promise);
partIndex += 2;
}
return PromiseWrapper.all(promises).then(function(cssParts) {
var cssText = cssParts.join('');
if (partIndex < parts.length) {
// append then content located after the last @import rule
cssText += parts[partIndex];
}
return cssText;
});
}
_transformImportedCss(css: string, mediaQuery: string, url: string): string {
css = this._styleUrlResolver.resolveUrls(css, url);
return _wrapInMediaRule(css, mediaQuery);
}
}
// Extracts the url from an import rule, supported formats:
// - 'url' / "url",
// - url(url) / url('url') / url("url")
function _extractUrl(importRule: string): string {
var match = RegExpWrapper.firstMatch(_urlRe, importRule);
if (isBlank(match)) return null;
return isPresent(match[1]) ? match[1] : match[2];
}
// Extracts the media query from an import rule.
// Returns null when there is no media query.
function _extractMediaQuery(importRule: string): string {
var match = RegExpWrapper.firstMatch(_mediaQueryRe, importRule);
if (isBlank(match)) return null;
var mediaQuery = match[1].trim();
return (mediaQuery.length > 0) ? mediaQuery : null;
}
// Wraps the css in a media rule when the media query is not null
function _wrapInMediaRule(css: string, query: string): string {
return (isBlank(query)) ? css : `@media ${query} {\n${css}\n}`;
}
var _importRe = /@import\s+([^;]+);/g;
var _urlRe = RegExpWrapper.create(
'url\\(\\s*?[\'"]?([^\'")]+)[\'"]?|' + // url(url) or url('url') or url("url")
'[\'"]([^\'")]+)[\'"]' // "url" or 'url'
);
var _mediaQueryRe = /['"][^'"]+['"]\s*\)?\s*(.*)/g;

View File

@ -0,0 +1,42 @@
// Some of the code comes from WebComponents.JS
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
import {Injectable} from 'angular2/di';
import {RegExp, RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
import {UrlResolver} from 'angular2/src/services/url_resolver';
/**
* Rewrites URLs by resolving '@import' and 'url()' URLs from the given base URL.
*/
@Injectable()
export class StyleUrlResolver {
constructor(public _resolver: UrlResolver) {}
resolveUrls(cssText: string, baseUrl: string): string {
cssText = this._replaceUrls(cssText, _cssUrlRe, baseUrl);
cssText = this._replaceUrls(cssText, _cssImportRe, baseUrl);
return cssText;
}
_replaceUrls(cssText: string, re: RegExp, baseUrl: string) {
return StringWrapper.replaceAllMapped(cssText, re, (m) => {
var pre = m[1];
var originalUrl = m[2];
if (RegExpWrapper.test(_dataUrlRe, originalUrl)) {
// Do not attempt to resolve data: URLs
return m[0];
}
var url = StringWrapper.replaceAll(originalUrl, _quoteRe, '');
var post = m[3];
var resolvedUrl = this._resolver.resolve(baseUrl, url);
return pre + "'" + resolvedUrl + "'" + post;
});
}
}
var _cssUrlRe = /(url\()([^)]*)(\))/g;
var _cssImportRe = /(@import[\s]+(?!url\())['"]([^'"]*)['"](.*;)/g;
var _quoteRe = /['"]/g;
var _dataUrlRe = /^['"]?data:/g;

View File

@ -0,0 +1,41 @@
import {RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Parser} from 'angular2/src/change_detection/change_detection';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
/**
* Parses interpolations in direct text child nodes of the current element.
*/
export class TextInterpolationParser implements CompileStep {
constructor(public _parser: Parser) {}
processStyle(style: string): string { return style; }
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
if (!current.compileChildren) {
return;
}
var element = current.element;
var childNodes = DOM.childNodes(DOM.templateAwareRoot(element));
for (var i = 0; i < childNodes.length; i++) {
var node = childNodes[i];
if (DOM.isTextNode(node)) {
var textNode = <Text>node;
var text = DOM.nodeValue(textNode);
var expr = this._parser.parseInterpolation(text, current.elementDescription);
if (isPresent(expr)) {
DOM.setText(textNode, ' ');
if (current.element === current.inheritedProtoView.rootElement) {
current.inheritedProtoView.bindRootText(textNode, expr);
} else {
current.bindElement().bindText(textNode, expr);
}
}
}
}
}
}

View File

@ -0,0 +1,168 @@
import {Injectable} from 'angular2/di';
import {
isBlank,
isPresent,
BaseException,
stringify,
isPromise,
StringWrapper
} from 'angular2/src/facade/lang';
import {Map, MapWrapper, ListWrapper, List} from 'angular2/src/facade/collection';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ViewDefinition} from '../../api';
import {XHR} from 'angular2/src/render/xhr';
import {StyleInliner} from './style_inliner';
import {StyleUrlResolver} from './style_url_resolver';
import {wtfStartTimeRange, wtfEndTimeRange} from '../../../profile/profile';
export class TemplateAndStyles {
constructor(public template: string, public styles: string[]) {}
}
/**
* Strategy to load component views.
* TODO: Make public API once we are more confident in this approach.
*/
@Injectable()
export class ViewLoader {
_cache: Map<string, Promise<string>> = new Map();
constructor(private _xhr: XHR, private _styleInliner: StyleInliner,
private _styleUrlResolver: StyleUrlResolver) {}
load(viewDef: ViewDefinition): Promise<TemplateAndStyles> {
var r = wtfStartTimeRange('ViewLoader#load()', stringify(viewDef.componentId));
let tplAndStyles: List<Promise<TemplateAndStyles>| Promise<string>| string> =
[this._loadHtml(viewDef.template, viewDef.templateAbsUrl, viewDef.componentId)];
if (isPresent(viewDef.styles)) {
viewDef.styles.forEach((cssText: string) => {
let textOrPromise = this._resolveAndInlineCssText(cssText, viewDef.templateAbsUrl);
tplAndStyles.push(textOrPromise);
});
}
if (isPresent(viewDef.styleAbsUrls)) {
viewDef.styleAbsUrls.forEach(url => {
let promise = this._loadText(url).then(
cssText => this._resolveAndInlineCssText(cssText, viewDef.templateAbsUrl));
tplAndStyles.push(promise);
});
}
// Inline the styles from the @View annotation
return PromiseWrapper.all(tplAndStyles)
.then((res: List<TemplateAndStyles | string>) => {
let loadedTplAndStyles = <TemplateAndStyles>res[0];
let styles = <string[]>ListWrapper.slice(res, 1);
var templateAndStyles = new TemplateAndStyles(loadedTplAndStyles.template,
loadedTplAndStyles.styles.concat(styles));
wtfEndTimeRange(r);
return templateAndStyles;
});
}
private _loadText(url: string): Promise<string> {
var response = this._cache.get(url);
if (isBlank(response)) {
// TODO(vicb): change error when TS gets fixed
// https://github.com/angular/angular/issues/2280
// throw new BaseException(`Failed to fetch url "${url}"`);
response = PromiseWrapper.catchError(
this._xhr.get(url),
_ => PromiseWrapper.reject(new BaseException(`Failed to fetch url "${url}"`), null));
this._cache.set(url, response);
}
return response;
}
// Load the html and inline any style tags
private _loadHtml(template: string, templateAbsUrl: string,
componentId: string): Promise<TemplateAndStyles> {
let html;
// Load the HTML
if (isPresent(template)) {
html = PromiseWrapper.resolve(template);
} else if (isPresent(templateAbsUrl)) {
html = this._loadText(templateAbsUrl);
} else {
throw new BaseException(
`View should have either the templateUrl or template property set but none was found for the '${componentId}' component`);
}
return html.then(html => {
var tplEl = DOM.createTemplate(html);
// Replace $baseUrl with the base url for the template
if (isPresent(templateAbsUrl) && templateAbsUrl.indexOf("/") >= 0) {
let baseUrl = templateAbsUrl.substring(0, templateAbsUrl.lastIndexOf("/"));
this._substituteBaseUrl(DOM.content(tplEl), baseUrl);
}
let styleEls = DOM.querySelectorAll(DOM.content(tplEl), 'STYLE');
let unresolvedStyles: string[] = [];
for (let i = 0; i < styleEls.length; i++) {
var styleEl = styleEls[i];
unresolvedStyles.push(DOM.getText(styleEl));
DOM.remove(styleEl);
}
let syncStyles: string[] = [];
let asyncStyles: Promise<string>[] = [];
// Inline the style tags from the html
for (let i = 0; i < styleEls.length; i++) {
let styleEl = styleEls[i];
let resolvedStyled = this._resolveAndInlineCssText(DOM.getText(styleEl), templateAbsUrl);
if (isPromise(resolvedStyled)) {
asyncStyles.push(<Promise<string>>resolvedStyled);
} else {
syncStyles.push(<string>resolvedStyled);
}
}
if (asyncStyles.length === 0) {
return PromiseWrapper.resolve(new TemplateAndStyles(DOM.getInnerHTML(tplEl), syncStyles));
} else {
return PromiseWrapper.all(asyncStyles)
.then(loadedStyles => new TemplateAndStyles(DOM.getInnerHTML(tplEl),
syncStyles.concat(<string[]>loadedStyles)));
}
});
}
/**
* Replace all occurrences of $baseUrl in the attributes of an element and its
* children with the base URL of the template.
*
* @param element The element to process
* @param baseUrl The base URL of the template.
* @private
*/
private _substituteBaseUrl(element, baseUrl: string): void {
if (DOM.isElementNode(element)) {
var attrs = DOM.attributeMap(element);
MapWrapper.forEach(attrs, (v, k) => {
if (isPresent(v) && v.indexOf('$baseUrl') >= 0) {
DOM.setAttribute(element, k, StringWrapper.replaceAll(v, /\$baseUrl/g, baseUrl));
}
});
}
let children = DOM.childNodes(element);
for (let i = 0; i < children.length; i++) {
if (DOM.isElementNode(children[i])) {
this._substituteBaseUrl(children[i], baseUrl);
}
}
}
private _resolveAndInlineCssText(cssText: string, baseUrl: string): string | Promise<string> {
cssText = this._styleUrlResolver.resolveUrls(cssText, baseUrl);
return this._styleInliner.inlineImports(cssText, baseUrl);
}
}

View File

@ -0,0 +1,124 @@
import {isBlank, isPresent, BaseException, StringWrapper} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {Parser} from 'angular2/src/change_detection/change_detection';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {dashCaseToCamelCase} from '../util';
/**
* Splits views at `<template>` elements or elements with `template` attribute:
* For `<template>` elements:
* - moves the content into a new and disconnected `<template>` element
* that is marked as view root.
*
* For elements with a `template` attribute:
* - replaces the element with an empty `<template>` element,
* parses the content of the `template` attribute and adds the information to that
* `<template>` element. Marks the elements as view root.
*
* Note: In both cases the root of the nested view is disconnected from its parent element.
* This is needed for browsers that don't support the `<template>` element
* as we want to do locate elements with bindings using `getElementsByClassName` later on,
* which should not descend into the nested view.
*/
export class ViewSplitter implements CompileStep {
constructor(public _parser: Parser) {}
processStyle(style: string): string { return style; }
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
var attrs = current.attrs();
var templateBindings = attrs.get('template');
var hasTemplateBinding = isPresent(templateBindings);
// look for template shortcuts such as *ng-if="condition" and treat them as template="if
// condition"
MapWrapper.forEach(attrs, (attrValue, attrName) => {
if (StringWrapper.startsWith(attrName, '*')) {
var key = StringWrapper.substring(attrName, 1); // remove the star
if (hasTemplateBinding) {
// 2nd template binding detected
throw new BaseException(`Only one template directive per element is allowed: ` +
`${templateBindings} and ${key} cannot be used simultaneously ` +
`in ${current.elementDescription}`);
} else {
templateBindings = (attrValue.length == 0) ? key : key + ' ' + attrValue;
hasTemplateBinding = true;
}
}
});
if (isPresent(parent)) {
if (DOM.isTemplateElement(current.element)) {
if (!current.isViewRoot) {
var viewRoot = new CompileElement(DOM.createTemplate(''));
viewRoot.inheritedProtoView = current.bindElement().bindNestedProtoView(viewRoot.element);
// viewRoot doesn't appear in the original template, so we associate
// the current element description to get a more meaningful message in case of error
viewRoot.elementDescription = current.elementDescription;
viewRoot.isViewRoot = true;
this._moveChildNodes(DOM.content(current.element), DOM.content(viewRoot.element));
control.addChild(viewRoot);
}
}
if (hasTemplateBinding) {
var anchor = new CompileElement(DOM.createTemplate(''));
anchor.inheritedProtoView = current.inheritedProtoView;
anchor.inheritedElementBinder = current.inheritedElementBinder;
anchor.distanceToInheritedBinder = current.distanceToInheritedBinder;
// newParent doesn't appear in the original template, so we associate
// the current element description to get a more meaningful message in case of error
anchor.elementDescription = current.elementDescription;
var viewRoot = new CompileElement(DOM.createTemplate(''));
viewRoot.inheritedProtoView = anchor.bindElement().bindNestedProtoView(viewRoot.element);
// viewRoot doesn't appear in the original template, so we associate
// the current element description to get a more meaningful message in case of error
viewRoot.elementDescription = current.elementDescription;
viewRoot.isViewRoot = true;
current.inheritedProtoView = viewRoot.inheritedProtoView;
current.inheritedElementBinder = null;
current.distanceToInheritedBinder = 0;
this._parseTemplateBindings(templateBindings, anchor);
DOM.insertBefore(current.element, anchor.element);
control.addParent(anchor);
DOM.appendChild(DOM.content(viewRoot.element), current.element);
control.addParent(viewRoot);
}
}
}
_moveChildNodes(source, target) {
var next = DOM.firstChild(source);
while (isPresent(next)) {
DOM.appendChild(target, next);
next = DOM.firstChild(source);
}
}
_parseTemplateBindings(templateBindings: string, compileElement: CompileElement) {
var bindings =
this._parser.parseTemplateBindings(templateBindings, compileElement.elementDescription);
for (var i = 0; i < bindings.length; i++) {
var binding = bindings[i];
if (binding.keyIsVar) {
compileElement.bindElement().bindVariable(dashCaseToCamelCase(binding.key), binding.name);
compileElement.attrs().set(binding.key, binding.name);
} else if (isPresent(binding.expression)) {
compileElement.bindElement().bindProperty(dashCaseToCamelCase(binding.key),
binding.expression);
compileElement.attrs().set(binding.key, binding.expression.source);
} else {
DOM.setAttribute(compileElement.element, binding.key, '');
}
}
}
}

View File

@ -0,0 +1,67 @@
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {isPresent, isArray} from 'angular2/src/facade/lang';
import {RenderDirectiveMetadata} from 'angular2/src/render/api';
/**
* Converts a [DirectiveMetadata] to a map representation. This creates a copy,
* that is, subsequent changes to `meta` will not be mirrored in the map.
*/
export function directiveMetadataToMap(meta: RenderDirectiveMetadata): Map<string, any> {
return MapWrapper.createFromPairs([
['id', meta.id],
['selector', meta.selector],
['compileChildren', meta.compileChildren],
['hostProperties', _cloneIfPresent(meta.hostProperties)],
['hostListeners', _cloneIfPresent(meta.hostListeners)],
['hostActions', _cloneIfPresent(meta.hostActions)],
['hostAttributes', _cloneIfPresent(meta.hostAttributes)],
['properties', _cloneIfPresent(meta.properties)],
['readAttributes', _cloneIfPresent(meta.readAttributes)],
['type', meta.type],
['exportAs', meta.exportAs],
['callOnDestroy', meta.callOnDestroy],
['callOnCheck', meta.callOnCheck],
['callOnInit', meta.callOnInit],
['callOnChange', meta.callOnChange],
['callOnAllChangesDone', meta.callOnAllChangesDone],
['events', meta.events],
['changeDetection', meta.changeDetection],
['version', 1],
]);
}
/**
* Converts a map representation of [DirectiveMetadata] into a
* [DirectiveMetadata] object. This creates a copy, that is, subsequent changes
* to `map` will not be mirrored in the [DirectiveMetadata] object.
*/
export function directiveMetadataFromMap(map: Map<string, any>): RenderDirectiveMetadata {
return new RenderDirectiveMetadata({
id:<string>map.get('id'),
selector:<string>map.get('selector'),
compileChildren:<boolean>map.get('compileChildren'),
hostProperties:<Map<string, string>>_cloneIfPresent(map.get('hostProperties')),
hostListeners:<Map<string, string>>_cloneIfPresent(map.get('hostListeners')),
hostActions:<Map<string, string>>_cloneIfPresent(map.get('hostActions')),
hostAttributes:<Map<string, string>>_cloneIfPresent(map.get('hostAttributes')),
properties:<List<string>>_cloneIfPresent(map.get('properties')),
readAttributes:<List<string>>_cloneIfPresent(map.get('readAttributes')),
type:<number>map.get('type'),
exportAs:<string>map.get('exportAs'),
callOnDestroy:<boolean>map.get('callOnDestroy'),
callOnCheck:<boolean>map.get('callOnCheck'),
callOnChange:<boolean>map.get('callOnChange'),
callOnInit:<boolean>map.get('callOnInit'),
callOnAllChangesDone:<boolean>map.get('callOnAllChangesDone'),
events:<List<string>>_cloneIfPresent(map.get('events')),
changeDetection:<string>map.get('changeDetection'),
});
}
/**
* Clones the [List] or [Map] `o` if it is present.
*/
function _cloneIfPresent(o): any {
if (!isPresent(o)) return null;
return isArray(o) ? ListWrapper.clone(o) : MapWrapper.clone(o);
}

View File

@ -0,0 +1,290 @@
import {Inject, Injectable, OpaqueToken} from 'angular2/di';
import {
isPresent,
isBlank,
BaseException,
RegExpWrapper,
CONST_EXPR
} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {EventManager} from './events/event_manager';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './view/proto_view';
import {DomView, DomViewRef, resolveInternalDomView} from './view/view';
import {DomFragmentRef, resolveInternalDomFragment} from './view/fragment';
import {DomSharedStylesHost} from './view/shared_styles_host';
import {
NG_BINDING_CLASS_SELECTOR,
NG_BINDING_CLASS,
cloneAndQueryProtoView,
camelCaseToDashCase
} from './util';
import {WtfScopeFn, wtfLeave, wtfCreateScope} from '../../profile/profile';
import {
Renderer,
RenderProtoViewRef,
RenderViewRef,
RenderElementRef,
RenderFragmentRef,
RenderViewWithFragments
} from '../api';
import {TemplateCloner} from './template_cloner';
import {DOCUMENT} from './dom_tokens';
const REFLECT_PREFIX: string = 'ng-reflect-';
@Injectable()
export class DomRenderer extends Renderer {
_document;
constructor(private _eventManager: EventManager,
private _domSharedStylesHost: DomSharedStylesHost,
private _templateCloner: TemplateCloner, @Inject(DOCUMENT) document) {
super();
this._document = document;
}
_scope_createRootHostView: WtfScopeFn = wtfCreateScope('DomRenderer#createRootHostView()');
createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number,
hostElementSelector: string): RenderViewWithFragments {
var s = this._scope_createRootHostView();
var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef);
var element = DOM.querySelector(this._document, hostElementSelector);
if (isBlank(element)) {
wtfLeave(s);
throw new BaseException(`The selector "${hostElementSelector}" did not match any elements`);
}
return wtfLeave(s, this._createView(hostProtoView, element));
}
_scope_createView = wtfCreateScope('DomRenderer#createView()');
createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments {
var s = this._scope_createView();
var protoView = resolveInternalDomProtoView(protoViewRef);
return wtfLeave(s, this._createView(protoView, null));
}
destroyView(viewRef: RenderViewRef) {
var view = resolveInternalDomView(viewRef);
var elementBinders = view.proto.elementBinders;
for (var i = 0; i < elementBinders.length; i++) {
var binder = elementBinders[i];
if (binder.hasNativeShadowRoot) {
this._domSharedStylesHost.removeHost(DOM.getShadowRoot(view.boundElements[i]));
}
}
}
getNativeElementSync(location: RenderElementRef): any {
if (isBlank(location.renderBoundElementIndex)) {
return null;
}
return resolveInternalDomView(location.renderView)
.boundElements[location.renderBoundElementIndex];
}
getRootNodes(fragment: RenderFragmentRef): List<Node> {
return resolveInternalDomFragment(fragment);
}
attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef,
fragmentRef: RenderFragmentRef) {
var previousFragmentNodes = resolveInternalDomFragment(previousFragmentRef);
if (previousFragmentNodes.length > 0) {
var sibling = previousFragmentNodes[previousFragmentNodes.length - 1];
moveNodesAfterSibling(sibling, resolveInternalDomFragment(fragmentRef));
}
}
attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) {
if (isBlank(elementRef.renderBoundElementIndex)) {
return;
}
var parentView = resolveInternalDomView(elementRef.renderView);
var element = parentView.boundElements[elementRef.renderBoundElementIndex];
moveNodesAfterSibling(element, resolveInternalDomFragment(fragmentRef));
}
_scope_detachFragment = wtfCreateScope('DomRenderer#detachFragment()');
detachFragment(fragmentRef: RenderFragmentRef) {
var s = this._scope_detachFragment();
var fragmentNodes = resolveInternalDomFragment(fragmentRef);
for (var i = 0; i < fragmentNodes.length; i++) {
DOM.remove(fragmentNodes[i]);
}
wtfLeave(s);
}
hydrateView(viewRef: RenderViewRef) {
var view = resolveInternalDomView(viewRef);
if (view.hydrated) throw new BaseException('The view is already hydrated.');
view.hydrated = true;
// add global events
view.eventHandlerRemovers = [];
var binders = view.proto.elementBinders;
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
if (isPresent(binder.globalEvents)) {
for (var i = 0; i < binder.globalEvents.length; i++) {
var globalEvent = binder.globalEvents[i];
var remover = this._createGlobalEventListener(view, binderIdx, globalEvent.name,
globalEvent.target, globalEvent.fullName);
view.eventHandlerRemovers.push(remover);
}
}
}
}
dehydrateView(viewRef: RenderViewRef) {
var view = resolveInternalDomView(viewRef);
// remove global events
for (var i = 0; i < view.eventHandlerRemovers.length; i++) {
view.eventHandlerRemovers[i]();
}
view.eventHandlerRemovers = null;
view.hydrated = false;
}
setElementProperty(location: RenderElementRef, propertyName: string, propertyValue: any): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementProperty(location.renderBoundElementIndex, propertyName, propertyValue);
}
setElementAttribute(location: RenderElementRef, attributeName: string, attributeValue: string):
void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementAttribute(location.renderBoundElementIndex, attributeName, attributeValue);
}
setElementClass(location: RenderElementRef, className: string, isAdd: boolean): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementClass(location.renderBoundElementIndex, className, isAdd);
}
setElementStyle(location: RenderElementRef, styleName: string, styleValue: string): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementStyle(location.renderBoundElementIndex, styleName, styleValue);
}
invokeElementMethod(location: RenderElementRef, methodName: string, args: List<any>): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.invokeElementMethod(location.renderBoundElementIndex, methodName, args);
}
setText(viewRef: RenderViewRef, textNodeIndex: number, text: string): void {
if (isBlank(textNodeIndex)) {
return;
}
var view = resolveInternalDomView(viewRef);
DOM.setText(view.boundTextNodes[textNodeIndex], text);
}
_scope_setEventDispatcher = wtfCreateScope('DomRenderer#setEventDispatcher()');
setEventDispatcher(viewRef: RenderViewRef, dispatcher: any /*api.EventDispatcher*/): void {
var s = this._scope_setEventDispatcher();
var view = resolveInternalDomView(viewRef);
view.eventDispatcher = dispatcher;
wtfLeave(s);
}
_createView(protoView: DomProtoView, inplaceElement: HTMLElement): RenderViewWithFragments {
var clonedProtoView = cloneAndQueryProtoView(this._templateCloner, protoView, true);
var boundElements = clonedProtoView.boundElements;
// adopt inplaceElement
if (isPresent(inplaceElement)) {
if (protoView.fragmentsRootNodeCount[0] !== 1) {
throw new BaseException('Root proto views can only contain one element!');
}
DOM.clearNodes(inplaceElement);
var tempRoot = clonedProtoView.fragments[0][0];
moveChildNodes(tempRoot, inplaceElement);
if (boundElements.length > 0 && boundElements[0] === tempRoot) {
boundElements[0] = inplaceElement;
}
clonedProtoView.fragments[0][0] = inplaceElement;
}
var view = new DomView(protoView, clonedProtoView.boundTextNodes, boundElements);
var binders = protoView.elementBinders;
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element = boundElements[binderIdx];
// native shadow DOM
if (binder.hasNativeShadowRoot) {
var shadowRootWrapper = DOM.firstChild(element);
var shadowRoot = DOM.createShadowRoot(element);
this._domSharedStylesHost.addHost(shadowRoot);
moveChildNodes(shadowRootWrapper, shadowRoot);
DOM.remove(shadowRootWrapper);
}
// events
if (isPresent(binder.eventLocals) && isPresent(binder.localEvents)) {
for (var i = 0; i < binder.localEvents.length; i++) {
this._createEventListener(view, element, binderIdx, binder.localEvents[i].name,
binder.eventLocals);
}
}
}
return new RenderViewWithFragments(
new DomViewRef(view), clonedProtoView.fragments.map(nodes => new DomFragmentRef(nodes)));
}
_createEventListener(view, element, elementIndex, eventName, eventLocals) {
this._eventManager.addEventListener(
element, eventName, (event) => { view.dispatchEvent(elementIndex, eventName, event); });
}
_createGlobalEventListener(view, elementIndex, eventName, eventTarget, fullName): Function {
return this._eventManager.addGlobalEventListener(
eventTarget, eventName, (event) => { view.dispatchEvent(elementIndex, fullName, event); });
}
}
function moveNodesAfterSibling(sibling, nodes) {
if (nodes.length > 0 && isPresent(DOM.parentElement(sibling))) {
for (var i = 0; i < nodes.length; i++) {
DOM.insertBefore(sibling, nodes[i]);
}
DOM.insertBefore(nodes[nodes.length - 1], sibling);
}
}
function moveChildNodes(source: Node, target: Node) {
var currChild = DOM.firstChild(source);
while (isPresent(currChild)) {
var nextChild = DOM.nextSibling(currChild);
DOM.appendChild(target, currChild);
currChild = nextChild;
}
}

View File

@ -0,0 +1,30 @@
import {OpaqueToken, Binding} from 'angular2/di';
import {CONST_EXPR, StringWrapper, Math} from 'angular2/src/facade/lang';
export const DOCUMENT: OpaqueToken = CONST_EXPR(new OpaqueToken('DocumentToken'));
/**
* A unique id (string) for an angular application.
*/
export const APP_ID: OpaqueToken = CONST_EXPR(new OpaqueToken('AppId'));
function _appIdRandomBindingFactory() {
return `${randomChar()}${randomChar()}${randomChar()}`;
}
/**
* Bindings that will generate a random APP_ID_TOKEN.
*/
export const APP_ID_RANDOM_BINDING: Binding =
CONST_EXPR(new Binding(APP_ID, {toFactory: _appIdRandomBindingFactory, deps: []}));
/**
* Defines when a compiled template should be stored as a string
* rather than keeping its Nodes to preserve memory.
*/
export const MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE: OpaqueToken =
CONST_EXPR(new OpaqueToken('MaxInMemoryElementsPerTemplate'));
function randomChar(): string {
return StringWrapper.fromCharCode(97 + Math.floor(Math.random() * 25));
}

View File

@ -0,0 +1,109 @@
import {isBlank, BaseException, isPresent, StringWrapper} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {NgZone} from 'angular2/src/core/zone/ng_zone';
var BUBBLE_SYMBOL = '^';
export class EventManager {
constructor(public _plugins: List<EventManagerPlugin>, public _zone: NgZone) {
for (var i = 0; i < _plugins.length; i++) {
_plugins[i].manager = this;
}
}
addEventListener(element: HTMLElement, eventName: string, handler: Function) {
var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
var plugin = this._findPluginFor(withoutBubbleSymbol);
plugin.addEventListener(element, withoutBubbleSymbol, handler,
withoutBubbleSymbol != eventName);
}
addGlobalEventListener(target: string, eventName: string, handler: Function): Function {
var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
var plugin = this._findPluginFor(withoutBubbleSymbol);
return plugin.addGlobalEventListener(target, withoutBubbleSymbol, handler,
withoutBubbleSymbol != eventName);
}
getZone(): NgZone { return this._zone; }
_findPluginFor(eventName: string): EventManagerPlugin {
var plugins = this._plugins;
for (var i = 0; i < plugins.length; i++) {
var plugin = plugins[i];
if (plugin.supports(eventName)) {
return plugin;
}
}
throw new BaseException(`No event manager plugin found for event ${eventName}`);
}
_removeBubbleSymbol(eventName: string): string {
return eventName[0] == BUBBLE_SYMBOL ? StringWrapper.substring(eventName, 1) : eventName;
}
}
export class EventManagerPlugin {
manager: EventManager;
// We are assuming here that all plugins support bubbled and non-bubbled events.
// That is equivalent to having supporting $event.target
// The bubbling flag (currently ^) is stripped before calling the supports and
// addEventListener methods.
supports(eventName: string): boolean { return false; }
addEventListener(element: HTMLElement, eventName: string, handler: Function,
shouldSupportBubble: boolean) {
throw "not implemented";
}
addGlobalEventListener(element: string, eventName: string, handler: Function,
shouldSupportBubble: boolean): Function {
throw "not implemented";
}
}
export class DomEventsPlugin extends EventManagerPlugin {
manager: EventManager;
// This plugin should come last in the list of plugins, because it accepts all
// events.
supports(eventName: string): boolean { return true; }
addEventListener(element: HTMLElement, eventName: string, handler: Function,
shouldSupportBubble: boolean) {
var outsideHandler =
this._getOutsideHandler(shouldSupportBubble, element, handler, this.manager._zone);
this.manager._zone.runOutsideAngular(() => { DOM.on(element, eventName, outsideHandler); });
}
addGlobalEventListener(target: string, eventName: string, handler: Function,
shouldSupportBubble: boolean): Function {
var element = DOM.getGlobalEventTarget(target);
var outsideHandler =
this._getOutsideHandler(shouldSupportBubble, element, handler, this.manager._zone);
return this.manager._zone.runOutsideAngular(
() => { return DOM.onAndCancel(element, eventName, outsideHandler); });
}
_getOutsideHandler(shouldSupportBubble: boolean, element: HTMLElement, handler: Function,
zone: NgZone) {
return shouldSupportBubble ? DomEventsPlugin.bubbleCallback(element, handler, zone) :
DomEventsPlugin.sameElementCallback(element, handler, zone);
}
static sameElementCallback(element: HTMLElement, handler: Function, zone: NgZone):
(event: Event) => void {
return (event) => {
if (event.target === element) {
zone.run(() => handler(event));
}
};
}
static bubbleCallback(element: HTMLElement, handler: Function, zone: NgZone):
(event: Event) => void {
return (event) => zone.run(() => handler(event));
}
}

View File

@ -0,0 +1,50 @@
import {EventManagerPlugin} from './event_manager';
import {StringMapWrapper} from 'angular2/src/facade/collection';
var _eventNames = {
// pan
'pan': true,
'panstart': true,
'panmove': true,
'panend': true,
'pancancel': true,
'panleft': true,
'panright': true,
'panup': true,
'pandown': true,
// pinch
'pinch': true,
'pinchstart': true,
'pinchmove': true,
'pinchend': true,
'pinchcancel': true,
'pinchin': true,
'pinchout': true,
// press
'press': true,
'pressup': true,
// rotate
'rotate': true,
'rotatestart': true,
'rotatemove': true,
'rotateend': true,
'rotatecancel': true,
// swipe
'swipe': true,
'swipeleft': true,
'swiperight': true,
'swipeup': true,
'swipedown': true,
// tap
'tap': true,
};
export class HammerGesturesPluginCommon extends EventManagerPlugin {
constructor() { super(); }
supports(eventName: string): boolean {
eventName = eventName.toLowerCase();
return StringMapWrapper.contains(_eventNames, eventName);
}
}

View File

@ -0,0 +1,93 @@
library angular.events;
import 'dart:html';
import './hammer_common.dart';
import 'package:angular2/src/facade/lang.dart' show BaseException;
import 'dart:js' as js;
class HammerGesturesPlugin extends HammerGesturesPluginCommon {
bool supports(String eventName) {
if (!super.supports(eventName)) return false;
if (!js.context.hasProperty('Hammer')) {
throw new BaseException(
'Hammer.js is not loaded, can not bind ${eventName} event');
}
return true;
}
addEventListener(Element element, String eventName, Function handler,
bool shouldSupportBubble) {
if (shouldSupportBubble) throw new BaseException(
'Hammer.js plugin does not support bubbling gestures.');
var zone = this.manager.getZone();
eventName = eventName.toLowerCase();
zone.runOutsideAngular(() {
// Creating the manager bind events, must be done outside of angular
var mc = new js.JsObject(js.context['Hammer'], [element]);
var jsObj = mc.callMethod('get', ['pinch']);
jsObj.callMethod('set', [
new js.JsObject.jsify({'enable': true})
]);
jsObj = mc.callMethod('get', ['rotate']);
jsObj.callMethod('set', [
new js.JsObject.jsify({'enable': true})
]);
mc.callMethod('on', [
eventName,
(eventObj) {
zone.run(() {
var dartEvent = new HammerEvent._fromJsEvent(eventObj);
handler(dartEvent);
});
}
]);
});
}
}
class HammerEvent {
num angle;
num centerX;
num centerY;
int deltaTime;
int deltaX;
int deltaY;
int direction;
int distance;
num rotation;
num scale;
Node target;
int timeStamp;
String type;
num velocity;
num velocityX;
num velocityY;
js.JsObject jsEvent;
HammerEvent._fromJsEvent(js.JsObject event) {
angle = event['angle'];
var center = event['center'];
centerX = center['x'];
centerY = center['y'];
deltaTime = event['deltaTime'];
deltaX = event['deltaX'];
deltaY = event['deltaY'];
direction = event['direction'];
distance = event['distance'];
rotation = event['rotation'];
scale = event['scale'];
target = event['target'];
timeStamp = event['timeStamp'];
type = event['type'];
velocity = event['velocity'];
velocityX = event['velocityX'];
velocityY = event['velocityY'];
jsEvent = event;
}
}

View File

@ -0,0 +1,35 @@
/// <reference path="../../../../typings/hammerjs/hammerjs.d.ts"/>
import {HammerGesturesPluginCommon} from './hammer_common';
import {isPresent, BaseException} from 'angular2/src/facade/lang';
export class HammerGesturesPlugin extends HammerGesturesPluginCommon {
constructor() { super(); }
supports(eventName: string): boolean {
if (!super.supports(eventName)) return false;
if (!isPresent(window['Hammer'])) {
throw new BaseException(`Hammer.js is not loaded, can not bind ${eventName} event`);
}
return true;
}
addEventListener(element: HTMLElement, eventName: string, handler: Function,
shouldSupportBubble: boolean) {
if (shouldSupportBubble)
throw new BaseException('Hammer.js plugin does not support bubbling gestures.');
var zone = this.manager.getZone();
eventName = eventName.toLowerCase();
zone.runOutsideAngular(function() {
// Creating the manager bind events, must be done outside of angular
var mc = new Hammer(element);
mc.get('pinch').set({enable: true});
mc.get('rotate').set({enable: true});
mc.on(eventName, function(eventObj) { zone.run(function() { handler(eventObj); }); });
});
}
}

View File

@ -0,0 +1,113 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
isPresent,
isBlank,
StringWrapper,
RegExpWrapper,
BaseException,
NumberWrapper
} from 'angular2/src/facade/lang';
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {EventManagerPlugin} from './event_manager';
import {NgZone} from 'angular2/src/core/zone/ng_zone';
var modifierKeys = ['alt', 'control', 'meta', 'shift'];
var modifierKeyGetters: StringMap<string, Function> = {
'alt': (event) => event.altKey,
'control': (event) => event.ctrlKey,
'meta': (event) => event.metaKey,
'shift': (event) => event.shiftKey
};
export class KeyEventsPlugin extends EventManagerPlugin {
constructor() { super(); }
supports(eventName: string): boolean {
return isPresent(KeyEventsPlugin.parseEventName(eventName));
}
addEventListener(element: HTMLElement, eventName: string, handler: (Event: any) => any,
shouldSupportBubble: boolean) {
var parsedEvent = KeyEventsPlugin.parseEventName(eventName);
var outsideHandler = KeyEventsPlugin.eventCallback(element, shouldSupportBubble,
StringMapWrapper.get(parsedEvent, 'fullKey'),
handler, this.manager.getZone());
this.manager.getZone().runOutsideAngular(() => {
DOM.on(element, StringMapWrapper.get(parsedEvent, 'domEventName'), outsideHandler);
});
}
static parseEventName(eventName: string): StringMap<string, string> {
var parts = eventName.toLowerCase().split('.');
var domEventName = ListWrapper.removeAt(parts, 0);
if ((parts.length === 0) ||
!(StringWrapper.equals(domEventName, 'keydown') ||
StringWrapper.equals(domEventName, 'keyup'))) {
return null;
}
var key = KeyEventsPlugin._normalizeKey(ListWrapper.removeLast(parts));
var fullKey = '';
ListWrapper.forEach(modifierKeys, (modifierName) => {
if (ListWrapper.contains(parts, modifierName)) {
ListWrapper.remove(parts, modifierName);
fullKey += modifierName + '.';
}
});
fullKey += key;
if (parts.length != 0 || key.length === 0) {
// returning null instead of throwing to let another plugin process the event
return null;
}
var result = StringMapWrapper.create();
StringMapWrapper.set(result, 'domEventName', domEventName);
StringMapWrapper.set(result, 'fullKey', fullKey);
return result;
}
static getEventFullKey(event: Event): string {
var fullKey = '';
var key = DOM.getEventKey(event);
key = key.toLowerCase();
if (StringWrapper.equals(key, ' ')) {
key = 'space'; // for readability
} else if (StringWrapper.equals(key, '.')) {
key = 'dot'; // because '.' is used as a separator in event names
}
ListWrapper.forEach(modifierKeys, (modifierName) => {
if (modifierName != key) {
var modifierGetter = StringMapWrapper.get(modifierKeyGetters, modifierName);
if (modifierGetter(event)) {
fullKey += modifierName + '.';
}
}
});
fullKey += key;
return fullKey;
}
static eventCallback(element: HTMLElement, shouldSupportBubble: boolean, fullKey: any,
handler: (Event) => any, zone: NgZone): (event: Event) => void {
return (event) => {
var correctElement = shouldSupportBubble || event.target === element;
if (correctElement && StringWrapper.equals(KeyEventsPlugin.getEventFullKey(event), fullKey)) {
zone.run(() => handler(event));
}
};
}
static _normalizeKey(keyName: string): string {
// TODO: switch to a StringMap if the mapping grows too much
switch (keyName) {
case 'esc':
return 'escape';
default:
return keyName;
}
}
}

View File

@ -0,0 +1,23 @@
import {isPresent} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ElementSchemaRegistry} from './element_schema_registry';
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
hasProperty(elm: any, propName: string): boolean {
var tagName = DOM.tagName(elm);
if (tagName.indexOf('-') !== -1) {
// can't tell now as we don't know which properties a custom element will get
// once it is instantiated
return true;
} else {
return DOM.hasProperty(elm, propName);
}
}
getMappedPropName(propName: string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, propName);
return isPresent(mappedPropName) ? mappedPropName : propName;
}
}

View File

@ -0,0 +1,4 @@
export class ElementSchemaRegistry {
hasProperty(elm: any, propName: string): boolean { return true; }
getMappedPropName(propName: string): string { return propName; }
}

View File

@ -0,0 +1,47 @@
import {isString} from 'angular2/src/facade/lang';
import {Injectable, Inject} from 'angular2/di';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE} from './dom_tokens';
@Injectable()
export class TemplateCloner {
maxInMemoryElementsPerTemplate: number;
constructor(@Inject(MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE) maxInMemoryElementsPerTemplate) {
this.maxInMemoryElementsPerTemplate = maxInMemoryElementsPerTemplate;
}
prepareForClone(templateRoot: Element): Element | string {
var elementCount = DOM.querySelectorAll(DOM.content(templateRoot), '*').length;
if (this.maxInMemoryElementsPerTemplate >= 0 &&
elementCount >= this.maxInMemoryElementsPerTemplate) {
return DOM.getInnerHTML(templateRoot);
} else {
return templateRoot;
}
}
cloneContent(preparedTemplateRoot: Element | string, importNode: boolean): Node {
var templateContent;
if (isString(preparedTemplateRoot)) {
templateContent = DOM.content(DOM.createTemplate(preparedTemplateRoot));
if (importNode) {
// Attention: We can't use document.adoptNode here
// as this does NOT wake up custom elements in Chrome 43
// TODO: Use div.innerHTML instead of template.innerHTML when we
// have code to support the various special cases and
// don't use importNode additionally (e.g. for <tr>, svg elements, ...)
// see https://github.com/angular/angular/issues/3364
templateContent = DOM.importIntoDoc(templateContent);
}
} else {
templateContent = DOM.content(<Element>preparedTemplateRoot);
if (importNode) {
templateContent = DOM.importIntoDoc(templateContent);
} else {
templateContent = DOM.clone(templateContent);
}
}
return templateContent;
}
}

View File

@ -0,0 +1,169 @@
import {StringWrapper, isPresent, isBlank} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper} from 'angular2/src/facade/collection';
import {DomProtoView} from './view/proto_view';
import {DomElementBinder} from './view/element_binder';
import {TemplateCloner} from './template_cloner';
export const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
export const NG_BINDING_CLASS = 'ng-binding';
export const EVENT_TARGET_SEPARATOR = ':';
export const NG_CONTENT_ELEMENT_NAME = 'ng-content';
export const NG_SHADOW_ROOT_ELEMENT_NAME = 'shadow-root';
const MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE = 20;
var CAMEL_CASE_REGEXP = /([A-Z])/g;
var DASH_CASE_REGEXP = /-([a-z])/g;
export function camelCaseToDashCase(input: string): string {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP,
(m) => { return '-' + m[1].toLowerCase(); });
}
export function dashCaseToCamelCase(input: string): string {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP,
(m) => { return m[1].toUpperCase(); });
}
export class EventConfig {
constructor(public fieldName: string, public eventName: string, public isLongForm: boolean) {}
static parse(eventConfig: string): EventConfig {
var fieldName = eventConfig, eventName = eventConfig, isLongForm = false;
var separatorIdx = eventConfig.indexOf(EVENT_TARGET_SEPARATOR);
if (separatorIdx > -1) {
// long format: 'fieldName: eventName'
fieldName = StringWrapper.substring(eventConfig, 0, separatorIdx).trim();
eventName = StringWrapper.substring(eventConfig, separatorIdx + 1).trim();
isLongForm = true;
}
return new EventConfig(fieldName, eventName, isLongForm);
}
getFullName(): string {
return this.isLongForm ? `${this.fieldName}${EVENT_TARGET_SEPARATOR}${this.eventName}` :
this.eventName;
}
}
// Attention: This is on the hot path, so don't use closures or default values!
export function queryBoundElements(templateContent: Node, isSingleElementChild: boolean):
Element[] {
var result;
var dynamicElementList;
var elementIdx = 0;
if (isSingleElementChild) {
var rootElement = DOM.firstChild(templateContent);
var rootHasBinding = DOM.hasClass(rootElement, NG_BINDING_CLASS);
dynamicElementList = DOM.getElementsByClassName(rootElement, NG_BINDING_CLASS);
result = ListWrapper.createFixedSize(dynamicElementList.length + (rootHasBinding ? 1 : 0));
if (rootHasBinding) {
result[elementIdx++] = rootElement;
}
} else {
dynamicElementList = DOM.querySelectorAll(templateContent, NG_BINDING_CLASS_SELECTOR);
result = ListWrapper.createFixedSize(dynamicElementList.length);
}
for (var i = 0; i < dynamicElementList.length; i++) {
result[elementIdx++] = dynamicElementList[i];
}
return result;
}
export class ClonedProtoView {
constructor(public original: DomProtoView, public fragments: Node[][],
public boundElements: Element[], public boundTextNodes: Node[]) {}
}
export function cloneAndQueryProtoView(templateCloner: TemplateCloner, pv: DomProtoView,
importIntoDocument: boolean): ClonedProtoView {
var templateContent = templateCloner.cloneContent(pv.cloneableTemplate, importIntoDocument);
var boundElements = queryBoundElements(templateContent, pv.isSingleElementFragment);
var boundTextNodes = queryBoundTextNodes(templateContent, pv.rootTextNodeIndices, boundElements,
pv.elementBinders, pv.boundTextNodeCount);
var fragments = queryFragments(templateContent, pv.fragmentsRootNodeCount);
return new ClonedProtoView(pv, fragments, boundElements, boundTextNodes);
}
function queryFragments(templateContent: Node, fragmentsRootNodeCount: number[]): Node[][] {
var fragments = ListWrapper.createGrowableSize(fragmentsRootNodeCount.length);
// Note: An explicit loop is the fastest way to convert a DOM array into a JS array!
var childNode = DOM.firstChild(templateContent);
for (var fragmentIndex = 0; fragmentIndex < fragments.length; fragmentIndex++) {
var fragment = ListWrapper.createFixedSize(fragmentsRootNodeCount[fragmentIndex]);
fragments[fragmentIndex] = fragment;
// Note: the 2nd, 3rd, ... fragments are separated by each other via a '|'
if (fragmentIndex >= 1) {
childNode = DOM.nextSibling(childNode);
}
for (var i = 0; i < fragment.length; i++) {
fragment[i] = childNode;
childNode = DOM.nextSibling(childNode);
}
}
return fragments;
}
function queryBoundTextNodes(templateContent: Node, rootTextNodeIndices: number[],
boundElements: Element[], elementBinders: DomElementBinder[],
boundTextNodeCount: number): Node[] {
var boundTextNodes = ListWrapper.createFixedSize(boundTextNodeCount);
var textNodeIndex = 0;
if (rootTextNodeIndices.length > 0) {
var rootChildNodes = DOM.childNodes(templateContent);
for (var i = 0; i < rootTextNodeIndices.length; i++) {
boundTextNodes[textNodeIndex++] = rootChildNodes[rootTextNodeIndices[i]];
}
}
for (var i = 0; i < elementBinders.length; i++) {
var binder = elementBinders[i];
var element: Node = boundElements[i];
if (binder.textNodeIndices.length > 0) {
var childNodes = DOM.childNodes(element);
for (var j = 0; j < binder.textNodeIndices.length; j++) {
boundTextNodes[textNodeIndex++] = childNodes[binder.textNodeIndices[j]];
}
}
}
return boundTextNodes;
}
export function isElementWithTag(node: Node, elementName: string): boolean {
return DOM.isElementNode(node) && DOM.tagName(node).toLowerCase() == elementName.toLowerCase();
}
export function queryBoundTextNodeIndices(parentNode: Node, boundTextNodes: Map<Node, any>,
resultCallback: Function) {
var childNodes = DOM.childNodes(parentNode);
for (var j = 0; j < childNodes.length; j++) {
var node = childNodes[j];
if (boundTextNodes.has(node)) {
resultCallback(node, j, boundTextNodes.get(node));
}
}
}
export function prependAll(parentNode: Node, nodes: Node[]) {
var lastInsertedNode = null;
nodes.forEach(node => {
if (isBlank(lastInsertedNode)) {
var firstChild = DOM.firstChild(parentNode);
if (isPresent(firstChild)) {
DOM.insertBefore(firstChild, node);
} else {
DOM.appendChild(parentNode, node);
}
} else {
DOM.insertAfter(lastInsertedNode, node);
}
lastInsertedNode = node;
});
}

View File

@ -0,0 +1,37 @@
import {AST} from 'angular2/src/change_detection/change_detection';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {isPresent} from 'angular2/src/facade/lang';
export class DomElementBinder {
textNodeIndices: List<number>;
hasNestedProtoView: boolean;
eventLocals: AST;
localEvents: List<Event>;
globalEvents: List<Event>;
hasNativeShadowRoot: boolean;
constructor({textNodeIndices, hasNestedProtoView, eventLocals, localEvents, globalEvents,
hasNativeShadowRoot}: {
textNodeIndices?: List<number>,
hasNestedProtoView?: boolean,
eventLocals?: AST,
localEvents?: List<Event>,
globalEvents?: List<Event>,
hasNativeShadowRoot?: boolean
} = {}) {
this.textNodeIndices = textNodeIndices;
this.hasNestedProtoView = hasNestedProtoView;
this.eventLocals = eventLocals;
this.localEvents = localEvents;
this.globalEvents = globalEvents;
this.hasNativeShadowRoot = isPresent(hasNativeShadowRoot) ? hasNativeShadowRoot : false;
}
}
export class Event {
constructor(public name: string, public target: string, public fullName: string) {}
}
export class HostAction {
constructor(public actionName: string, public actionExpression: string, public expression: AST) {}
}

View File

@ -0,0 +1,9 @@
import {RenderFragmentRef} from '../../api';
export function resolveInternalDomFragment(fragmentRef: RenderFragmentRef): Node[] {
return (<DomFragmentRef>fragmentRef)._nodes;
}
export class DomFragmentRef extends RenderFragmentRef {
constructor(public _nodes: Node[]) { super(); }
}

View File

@ -0,0 +1,41 @@
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {DomElementBinder} from './element_binder';
import {RenderProtoViewRef, ViewType, ViewEncapsulation} from '../../api';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {TemplateCloner} from '../template_cloner';
export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView {
return (<DomProtoViewRef>protoViewRef)._protoView;
}
export class DomProtoViewRef extends RenderProtoViewRef {
constructor(public _protoView: DomProtoView) { super(); }
}
export class DomProtoView {
static create(templateCloner: TemplateCloner, type: ViewType, rootElement: Element,
viewEncapsulation: ViewEncapsulation, fragmentsRootNodeCount: number[],
rootTextNodeIndices: number[], elementBinders: List<DomElementBinder>,
hostAttributes: Map<string, string>): DomProtoView {
var boundTextNodeCount = rootTextNodeIndices.length;
for (var i = 0; i < elementBinders.length; i++) {
boundTextNodeCount += elementBinders[i].textNodeIndices.length;
}
var isSingleElementFragment = fragmentsRootNodeCount.length === 1 &&
fragmentsRootNodeCount[0] === 1 &&
DOM.isElementNode(DOM.firstChild(DOM.content(rootElement)));
return new DomProtoView(type, templateCloner.prepareForClone(rootElement), viewEncapsulation,
elementBinders, hostAttributes, rootTextNodeIndices, boundTextNodeCount,
fragmentsRootNodeCount, isSingleElementFragment);
}
// Note: fragments are separated by a comment node that is not counted in fragmentsRootNodeCount!
constructor(public type: ViewType, public cloneableTemplate: Element | string,
public encapsulation: ViewEncapsulation,
public elementBinders: List<DomElementBinder>,
public hostAttributes: Map<string, string>, public rootTextNodeIndices: number[],
public boundTextNodeCount: number, public fragmentsRootNodeCount: number[],
public isSingleElementFragment: boolean) {}
}

View File

@ -0,0 +1,402 @@
import {isPresent, isBlank, BaseException, StringWrapper} from 'angular2/src/facade/lang';
import {
ListWrapper,
MapWrapper,
Set,
SetWrapper,
List,
StringMapWrapper
} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
ASTWithSource,
AST,
AstTransformer,
PropertyRead,
LiteralArray,
ImplicitReceiver
} from 'angular2/src/change_detection/change_detection';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {DomElementBinder, Event, HostAction} from './element_binder';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {TemplateCloner} from '../template_cloner';
import {
ViewType,
ViewEncapsulation,
ProtoViewDto,
DirectiveBinder,
RenderElementBinder,
EventBinding,
ElementPropertyBinding,
PropertyBindingType
} from '../../api';
import {
NG_BINDING_CLASS,
EVENT_TARGET_SEPARATOR,
queryBoundTextNodeIndices,
camelCaseToDashCase
} from '../util';
export class ProtoViewBuilder {
variableBindings: Map<string, string> = new Map();
elements: List<ElementBinderBuilder> = [];
rootTextBindings: Map<Node, ASTWithSource> = new Map();
ngContentCount: number = 0;
hostAttributes: Map<string, string> = new Map();
constructor(public rootElement, public type: ViewType,
public viewEncapsulation: ViewEncapsulation) {}
bindElement(element: HTMLElement, description: string = null): ElementBinderBuilder {
var builder = new ElementBinderBuilder(this.elements.length, element, description);
this.elements.push(builder);
DOM.addClass(element, NG_BINDING_CLASS);
return builder;
}
bindVariable(name: string, value: string) {
// Store the variable map from value to variable, reflecting how it will be used later by
// DomView. When a local is set to the view, a lookup for the variable name will take place
// keyed
// by the "value", or exported identifier. For example, ng-for sets a view local of "index".
// When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
// it.
this.variableBindings.set(value, name);
}
// Note: We don't store the node index until the compilation is complete,
// as the compiler might change the order of elements.
bindRootText(textNode: Text, expression: ASTWithSource) {
this.rootTextBindings.set(textNode, expression);
}
bindNgContent() { this.ngContentCount++; }
setHostAttribute(name: string, value: string) { this.hostAttributes.set(name, value); }
build(schemaRegistry: ElementSchemaRegistry, templateCloner: TemplateCloner): ProtoViewDto {
var domElementBinders = [];
var apiElementBinders = [];
var textNodeExpressions = [];
var rootTextNodeIndices = [];
var transitiveNgContentCount = this.ngContentCount;
queryBoundTextNodeIndices(DOM.content(this.rootElement), this.rootTextBindings,
(node, nodeIndex, expression) => {
textNodeExpressions.push(expression);
rootTextNodeIndices.push(nodeIndex);
});
ListWrapper.forEach(this.elements, (ebb: ElementBinderBuilder) => {
var directiveTemplatePropertyNames = new Set();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb: DirectiveBuilder) => {
ebb.eventBuilder.merge(dbb.eventBuilder);
ListWrapper.forEach(dbb.templatePropertyNames,
(name) => directiveTemplatePropertyNames.add(name));
return new DirectiveBinder({
directiveIndex: dbb.directiveIndex,
propertyBindings: dbb.propertyBindings,
eventBindings: dbb.eventBindings,
hostPropertyBindings: buildElementPropertyBindings(schemaRegistry, ebb.element, true,
dbb.hostPropertyBindings, null)
});
});
var nestedProtoView = isPresent(ebb.nestedProtoView) ?
ebb.nestedProtoView.build(schemaRegistry, templateCloner) :
null;
if (isPresent(nestedProtoView)) {
transitiveNgContentCount += nestedProtoView.transitiveNgContentCount;
}
var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
var textNodeIndices = [];
queryBoundTextNodeIndices(ebb.element, ebb.textBindings, (node, nodeIndex, expression) => {
textNodeExpressions.push(expression);
textNodeIndices.push(nodeIndex);
});
apiElementBinders.push(new RenderElementBinder({
index: ebb.index,
parentIndex: parentIndex,
distanceToParent: ebb.distanceToParent,
directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView,
propertyBindings:
buildElementPropertyBindings(schemaRegistry, ebb.element, isPresent(ebb.componentId),
ebb.propertyBindings, directiveTemplatePropertyNames),
variableBindings: ebb.variableBindings,
eventBindings: ebb.eventBindings,
readAttributes: ebb.readAttributes
}));
domElementBinders.push(new DomElementBinder({
textNodeIndices: textNodeIndices,
hasNestedProtoView: isPresent(nestedProtoView) || isPresent(ebb.componentId),
hasNativeShadowRoot: false,
eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
localEvents: ebb.eventBuilder.buildLocalEvents(),
globalEvents: ebb.eventBuilder.buildGlobalEvents()
}));
});
var rootNodeCount = DOM.childNodes(DOM.content(this.rootElement)).length;
return new ProtoViewDto({
render: new DomProtoViewRef(DomProtoView.create(
templateCloner, this.type, this.rootElement, this.viewEncapsulation, [rootNodeCount],
rootTextNodeIndices, domElementBinders, this.hostAttributes)),
type: this.type,
elementBinders: apiElementBinders,
variableBindings: this.variableBindings,
textBindings: textNodeExpressions,
transitiveNgContentCount: transitiveNgContentCount
});
}
}
export class ElementBinderBuilder {
parent: ElementBinderBuilder = null;
distanceToParent: number = 0;
directives: List<DirectiveBuilder> = [];
nestedProtoView: ProtoViewBuilder = null;
propertyBindings: Map<string, ASTWithSource> = new Map();
variableBindings: Map<string, string> = new Map();
eventBindings: List<EventBinding> = [];
eventBuilder: EventBuilder = new EventBuilder();
textBindings: Map<Node, ASTWithSource> = new Map();
readAttributes: Map<string, string> = new Map();
componentId: string = null;
constructor(public index: number, public element, description: string) {}
setParent(parent: ElementBinderBuilder, distanceToParent: number): ElementBinderBuilder {
this.parent = parent;
if (isPresent(parent)) {
this.distanceToParent = distanceToParent;
}
return this;
}
readAttribute(attrName: string) {
if (isBlank(this.readAttributes.get(attrName))) {
this.readAttributes.set(attrName, DOM.getAttribute(this.element, attrName));
}
}
bindDirective(directiveIndex: number): DirectiveBuilder {
var directive = new DirectiveBuilder(directiveIndex);
this.directives.push(directive);
return directive;
}
bindNestedProtoView(rootElement: HTMLElement): ProtoViewBuilder {
if (isPresent(this.nestedProtoView)) {
throw new BaseException('Only one nested view per element is allowed');
}
this.nestedProtoView =
new ProtoViewBuilder(rootElement, ViewType.EMBEDDED, ViewEncapsulation.NONE);
return this.nestedProtoView;
}
bindProperty(name: string, expression: ASTWithSource) {
this.propertyBindings.set(name, expression);
}
bindVariable(name: string, value: string) {
// When current is a view root, the variable bindings are set to the *nested* proto view.
// The root view conceptually signifies a new "block scope" (the nested view), to which
// the variables are bound.
if (isPresent(this.nestedProtoView)) {
this.nestedProtoView.bindVariable(name, value);
} else {
// Store the variable map from value to variable, reflecting how it will be used later by
// DomView. When a local is set to the view, a lookup for the variable name will take place
// keyed
// by the "value", or exported identifier. For example, ng-for sets a view local of "index".
// When this occurs, a lookup keyed by "index" must occur to find if there is a var
// referencing
// it.
this.variableBindings.set(value, name);
}
}
bindEvent(name: string, expression: ASTWithSource, target: string = null) {
this.eventBindings.push(this.eventBuilder.add(name, expression, target));
}
// Note: We don't store the node index until the compilation is complete,
// as the compiler might change the order of elements.
bindText(textNode: Text, expression: ASTWithSource) {
this.textBindings.set(textNode, expression);
}
setComponentId(componentId: string) { this.componentId = componentId; }
}
export class DirectiveBuilder {
// mapping from directive property name to AST for that directive
propertyBindings: Map<string, ASTWithSource> = new Map();
// property names used in the template
templatePropertyNames: List<string> = [];
hostPropertyBindings: Map<string, ASTWithSource> = new Map();
eventBindings: List<EventBinding> = [];
eventBuilder: EventBuilder = new EventBuilder();
constructor(public directiveIndex: number) {}
bindProperty(name: string, expression: ASTWithSource, elProp: string) {
this.propertyBindings.set(name, expression);
if (isPresent(elProp)) {
// we are filling in a set of property names that are bound to a property
// of at least one directive. This allows us to report "dangling" bindings.
this.templatePropertyNames.push(elProp);
}
}
bindHostProperty(name: string, expression: ASTWithSource) {
this.hostPropertyBindings.set(name, expression);
}
bindEvent(name: string, expression: ASTWithSource, target: string = null) {
this.eventBindings.push(this.eventBuilder.add(name, expression, target));
}
}
class EventBuilder extends AstTransformer {
locals: List<AST> = [];
localEvents: List<Event> = [];
globalEvents: List<Event> = [];
_implicitReceiver: AST = new ImplicitReceiver();
constructor() { super(); }
add(name: string, source: ASTWithSource, target: string): EventBinding {
// TODO(tbosch): reenable this when we are parsing element properties
// out of action expressions
// var adjustedAst = astWithSource.ast.visit(this);
var adjustedAst = source.ast;
var fullName = isPresent(target) ? target + EVENT_TARGET_SEPARATOR + name : name;
var result =
new EventBinding(fullName, new ASTWithSource(adjustedAst, source.source, source.location));
var event = new Event(name, target, fullName);
if (isBlank(target)) {
this.localEvents.push(event);
} else {
this.globalEvents.push(event);
}
return result;
}
visitPropertyRead(ast: PropertyRead): PropertyRead {
var isEventAccess = false;
var current: AST = ast;
while (!isEventAccess && (current instanceof PropertyRead)) {
var am = <PropertyRead>current;
if (am.name == '$event') {
isEventAccess = true;
}
current = am.receiver;
}
if (isEventAccess) {
this.locals.push(ast);
var index = this.locals.length - 1;
return new PropertyRead(this._implicitReceiver, `${index}`, (arr) => arr[index]);
} else {
return ast;
}
}
buildEventLocals(): List<AST> { return this.locals; }
buildLocalEvents(): List<Event> { return this.localEvents; }
buildGlobalEvents(): List<Event> { return this.globalEvents; }
merge(eventBuilder: EventBuilder) {
this._merge(this.localEvents, eventBuilder.localEvents);
this._merge(this.globalEvents, eventBuilder.globalEvents);
ListWrapper.concat(this.locals, eventBuilder.locals);
}
_merge(host: List<Event>, tobeAdded: List<Event>) {
var names = [];
for (var i = 0; i < host.length; i++) {
names.push(host[i].fullName);
}
for (var j = 0; j < tobeAdded.length; j++) {
if (!ListWrapper.contains(names, tobeAdded[j].fullName)) {
host.push(tobeAdded[j]);
}
}
}
}
var PROPERTY_PARTS_SEPARATOR = new RegExp('\\.');
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
function buildElementPropertyBindings(
schemaRegistry: ElementSchemaRegistry, protoElement: /*element*/ any, isNgComponent: boolean,
bindingsInTemplate: Map<string, ASTWithSource>, directiveTemplatePropertyNames: Set<string>):
List<ElementPropertyBinding> {
var propertyBindings = [];
MapWrapper.forEach(bindingsInTemplate, (ast, propertyNameInTemplate) => {
var propertyBinding = createElementPropertyBinding(schemaRegistry, ast, propertyNameInTemplate);
if (isPresent(directiveTemplatePropertyNames) &&
SetWrapper.has(directiveTemplatePropertyNames, propertyNameInTemplate)) {
// We do nothing because directives shadow native elements properties.
} else if (isValidElementPropertyBinding(schemaRegistry, protoElement, isNgComponent,
propertyBinding)) {
propertyBindings.push(propertyBinding);
} else {
var exMsg =
`Can't bind to '${propertyNameInTemplate}' since it isn't a known property of the '<${DOM.tagName(protoElement).toLowerCase()}>' element`;
// directiveTemplatePropertyNames is null for host property bindings
if (isPresent(directiveTemplatePropertyNames)) {
exMsg += ' and there are no matching directives with a corresponding property';
}
throw new BaseException(exMsg);
}
});
return propertyBindings;
}
function isValidElementPropertyBinding(schemaRegistry: ElementSchemaRegistry,
protoElement: /*element*/ any, isNgComponent: boolean,
binding: ElementPropertyBinding): boolean {
if (binding.type === PropertyBindingType.PROPERTY) {
if (!isNgComponent) {
return schemaRegistry.hasProperty(protoElement, binding.property);
} else {
// TODO(pk): change this logic as soon as we can properly detect custom elements
return DOM.hasProperty(protoElement, binding.property);
}
}
return true;
}
function createElementPropertyBinding(schemaRegistry: ElementSchemaRegistry, ast: ASTWithSource,
propertyNameInTemplate: string): ElementPropertyBinding {
var parts = StringWrapper.split(propertyNameInTemplate, PROPERTY_PARTS_SEPARATOR);
if (parts.length === 1) {
var propName = schemaRegistry.getMappedPropName(parts[0]);
return new ElementPropertyBinding(PropertyBindingType.PROPERTY, ast, propName);
} else if (parts[0] == ATTRIBUTE_PREFIX) {
return new ElementPropertyBinding(PropertyBindingType.ATTRIBUTE, ast, parts[1]);
} else if (parts[0] == CLASS_PREFIX) {
return new ElementPropertyBinding(PropertyBindingType.CLASS, ast,
camelCaseToDashCase(parts[1]));
} else if (parts[0] == STYLE_PREFIX) {
var unit = parts.length > 2 ? parts[2] : null;
return new ElementPropertyBinding(PropertyBindingType.STYLE, ast, parts[1], unit);
} else {
throw new BaseException(`Invalid property name ${propertyNameInTemplate}`);
}
}

View File

@ -0,0 +1,450 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isPresent, isBlank, BaseException, isArray} from 'angular2/src/facade/lang';
import {ListWrapper, SetWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {DomElementBinder} from './element_binder';
import {
RenderProtoViewMergeMapping,
RenderProtoViewRef,
ViewType,
ViewEncapsulation
} from '../../api';
import {
NG_BINDING_CLASS,
NG_CONTENT_ELEMENT_NAME,
ClonedProtoView,
cloneAndQueryProtoView,
queryBoundElements,
queryBoundTextNodeIndices,
NG_SHADOW_ROOT_ELEMENT_NAME,
isElementWithTag,
prependAll
} from '../util';
import {TemplateCloner} from '../template_cloner';
export function mergeProtoViewsRecursively(templateCloner: TemplateCloner,
protoViewRefs: List<RenderProtoViewRef | List<any>>):
RenderProtoViewMergeMapping {
// clone
var clonedProtoViews = [];
var hostViewAndBinderIndices: number[][] = [];
cloneProtoViews(templateCloner, protoViewRefs, clonedProtoViews, hostViewAndBinderIndices);
var mainProtoView: ClonedProtoView = clonedProtoViews[0];
// modify the DOM
mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews, hostViewAndBinderIndices);
var fragments = [];
var elementsWithNativeShadowRoot: Set<Element> = new Set();
mergeComponents(clonedProtoViews, hostViewAndBinderIndices, fragments,
elementsWithNativeShadowRoot);
// Note: Need to remark parent elements of bound text nodes
// so that we can find them later via queryBoundElements!
markBoundTextNodeParentsAsBoundElements(clonedProtoViews);
// create a new root element with the changed fragments and elements
var fragmentsRootNodeCount = fragments.map(fragment => fragment.length);
var rootElement = createRootElementFromFragments(fragments);
var rootNode = DOM.content(rootElement);
// read out the new element / text node / ElementBinder order
var mergedBoundElements = queryBoundElements(rootNode, false);
var mergedBoundTextIndices: Map<Node, number> = new Map();
var boundTextNodeMap: Map<Node, any> = indexBoundTextNodes(clonedProtoViews);
var rootTextNodeIndices =
calcRootTextNodeIndices(rootNode, boundTextNodeMap, mergedBoundTextIndices);
var mergedElementBinders =
calcElementBinders(clonedProtoViews, mergedBoundElements, elementsWithNativeShadowRoot,
boundTextNodeMap, mergedBoundTextIndices);
// create element / text index mappings
var mappedElementIndices = calcMappedElementIndices(clonedProtoViews, mergedBoundElements);
var mappedTextIndices = calcMappedTextIndices(clonedProtoViews, mergedBoundTextIndices);
// create result
var hostElementIndicesByViewIndex =
calcHostElementIndicesByViewIndex(clonedProtoViews, hostViewAndBinderIndices);
var nestedViewCounts = calcNestedViewCounts(hostViewAndBinderIndices);
var mergedProtoView =
DomProtoView.create(templateCloner, mainProtoView.original.type, rootElement,
mainProtoView.original.encapsulation, fragmentsRootNodeCount,
rootTextNodeIndices, mergedElementBinders, new Map());
return new RenderProtoViewMergeMapping(new DomProtoViewRef(mergedProtoView),
fragmentsRootNodeCount.length, mappedElementIndices,
mergedBoundElements.length, mappedTextIndices,
hostElementIndicesByViewIndex, nestedViewCounts);
}
function cloneProtoViews(
templateCloner: TemplateCloner, protoViewRefs: List<RenderProtoViewRef | List<any>>,
targetClonedProtoViews: ClonedProtoView[], targetHostViewAndBinderIndices: number[][]) {
var hostProtoView = resolveInternalDomProtoView(protoViewRefs[0]);
var hostPvIdx = targetClonedProtoViews.length;
targetClonedProtoViews.push(cloneAndQueryProtoView(templateCloner, hostProtoView, false));
if (targetHostViewAndBinderIndices.length === 0) {
targetHostViewAndBinderIndices.push([null, null]);
}
var protoViewIdx = 1;
for (var i = 0; i < hostProtoView.elementBinders.length; i++) {
var binder = hostProtoView.elementBinders[i];
if (binder.hasNestedProtoView) {
var nestedEntry = protoViewRefs[protoViewIdx++];
if (isPresent(nestedEntry)) {
targetHostViewAndBinderIndices.push([hostPvIdx, i]);
if (isArray(nestedEntry)) {
cloneProtoViews(templateCloner, <any[]>nestedEntry, targetClonedProtoViews,
targetHostViewAndBinderIndices);
} else {
targetClonedProtoViews.push(cloneAndQueryProtoView(
templateCloner, resolveInternalDomProtoView(nestedEntry), false));
}
}
}
}
}
function markBoundTextNodeParentsAsBoundElements(mergableProtoViews: ClonedProtoView[]) {
mergableProtoViews.forEach((mergableProtoView) => {
mergableProtoView.boundTextNodes.forEach((textNode) => {
var parentNode = textNode.parentNode;
if (isPresent(parentNode) && DOM.isElementNode(parentNode)) {
DOM.addClass(parentNode, NG_BINDING_CLASS);
}
});
});
}
function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map<Node, any> {
var boundTextNodeMap = new Map();
for (var pvIndex = 0; pvIndex < mergableProtoViews.length; pvIndex++) {
var mergableProtoView = mergableProtoViews[pvIndex];
mergableProtoView.boundTextNodes.forEach(
(textNode) => { boundTextNodeMap.set(textNode, null); });
}
return boundTextNodeMap;
}
function mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews: ClonedProtoView[],
hostViewAndBinderIndices: number[][]) {
var nearestHostComponentOrRootPvIndices =
calcNearestHostComponentOrRootPvIndices(clonedProtoViews, hostViewAndBinderIndices);
for (var viewIdx = 1; viewIdx < clonedProtoViews.length; viewIdx++) {
var clonedProtoView = clonedProtoViews[viewIdx];
if (clonedProtoView.original.type === ViewType.EMBEDDED) {
var hostComponentIdx = nearestHostComponentOrRootPvIndices[viewIdx];
var hostPv = clonedProtoViews[hostComponentIdx];
clonedProtoView.fragments.forEach((fragment) => hostPv.fragments.push(fragment));
}
}
}
function calcNearestHostComponentOrRootPvIndices(clonedProtoViews: ClonedProtoView[],
hostViewAndBinderIndices: number[][]): number[] {
var nearestHostComponentOrRootPvIndices = ListWrapper.createFixedSize(clonedProtoViews.length);
nearestHostComponentOrRootPvIndices[0] = null;
for (var viewIdx = 1; viewIdx < hostViewAndBinderIndices.length; viewIdx++) {
var hostViewIdx = hostViewAndBinderIndices[viewIdx][0];
var hostProtoView = clonedProtoViews[hostViewIdx];
if (hostViewIdx === 0 || hostProtoView.original.type === ViewType.COMPONENT) {
nearestHostComponentOrRootPvIndices[viewIdx] = hostViewIdx;
} else {
nearestHostComponentOrRootPvIndices[viewIdx] =
nearestHostComponentOrRootPvIndices[hostViewIdx];
}
}
return nearestHostComponentOrRootPvIndices;
}
function mergeComponents(clonedProtoViews: ClonedProtoView[], hostViewAndBinderIndices: number[][],
targetFragments: Node[][],
targetElementsWithNativeShadowRoot: Set<Element>) {
var hostProtoView = clonedProtoViews[0];
hostProtoView.fragments.forEach((fragment) => targetFragments.push(fragment));
for (var viewIdx = 1; viewIdx < clonedProtoViews.length; viewIdx++) {
var hostViewIdx = hostViewAndBinderIndices[viewIdx][0];
var hostBinderIdx = hostViewAndBinderIndices[viewIdx][1];
var hostProtoView = clonedProtoViews[hostViewIdx];
var clonedProtoView = clonedProtoViews[viewIdx];
if (clonedProtoView.original.type === ViewType.COMPONENT) {
mergeComponent(hostProtoView, hostBinderIdx, clonedProtoView, targetFragments,
targetElementsWithNativeShadowRoot);
}
}
}
function mergeComponent(hostProtoView: ClonedProtoView, binderIdx: number,
nestedProtoView: ClonedProtoView, targetFragments: Node[][],
targetElementsWithNativeShadowRoot: Set<Element>) {
var hostElement = hostProtoView.boundElements[binderIdx];
// We wrap the fragments into elements so that we can expand <ng-content>
// even for root nodes in the fragment without special casing them.
var fragmentElements = mapFragmentsIntoElements(nestedProtoView.fragments);
var contentElements = findContentElements(fragmentElements);
var projectableNodes = DOM.childNodesAsList(hostElement);
for (var i = 0; i < contentElements.length; i++) {
var contentElement = contentElements[i];
var select = DOM.getAttribute(contentElement, 'select');
projectableNodes = projectMatchingNodes(select, contentElement, projectableNodes);
}
// unwrap the fragment elements into arrays of nodes after projecting
var fragments = extractFragmentNodesFromElements(fragmentElements);
var useNativeShadowRoot = nestedProtoView.original.encapsulation === ViewEncapsulation.NATIVE;
if (useNativeShadowRoot) {
targetElementsWithNativeShadowRoot.add(hostElement);
}
MapWrapper.forEach(nestedProtoView.original.hostAttributes, (attrValue, attrName) => {
DOM.setAttribute(hostElement, attrName, attrValue);
});
appendComponentNodesToHost(hostProtoView, binderIdx, fragments[0], useNativeShadowRoot);
for (var i = 1; i < fragments.length; i++) {
targetFragments.push(fragments[i]);
}
}
function mapFragmentsIntoElements(fragments: Node[][]): Element[] {
return fragments.map(fragment => {
var fragmentElement = DOM.createTemplate('');
fragment.forEach(node => DOM.appendChild(DOM.content(fragmentElement), node));
return fragmentElement;
});
}
function extractFragmentNodesFromElements(fragmentElements: Element[]): Node[][] {
return fragmentElements.map(
(fragmentElement) => { return DOM.childNodesAsList(DOM.content(fragmentElement)); });
}
function findContentElements(fragmentElements: Element[]): Element[] {
var contentElements = [];
fragmentElements.forEach((fragmentElement: Element) => {
var fragmentContentElements =
DOM.querySelectorAll(DOM.content(fragmentElement), NG_CONTENT_ELEMENT_NAME);
for (var i = 0; i < fragmentContentElements.length; i++) {
contentElements.push(fragmentContentElements[i]);
}
});
return sortContentElements(contentElements);
}
function appendComponentNodesToHost(hostProtoView: ClonedProtoView, binderIdx: number,
componentRootNodes: Node[], useNativeShadowRoot: boolean) {
var hostElement = hostProtoView.boundElements[binderIdx];
if (useNativeShadowRoot) {
var shadowRootWrapper = DOM.createElement(NG_SHADOW_ROOT_ELEMENT_NAME);
for (var i = 0; i < componentRootNodes.length; i++) {
DOM.appendChild(shadowRootWrapper, componentRootNodes[i]);
}
var firstChild = DOM.firstChild(hostElement);
if (isPresent(firstChild)) {
DOM.insertBefore(firstChild, shadowRootWrapper);
} else {
DOM.appendChild(hostElement, shadowRootWrapper);
}
} else {
DOM.clearNodes(hostElement);
for (var i = 0; i < componentRootNodes.length; i++) {
DOM.appendChild(hostElement, componentRootNodes[i]);
}
}
}
function projectMatchingNodes(selector: string, contentElement: Element, nodes: Node[]): Node[] {
var remaining = [];
DOM.insertBefore(contentElement, DOM.createComment('['));
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var matches = false;
if (isWildcard(selector)) {
matches = true;
} else if (DOM.isElementNode(node) && DOM.elementMatches(node, selector)) {
matches = true;
}
if (matches) {
DOM.insertBefore(contentElement, node);
} else {
remaining.push(node);
}
}
DOM.insertBefore(contentElement, DOM.createComment(']'));
DOM.remove(contentElement);
return remaining;
}
function isWildcard(selector): boolean {
return isBlank(selector) || selector.length === 0 || selector == '*';
}
// we need to sort content elements as they can originate from
// different sub views
function sortContentElements(contentElements: Element[]): Element[] {
// for now, only move the wildcard selector to the end.
// TODO(tbosch): think about sorting by selector specifity...
var firstWildcard = null;
var sorted = [];
contentElements.forEach((contentElement) => {
var select = DOM.getAttribute(contentElement, 'select');
if (isWildcard(select)) {
if (isBlank(firstWildcard)) {
firstWildcard = contentElement;
}
} else {
sorted.push(contentElement);
}
});
if (isPresent(firstWildcard)) {
sorted.push(firstWildcard);
}
return sorted;
}
function createRootElementFromFragments(fragments: Node[][]): Element {
var rootElement = DOM.createTemplate('');
var rootNode = DOM.content(rootElement);
for (var i = 0; i < fragments.length; i++) {
var fragment = fragments[i];
if (i >= 1) {
// Note: We need to seprate fragments by a comment so that sibling
// text nodes don't get merged when we serialize the DomProtoView into a string
// and parse it back again.
DOM.appendChild(rootNode, DOM.createComment('|'));
}
fragment.forEach((node) => { DOM.appendChild(rootNode, node); });
}
return rootElement;
}
function calcRootTextNodeIndices(rootNode: Node, boundTextNodes: Map<Node, any>,
targetBoundTextIndices: Map<Node, number>): number[] {
var rootTextNodeIndices = [];
queryBoundTextNodeIndices(rootNode, boundTextNodes, (textNode, nodeIndex, _) => {
rootTextNodeIndices.push(nodeIndex);
targetBoundTextIndices.set(textNode, targetBoundTextIndices.size);
});
return rootTextNodeIndices;
}
function calcElementBinders(clonedProtoViews: ClonedProtoView[], mergedBoundElements: Element[],
elementsWithNativeShadowRoot: Set<Element>,
boundTextNodes: Map<Node, any>,
targetBoundTextIndices: Map<Node, number>): DomElementBinder[] {
var elementBinderByElement: Map<Element, DomElementBinder> =
indexElementBindersByElement(clonedProtoViews);
var mergedElementBinders = [];
for (var i = 0; i < mergedBoundElements.length; i++) {
var element = mergedBoundElements[i];
var textNodeIndices = [];
queryBoundTextNodeIndices(element, boundTextNodes, (textNode, nodeIndex, _) => {
textNodeIndices.push(nodeIndex);
targetBoundTextIndices.set(textNode, targetBoundTextIndices.size);
});
mergedElementBinders.push(
updateElementBinders(elementBinderByElement.get(element), textNodeIndices,
SetWrapper.has(elementsWithNativeShadowRoot, element)));
}
return mergedElementBinders;
}
function indexElementBindersByElement(mergableProtoViews: ClonedProtoView[]):
Map<Element, DomElementBinder> {
var elementBinderByElement = new Map();
mergableProtoViews.forEach((mergableProtoView) => {
for (var i = 0; i < mergableProtoView.boundElements.length; i++) {
var el = mergableProtoView.boundElements[i];
if (isPresent(el)) {
elementBinderByElement.set(el, mergableProtoView.original.elementBinders[i]);
}
}
});
return elementBinderByElement;
}
function updateElementBinders(elementBinder: DomElementBinder, textNodeIndices: number[],
hasNativeShadowRoot: boolean): DomElementBinder {
var result;
if (isBlank(elementBinder)) {
result = new DomElementBinder({
textNodeIndices: textNodeIndices,
hasNestedProtoView: false,
eventLocals: null,
localEvents: [],
globalEvents: [],
hasNativeShadowRoot: false
});
} else {
result = new DomElementBinder({
textNodeIndices: textNodeIndices,
hasNestedProtoView: false,
eventLocals: elementBinder.eventLocals,
localEvents: elementBinder.localEvents,
globalEvents: elementBinder.globalEvents,
hasNativeShadowRoot: hasNativeShadowRoot
});
}
return result;
}
function calcMappedElementIndices(clonedProtoViews: ClonedProtoView[],
mergedBoundElements: Element[]): number[] {
var mergedBoundElementIndices: Map<Element, number> = indexArray(mergedBoundElements);
var mappedElementIndices = [];
clonedProtoViews.forEach((clonedProtoView) => {
clonedProtoView.boundElements.forEach((boundElement) => {
var mappedElementIndex = mergedBoundElementIndices.get(boundElement);
mappedElementIndices.push(mappedElementIndex);
});
});
return mappedElementIndices;
}
function calcMappedTextIndices(clonedProtoViews: ClonedProtoView[],
mergedBoundTextIndices: Map<Node, number>): number[] {
var mappedTextIndices = [];
clonedProtoViews.forEach((clonedProtoView) => {
clonedProtoView.boundTextNodes.forEach((textNode) => {
var mappedTextIndex = mergedBoundTextIndices.get(textNode);
mappedTextIndices.push(mappedTextIndex);
});
});
return mappedTextIndices;
}
function calcHostElementIndicesByViewIndex(clonedProtoViews: ClonedProtoView[],
hostViewAndBinderIndices: number[][]): number[] {
var hostElementIndices = [null];
var viewElementOffsets = [0];
var elementIndex = clonedProtoViews[0].original.elementBinders.length;
for (var viewIdx = 1; viewIdx < hostViewAndBinderIndices.length; viewIdx++) {
viewElementOffsets.push(elementIndex);
elementIndex += clonedProtoViews[viewIdx].original.elementBinders.length;
var hostViewIdx = hostViewAndBinderIndices[viewIdx][0];
var hostBinderIdx = hostViewAndBinderIndices[viewIdx][1];
hostElementIndices.push(viewElementOffsets[hostViewIdx] + hostBinderIdx);
}
return hostElementIndices;
}
function calcNestedViewCounts(hostViewAndBinderIndices: number[][]): number[] {
var nestedViewCounts = ListWrapper.createFixedSize(hostViewAndBinderIndices.length);
ListWrapper.fill(nestedViewCounts, 0);
for (var viewIdx = hostViewAndBinderIndices.length - 1; viewIdx >= 1; viewIdx--) {
var hostViewAndElementIdx = hostViewAndBinderIndices[viewIdx];
if (isPresent(hostViewAndElementIdx)) {
nestedViewCounts[hostViewAndElementIdx[0]] += nestedViewCounts[viewIdx] + 1;
}
}
return nestedViewCounts;
}
function indexArray(arr: any[]): Map<any, number> {
var map = new Map();
for (var i = 0; i < arr.length; i++) {
map.set(arr[i], i);
}
return map;
}

View File

@ -0,0 +1,52 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Inject, Injectable} from 'angular2/di';
import {SetWrapper} from 'angular2/src/facade/collection';
import {DOCUMENT} from '../dom_tokens';
@Injectable()
export class SharedStylesHost {
protected _styles: string[] = [];
protected _stylesSet: Set<string> = new Set();
constructor() {}
addStyles(styles: string[]) {
var additions = [];
styles.forEach(style => {
if (!SetWrapper.has(this._stylesSet, style)) {
this._stylesSet.add(style);
this._styles.push(style);
additions.push(style);
}
});
this.onStylesAdded(additions);
}
protected onStylesAdded(additions: string[]) {}
getAllStyles(): string[] { return this._styles; }
}
@Injectable()
export class DomSharedStylesHost extends SharedStylesHost {
private _hostNodes: Set<Node> = new Set();
constructor(@Inject(DOCUMENT) doc: any) {
super();
this._hostNodes.add(doc.head);
}
_addStylesToHost(styles: string[], host: Node) {
for (var i = 0; i < styles.length; i++) {
var style = styles[i];
DOM.appendChild(host, DOM.createStyleElement(style));
}
}
addHost(hostNode: Node) {
this._addStylesToHost(this._styles, hostNode);
this._hostNodes.add(hostNode);
}
removeHost(hostNode: Node) { SetWrapper.delete(this._hostNodes, hostNode); }
onStylesAdded(additions: string[]) {
this._hostNodes.forEach((hostNode) => { this._addStylesToHost(additions, hostNode); });
}
}

View File

@ -0,0 +1,87 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {isPresent, isBlank, BaseException, stringify} from 'angular2/src/facade/lang';
import {DomProtoView} from './proto_view';
import {RenderViewRef, RenderEventDispatcher} from '../../api';
import {camelCaseToDashCase} from '../util';
export function resolveInternalDomView(viewRef: RenderViewRef): DomView {
return (<DomViewRef>viewRef)._view;
}
export class DomViewRef extends RenderViewRef {
constructor(public _view: DomView) { super(); }
}
/**
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*/
export class DomView {
hydrated: boolean = false;
eventDispatcher: RenderEventDispatcher = null;
eventHandlerRemovers: List<Function> = [];
constructor(public proto: DomProtoView, public boundTextNodes: List<Node>,
public boundElements: Element[]) {}
setElementProperty(elementIndex: number, propertyName: string, value: any) {
DOM.setProperty(this.boundElements[elementIndex], propertyName, value);
}
setElementAttribute(elementIndex: number, attributeName: string, value: string) {
var element = this.boundElements[elementIndex];
var dashCasedAttributeName = camelCaseToDashCase(attributeName);
if (isPresent(value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
DOM.removeAttribute(element, dashCasedAttributeName);
}
}
setElementClass(elementIndex: number, className: string, isAdd: boolean) {
var element = this.boundElements[elementIndex];
if (isAdd) {
DOM.addClass(element, className);
} else {
DOM.removeClass(element, className);
}
}
setElementStyle(elementIndex: number, styleName: string, value: string) {
var element = this.boundElements[elementIndex];
var dashCasedStyleName = camelCaseToDashCase(styleName);
if (isPresent(value)) {
DOM.setStyle(element, dashCasedStyleName, stringify(value));
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
}
invokeElementMethod(elementIndex: number, methodName: string, args: List<any>) {
var element = this.boundElements[elementIndex];
DOM.invoke(element, methodName, args);
}
setText(textIndex: number, value: string) { DOM.setText(this.boundTextNodes[textIndex], value); }
dispatchEvent(elementIndex: number, eventName: string, event: Event): boolean {
var allowDefaultBehavior = true;
if (isPresent(this.eventDispatcher)) {
var evalLocals = new Map();
evalLocals.set('$event', event);
// TODO(tbosch): reenable this when we are parsing element properties
// out of action expressions
// var localValues = this.proto.elementBinders[elementIndex].eventLocals.eval(null, new
// Locals(null, evalLocals));
// this.eventDispatcher.dispatchEvent(elementIndex, eventName, localValues);
allowDefaultBehavior =
this.eventDispatcher.dispatchRenderEvent(elementIndex, eventName, evalLocals);
if (!allowDefaultBehavior) {
DOM.preventDefault(event);
}
}
return allowDefaultBehavior;
}
}

View File

@ -0,0 +1,8 @@
import {ListWrapper, MapWrapper, List} from 'angular2/src/facade/collection';
import * as viewModule from './view';
export class DomViewContainer {
// The order in this list matches the DOM order.
views: List<viewModule.DomView> = [];
}

View File

@ -0,0 +1,13 @@
/**
* @module
* @description
* This module provides advanced support for extending dom strategy.
*/
export * from './dom/compiler/view_loader';
export * from './dom/view/shared_styles_host';
export * from './dom/compiler/compiler';
export * from './dom/dom_renderer';
export * from './dom/dom_tokens';
export * from './dom/template_cloner';
export * from './api';

View File

@ -0,0 +1,5 @@
import {Promise} from 'angular2/src/facade/async';
export class XHR {
get(url: string): Promise<string> { return null; }
}

View File

@ -0,0 +1,14 @@
library angular2.src.services.xhr_impl;
import 'dart:async' show Future;
import 'dart:html' show HttpRequest;
import 'package:angular2/di.dart';
import './xhr.dart' show XHR;
@Injectable()
class XHRImpl extends XHR {
Future<String> get(String url) {
return HttpRequest.request(url).then((HttpRequest req) => req.responseText,
onError: (_) => new Future.error('Failed to load $url'));
}
}

View File

@ -0,0 +1,41 @@
import {Injectable} from 'angular2/di';
import {Promise, PromiseWrapper, PromiseCompleter} from 'angular2/src/facade/async';
import {isPresent} from 'angular2/src/facade/lang';
import {XHR} from './xhr';
@Injectable()
export class XHRImpl extends XHR {
get(url: string): Promise<string> {
var completer: PromiseCompleter < string >= PromiseWrapper.completer();
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onload = function() {
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
var response = isPresent(xhr.response) ? xhr.response : xhr.responseText;
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
var status = xhr.status === 1223 ? 204 : xhr.status;
// fix status code when it is 0 (0 status is undocumented).
// Occurs when accessing file resources or on Android 4.1 stock browser
// while retrieving files from application cache.
if (status === 0) {
status = response ? 200 : 0;
}
if (200 <= status && status <= 300) {
completer.resolve(response);
} else {
completer.reject(`Failed to load ${url}`, null);
}
};
xhr.onerror = function() { completer.reject(`Failed to load ${url}`, null); };
xhr.send();
return completer.promise;
}
}

View File

@ -0,0 +1,105 @@
import {XHR} from 'angular2/src/render/xhr';
import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent, normalizeBlank, BaseException} from 'angular2/src/facade/lang';
import {PromiseCompleter, PromiseWrapper, Promise} from 'angular2/src/facade/async';
export class MockXHR extends XHR {
private _expectations: List<_Expectation>;
private _definitions: Map<string, string>;
private _requests: List<_PendingRequest>;
constructor() {
super();
this._expectations = [];
this._definitions = new Map();
this._requests = [];
}
get(url: string): Promise<string> {
var request = new _PendingRequest(url);
this._requests.push(request);
return request.getPromise();
}
expect(url: string, response: string) {
var expectation = new _Expectation(url, response);
this._expectations.push(expectation);
}
when(url: string, response: string) { this._definitions.set(url, response); }
flush() {
if (this._requests.length === 0) {
throw new BaseException('No pending requests to flush');
}
do {
var request = ListWrapper.removeAt(this._requests, 0);
this._processRequest(request);
} while (this._requests.length > 0);
this.verifyNoOustandingExpectations();
}
verifyNoOustandingExpectations() {
if (this._expectations.length === 0) return;
var urls = [];
for (var i = 0; i < this._expectations.length; i++) {
var expectation = this._expectations[i];
urls.push(expectation.url);
}
throw new BaseException(`Unsatisfied requests: ${ListWrapper.join(urls, ', ')}`);
}
private _processRequest(request: _PendingRequest) {
var url = request.url;
if (this._expectations.length > 0) {
var expectation = this._expectations[0];
if (expectation.url == url) {
ListWrapper.remove(this._expectations, expectation);
request.complete(expectation.response);
return;
}
}
if (this._definitions.has(url)) {
var response = this._definitions.get(url);
request.complete(normalizeBlank(response));
return;
}
throw new BaseException(`Unexpected request ${url}`);
}
}
class _PendingRequest {
url: string;
completer: PromiseCompleter<string>;
constructor(url) {
this.url = url;
this.completer = PromiseWrapper.completer();
}
complete(response: string) {
if (isBlank(response)) {
this.completer.reject(`Failed to load ${this.url}`, null);
} else {
this.completer.resolve(response);
}
}
getPromise(): Promise<string> { return this.completer.promise; }
}
class _Expectation {
url: string;
response: string;
constructor(url: string, response: string) {
this.url = url;
this.response = response;
}
}