feat(render): add initial implementation of render layer

This commit is contained in:
Tobias Bosch
2015-03-23 14:10:55 -07:00
parent 814d389b6e
commit 6c60c3e547
64 changed files with 7248 additions and 1 deletions

226
modules/angular2/src/render/api.js vendored Normal file
View File

@ -0,0 +1,226 @@
import {isPresent} from 'angular2/src/facade/lang';
import {Promise} from 'angular2/src/facade/async';
import {List, Map} from 'angular2/src/facade/collection';
import {ASTWithSource} from 'angular2/change_detection';
/**
* General notes:
* 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 ElementBinder {
index:number;
parentIndex:number;
distanceToParent:number;
parentWithDirectivesIndex:number;
distanceToParentWithDirectives:number;
directives:List<DirectiveBinder>;
nestedProtoView:ProtoView;
propertyBindings: Map<string, ASTWithSource>;
variableBindings: 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: Map<string, ASTWithSource>;
textBindings: List<ASTWithSource>;
constructor({
index, parentIndex, distanceToParent, parentWithDirectivesIndex,
distanceToParentWithDirectives, directives, nestedProtoView,
propertyBindings, variableBindings,
eventBindings, textBindings
}) {
this.index = index;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
this.parentWithDirectivesIndex = parentWithDirectivesIndex;
this.distanceToParentWithDirectives = distanceToParentWithDirectives;
this.directives = directives;
this.nestedProtoView = nestedProtoView;
this.propertyBindings = propertyBindings;
this.variableBindings = variableBindings;
this.eventBindings = eventBindings;
this.textBindings = textBindings;
}
}
export class DirectiveBinder {
// Index into the array of directives in the Template instance
directiveIndex:any;
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: Map<string, ASTWithSource>;
constructor({
directiveIndex, propertyBindings, eventBindings
}) {
this.directiveIndex = directiveIndex;
this.propertyBindings = propertyBindings;
this.eventBindings = eventBindings;
}
}
export class ProtoView {
render: ProtoViewRef;
elementBinders:List<ElementBinder>;
variableBindings: Map<string, string>;
constructor({render, elementBinders, variableBindings}) {
this.render = render;
this.elementBinders = elementBinders;
this.variableBindings = variableBindings;
}
}
export class DirectiveMetadata {
static get DECORATOR_TYPE() { return 0; }
static get COMPONENT_TYPE() { return 1; }
static get VIEWPORT_TYPE() { return 2; }
id:any;
selector:string;
compileChildren:boolean;
events:Map<string, string>;
bind:Map<string, string>;
setters:List<string>;
type:number;
constructor({id, selector, compileChildren, events, bind, setters, type}) {
this.id = id;
this.selector = selector;
this.compileChildren = isPresent(compileChildren) ? compileChildren : true;
this.events = events;
this.bind = bind;
this.setters = setters;
this.type = type;
}
}
// An opaque reference to a ProtoView
export class ProtoViewRef {}
// An opaque reference to a View
export class ViewRef {}
export class ViewContainerRef {
view:ViewRef;
elementIndex:number;
constructor(view:ViewRef, elementIndex: number) {
this.view = view;
this.elementIndex = elementIndex;
}
}
export class Template {
componentId: string;
absUrl: string;
inline: string;
directives: List<DirectiveMetadata>;
constructor({componentId, absUrl, inline, directives}) {
this.componentId = componentId;
this.absUrl = absUrl;
this.inline = inline;
this.directives = directives;
}
}
export class Renderer {
/**
* Compiles a single ProtoView. 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(template:Template):Promise<ProtoView> { return null; }
/**
* Creates a new ProtoView with preset nested components,
* which will be instantiated when this protoView is instantiated.
* @param {List<ProtoViewRef>} protoViewRefs
* ProtoView for every element with a component in this protoView or in a view container's protoView
* @return {List<ProtoViewRef>}
* new ProtoViewRef for the given protoView and all of its view container's protoViews
*/
mergeChildComponentProtoViews(protoViewRef:ProtoViewRef, protoViewRefs:List<ProtoViewRef>):List<ProtoViewRef> { return null; }
/**
* Creats a ProtoView that will create a root view for the given element,
* i.e. it will not clone the element but only attach other proto views to it.
*/
createRootProtoView(selectorOrElement):ProtoViewRef { return null; }
/**
* Creates a view and all of its nested child components.
* @return {List<ViewRef>} depth first list of nested child components
*/
createView(protoView:ProtoViewRef):List<ViewRef> { return null; }
/**
* Destroys a view and returns it back into the pool.
*/
destroyView(view:ViewRef):void {}
/**
* Inserts a detached view into a viewContainer.
*/
insertViewIntoContainer(vcRef:ViewContainerRef, view:ViewRef, atIndex):void {}
/**
* Detaches a view from a container so that it can be inserted later on
* Note: We are not return the ViewRef as this can't be done in sync,
* so we assume that the caller knows which view is in which spot...
*/
detachViewFromContainer(vcRef:ViewContainerRef, atIndex:number):void {}
/**
* Sets a property on an element.
* Note: This will fail if the property was not mentioned previously as a propertySetter
* in the Template.
*/
setElementProperty(view:ViewRef, elementIndex:number, propertyName:string, propertyValue:any):void {}
/**
* Installs a nested component in another view.
* Note: only allowed if there is a dynamic component directive
*/
setDynamicComponentView(view:ViewRef, elementIndex:number, nestedViewRef:ViewRef):void {}
/**
* This will set the value for a text node.
* Note: This needs to be separate from setElementProperty as we don't have ElementBinders
* for text nodes in the ProtoView either.
*/
setText(view:ViewRef, textNodeIndex:number, text:string):void {}
/**
* Sets the dispatcher for all events that have been defined in the template or in directives
* in the given view.
*/
setEventDispatcher(viewRef:ViewRef, dispatcher:EventDispatcher):void {}
/**
* To be called at the end of the VmTurn so the API can buffer calls
*/
flush():void {}
}
/**
* A dispatcher for all events happening in a view.
*/
export class EventDispatcher {
/**
* Called when an event was triggered for a on-* attribute on an element.
* @param {List<any>} locals Locals to be used to evaluate the
* event expressions
*/
dispatchEvent(
elementIndex:number, eventName:string, locals:List<any>
):void {}
}

View File

@ -0,0 +1,58 @@
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 {
_steps:List<CompileStep>;
_currentStepIndex:number;
_parent:CompileElement;
_results;
_additionalChildren;
constructor(steps) {
this._steps = steps;
this._currentStepIndex = 0;
this._parent = null;
this._results = null;
this._additionalChildren = null;
}
// only public so that it can be used by compile_pipeline
internalProcess(results, startStepIndex, parent:CompileElement, current:CompileElement) {
this._results = results;
var previousStepIndex = this._currentStepIndex;
var previousParent = this._parent;
for (var i=startStepIndex; i<this._steps.length; i++) {
var step = this._steps[i];
this._parent = parent;
this._currentStepIndex = i;
step.process(parent, current, this);
parent = this._parent;
}
ListWrapper.push(results, 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 = ListWrapper.create();
}
ListWrapper.push(this._additionalChildren, element);
}
}

View File

@ -0,0 +1,124 @@
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {int, 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 {
element;
_attrs:Map;
_classList:List;
isViewRoot:boolean;
inheritedProtoView:ProtoViewBuilder;
distanceToInheritedBinder:number;
inheritedElementBinder:ElementBinderBuilder;
compileChildren: boolean;
ignoreBindings: boolean;
elementDescription: string; // e.g. '<div [class]="foo">' : used to provide context in case of error
constructor(element, compilationUnit = '') {
this.element = element;
this._attrs = null;
this._classList = null;
this.isViewRoot = false;
// inherited down to children if they don't have
// an own protoView
this.inheritedProtoView = null;
// inherited down to children if they don't have
// an own elementBinder
this.inheritedElementBinder = null;
this.distanceToInheritedBinder = 0;
this.compileChildren = true;
// set to true to ignore all the bindings on the element
this.ignoreBindings = false;
// 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() {
return isPresent(this.inheritedElementBinder) && this.distanceToInheritedBinder === 0;
}
bindElement() {
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;
}
refreshAttrs() {
this._attrs = null;
}
attrs():Map<string,string> {
if (isBlank(this._attrs)) {
this._attrs = DOM.attributeMap(this.element);
}
return this._attrs;
}
refreshClassList() {
this._classList = null;
}
classList():List<string> {
if (isBlank(this._classList)) {
this._classList = ListWrapper.create();
var elClassList = DOM.classList(this.element);
for (var i = 0; i < elClassList.length; i++) {
ListWrapper.push(this._classList, 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", MapWrapper.get(atts, "id"));
addDescriptionAttribute(buf, "class", MapWrapper.get(atts, "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,56 @@
import {isPresent} 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';
/**
* CompilePipeline for executing CompileSteps recursively for
* all elements in a template.
*/
export class CompilePipeline {
_control:CompileControl;
constructor(steps:List<CompileStep>) {
this._control = new CompileControl(steps);
}
process(rootElement, compilationCtxtDescription:string = ''):List {
var results = ListWrapper.create();
var rootCompileElement = new CompileElement(rootElement, compilationCtxtDescription);
rootCompileElement.inheritedProtoView = new ProtoViewBuilder(rootElement);
rootCompileElement.isViewRoot = true;
this._process(results, null, rootCompileElement,
compilationCtxtDescription
);
return results;
}
_process(results, 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._process(results, current, childCompileElement);
}
node = nextNode;
}
}
if (isPresent(additionalChildren)) {
for (var i=0; i<additionalChildren.length; i++) {
this._process(results, current, additionalChildren[i]);
}
}
}
}

View File

@ -0,0 +1,10 @@
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 class CompileStep {
process(parent:CompileElement, current:CompileElement, control:compileControlModule.CompileControl) {}
}

View File

@ -0,0 +1,39 @@
import {List} from 'angular2/src/facade/collection';
import {Promise} from 'angular2/src/facade/async';
import {Parser} from 'angular2/change_detection';
import {Template} 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 {ShadowDomCompileStep} from '../shadow_dom/shadow_dom_compile_step';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
export class CompileStepFactory {
createSteps(template: Template, subTaskPromises: List<Promise>):List<CompileStep> {
return null;
}
}
export class DefaultStepFactory extends CompileStepFactory {
_parser: Parser;
_shadowDomStrategy: ShadowDomStrategy;
constructor(parser: Parser, shadowDomStrategy) {
super();
this._parser = parser;
this._shadowDomStrategy = shadowDomStrategy;
}
createSteps(template: Template, subTaskPromises: List<Promise>) {
return [
new ViewSplitter(this._parser),
new PropertyBindingParser(this._parser),
new DirectiveParser(this._parser, template.directives),
new TextInterpolationParser(this._parser),
new ShadowDomCompileStep(this._shadowDomStrategy, template, subTaskPromises)
];
}
}

View File

@ -0,0 +1,41 @@
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {BaseException} from 'angular2/src/facade/lang';
import {Template, ProtoView} from '../../api';
import {CompilePipeline} from './compile_pipeline';
import {TemplateLoader} from './template_loader';
import {CompileStepFactory} from './compile_step_factory';
export class Compiler {
_templateLoader: TemplateLoader;
_stepFactory: CompileStepFactory;
constructor(stepFactory: CompileStepFactory, templateLoader: TemplateLoader) {
this._templateLoader = templateLoader;
this._stepFactory = stepFactory;
}
compile(template: Template):Promise<ProtoView> {
var tplPromise = this._templateLoader.load(template);
return PromiseWrapper.then(tplPromise,
(el) => this._compileTemplate(template, el),
(_) => { throw new BaseException(`Failed to load the template "${template.componentId}"`); }
);
}
_compileTemplate(template: Template, tplElement):Promise<ProtoView> {
var subTaskPromises = [];
var pipeline = new CompilePipeline(this._stepFactory.createSteps(template, subTaskPromises));
var compileElements;
compileElements = pipeline.process(tplElement, template.componentId);
var protoView = compileElements[0].inheritedProtoView.build();
if (subTaskPromises.length > 0) {
return PromiseWrapper.all(subTaskPromises).then((_) => protoView);
} else {
return PromiseWrapper.resolve(protoView);
}
}
}

View File

@ -0,0 +1,139 @@
import {isPresent, isBlank, BaseException, assertionsEnabled, RegExpWrapper} 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/change_detection';
import {SelectorMatcher, CssSelector} from './selector';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {setterFactory} from './property_setter_factory';
import {DirectiveMetadata} from '../../api';
import {dashCaseToCamelCase, camelCaseToDashCase} from '../util';
/**
* Parses the directives on a single element. Assumes ViewSplitter has already created
* <template> elements for template directives.
*/
export class DirectiveParser extends CompileStep {
_selectorMatcher:SelectorMatcher;
_directives:List<DirectiveMetadata>;
_parser:Parser;
constructor(parser: Parser, directives:List<DirectiveMetadata>) {
super();
this._parser = parser;
this._selectorMatcher = new SelectorMatcher();
this._directives = directives;
for (var i=0; i<directives.length; i++) {
var selector = CssSelector.parse(directives[i].selector);
this._selectorMatcher.addSelectables(selector, i);
}
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var attrs = current.attrs();
var classList = current.classList();
var cssSelector = new CssSelector();
var nodeName = DOM.nodeName(current.element);
cssSelector.setElement(nodeName);
for (var i=0; i < classList.length; i++) {
cssSelector.addClassName(classList[i]);
}
MapWrapper.forEach(attrs, (attrValue, attrName) => {
cssSelector.addAttribute(attrName, attrValue);
});
var viewportDirective;
var componentDirective;
// Note: We assume that the ViewSplitter already did its work, i.e. template directive should
// only be present on <template> elements!
var isTemplateElement = DOM.isTemplateElement(current.element);
this._selectorMatcher.match(cssSelector, (selector, directiveIndex) => {
var elementBinder = current.bindElement();
var directive = this._directives[directiveIndex];
var directiveBinder = elementBinder.bindDirective(directiveIndex);
current.compileChildren = current.compileChildren && directive.compileChildren;
if (isPresent(directive.bind)) {
MapWrapper.forEach(directive.bind, (bindConfig, dirProperty) => {
this._bindDirectiveProperty(dirProperty, bindConfig, current, directiveBinder);
});
}
if (isPresent(directive.events)) {
MapWrapper.forEach(directive.events, (action, eventName) => {
this._bindDirectiveEvent(eventName, action, current, directiveBinder);
});
}
if (isPresent(directive.setters)) {
ListWrapper.forEach(directive.setters, (propertyName) => {
directiveBinder.bindPropertySetter(propertyName, setterFactory(propertyName));
});
}
if (directive.type === DirectiveMetadata.VIEWPORT_TYPE) {
if (!isTemplateElement) {
throw new BaseException(`Viewport directives need to be placed on <template> elements or elements ` +
`with template attribute - check ${current.elementDescription}`);
}
if (isPresent(viewportDirective)) {
throw new BaseException(`Only one viewport directive is allowed per element - check ${current.elementDescription}`);
}
viewportDirective = directive;
} else {
if (isTemplateElement) {
throw new BaseException(`Only template directives are allowed on template elements - check ${current.elementDescription}`);
}
if (directive.type === DirectiveMetadata.COMPONENT_TYPE) {
if (isPresent(componentDirective)) {
throw new BaseException(`Only one component directive is allowed per element - check ${current.elementDescription}`);
}
componentDirective = directive;
elementBinder.setComponentId(directive.id);
}
}
});
}
_bindDirectiveProperty(dirProperty, bindConfig, compileElement, directiveBinder) {
var pipes = this._splitBindConfig(bindConfig);
var elProp = ListWrapper.removeAt(pipes, 0);
var bindingAst = MapWrapper.get(
compileElement.bindElement().propertyBindings,
dashCaseToCamelCase(elProp)
);
if (isBlank(bindingAst)) {
var attributeValue = MapWrapper.get(compileElement.attrs(), 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)) {
var fullExpAstWithBindPipes = this._parser.addPipes(bindingAst, pipes);
directiveBinder.bindProperty(
dirProperty, fullExpAstWithBindPipes
);
}
}
_bindDirectiveEvent(eventName, action, compileElement, directiveBinder) {
var ast = this._parser.parseAction(action, compileElement.elementDescription);
directiveBinder.bindEvent(eventName, ast);
}
_splitBindConfig(bindConfig:string) {
return ListWrapper.map(bindConfig.split('|'), (s) => s.trim());
}
}

View File

@ -0,0 +1,110 @@
import {isPresent, isBlank, RegExpWrapper, BaseException, StringWrapper} from 'angular2/src/facade/lang';
import {MapWrapper} from 'angular2/src/facade/collection';
import {Parser, AST, ExpressionWithSource} from 'angular2/change_detection';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {dashCaseToCamelCase} from '../util';
import {setterFactory} from './property_setter_factory';
// Group 1 = "bind"
// Group 2 = "var"
// Group 3 = "on"
// Group 4 = the identifier after "bind", "var", or "on"
// Group 5 = idenitifer inside square braces
// Group 6 = identifier inside parenthesis
// Group 7 = "#"
// Group 8 = identifier after "#"
var BIND_NAME_REGEXP = RegExpWrapper.create(
'^(?:(?:(?:(bind)|(var)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\)]+)\\)|(#)(.+))$');
/**
* Parses the property bindings on a single element.
*/
export class PropertyBindingParser extends CompileStep {
_parser:Parser;
constructor(parser:Parser) {
super();
this._parser = parser;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
if (current.ignoreBindings) {
return;
}
var attrs = current.attrs();
var newAttrs = MapWrapper.create();
MapWrapper.forEach(attrs, (attrValue, attrName) => {
var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
if (isPresent(bindParts)) {
if (isPresent(bindParts[1])) {
// match: bind-prop
this._bindProperty(bindParts[4], attrValue, current, newAttrs);
} else if (isPresent(bindParts[2]) || isPresent(bindParts[7])) {
// match: var-name / var-name="iden" / #name / #name="iden"
var identifier = (isPresent(bindParts[4]) && bindParts[4] !== '') ?
bindParts[4] : bindParts[8];
var value = attrValue == '' ? '\$implicit' : attrValue;
this._bindVariable(identifier, value, current, newAttrs);
} else if (isPresent(bindParts[3])) {
// match: on-event
this._bindEvent(bindParts[4], attrValue, current, newAttrs);
} else if (isPresent(bindParts[5])) {
// match: [prop]
this._bindProperty(bindParts[5], attrValue, current, newAttrs);
} else if (isPresent(bindParts[6])) {
// match: (event)
this._bindEvent(bindParts[6], 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) => {
MapWrapper.set(attrs, attrName, attrValue);
});
}
_bindVariable(identifier, value, current:CompileElement, newAttrs) {
current.bindElement().bindVariable(dashCaseToCamelCase(identifier), value);
MapWrapper.set(newAttrs, 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) {
var binder = current.bindElement();
var camelCaseName = dashCaseToCamelCase(name);
binder.bindProperty(camelCaseName, ast);
binder.bindPropertySetter(camelCaseName, setterFactory(camelCaseName));
MapWrapper.set(newAttrs, name, ast.source);
}
_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,138 @@
import {StringWrapper, RegExpWrapper, BaseException, isPresent, isBlank, isString, stringify} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {reflector} from 'angular2/src/reflection/reflection';
var DASH_CASE_REGEXP = RegExpWrapper.create('-([a-z])');
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
export function dashCaseToCamelCase(input:string): string {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => {
return m[1].toUpperCase();
});
}
export function camelCaseToDashCase(input:string): string {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, (m) => {
return '-' + m[1].toLowerCase();
});
}
const STYLE_SEPARATOR = '.';
var propertySettersCache = StringMapWrapper.create();
var innerHTMLSetterCache;
export function setterFactory(property: string): Function {
var setterFn, styleParts, styleSuffix;
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
setterFn = attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
styleParts = property.split(STYLE_SEPARATOR);
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
} else if (StringWrapper.equals(property, 'innerHtml')) {
if (isBlank(innerHTMLSetterCache)) {
innerHTMLSetterCache = (el, value) => DOM.setInnerHTML(el, value);
}
setterFn = innerHTMLSetterCache;
} else {
property = resolvePropertyName(property);
setterFn = StringMapWrapper.get(propertySettersCache, property);
if (isBlank(setterFn)) {
var propertySetterFn = reflector.setter(property);
setterFn = function(receiver, value) {
if (DOM.hasProperty(receiver, property)) {
return propertySetterFn(receiver, value);
}
}
StringMapWrapper.set(propertySettersCache, property, setterFn);
}
}
return setterFn;
}
const ATTRIBUTE_PREFIX = 'attr.';
var attributeSettersCache = StringMapWrapper.create();
function _isValidAttributeValue(attrName:string, value: any): boolean {
if (attrName == "role") {
return isString(value);
} else {
return isPresent(value);
}
}
function attributeSetterFactory(attrName:string): Function {
var setterFn = StringMapWrapper.get(attributeSettersCache, attrName);
var dashCasedAttributeName;
if (isBlank(setterFn)) {
dashCasedAttributeName = camelCaseToDashCase(attrName);
setterFn = function(element, value) {
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
if (isPresent(value)) {
throw new BaseException("Invalid " + dashCasedAttributeName +
" attribute, only string values are allowed, got '" + stringify(value) + "'");
}
DOM.removeAttribute(element, dashCasedAttributeName);
}
};
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
}
return setterFn;
}
const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create();
function classSetterFactory(className:string): Function {
var setterFn = StringMapWrapper.get(classSettersCache, className);
if (isBlank(setterFn)) {
setterFn = function(element, value) {
if (value) {
DOM.addClass(element, className);
} else {
DOM.removeClass(element, className);
}
};
StringMapWrapper.set(classSettersCache, className, setterFn);
}
return setterFn;
}
const STYLE_PREFIX = 'style.';
var styleSettersCache = StringMapWrapper.create();
function styleSetterFactory(styleName:string, styleSuffix:string): Function {
var cacheKey = styleName + styleSuffix;
var setterFn = StringMapWrapper.get(styleSettersCache, cacheKey);
var dashCasedStyleName;
if (isBlank(setterFn)) {
dashCasedStyleName = camelCaseToDashCase(styleName);
setterFn = function(element, value) {
var valAsStr;
if (isPresent(value)) {
valAsStr = stringify(value);
DOM.setStyle(element, dashCasedStyleName, valAsStr + styleSuffix);
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(styleSettersCache, cacheKey, setterFn);
}
return setterFn;
}
function resolvePropertyName(attrName:string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
return isPresent(mappedPropName) ? mappedPropName : attrName;
}

View File

@ -0,0 +1,352 @@
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;
classNames:List;
attrs:List;
notSelector: CssSelector;
static parse(selector:string): List<CssSelector> {
var results = ListWrapper.create();
var _addResult = (res, cssSel) => {
if (isPresent(cssSel.notSelector) && isBlank(cssSel.element)
&& ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) {
cssSel.element = "*";
}
ListWrapper.push(res, cssSel);
}
var cssSelector = new CssSelector();
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
var match;
var current = cssSelector;
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
if (isPresent(match[1])) {
if (isPresent(cssSelector.notSelector)) {
throw new BaseException('Nesting :not is not allowed in a selector');
}
current.notSelector = new CssSelector();
current = current.notSelector;
}
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])) {
_addResult(results, cssSelector);
cssSelector = current = new CssSelector();
}
}
_addResult(results, cssSelector);
return results;
}
constructor() {
this.element = null;
this.classNames = ListWrapper.create();
this.attrs = ListWrapper.create();
this.notSelector = null;
}
setElement(element:string = null) {
if (isPresent(element)) {
element = element.toLowerCase();
}
this.element = element;
}
addAttribute(name:string, value:string = _EMPTY_ATTR_VALUE) {
ListWrapper.push(this.attrs, name.toLowerCase());
if (isPresent(value)) {
value = value.toLowerCase();
} else {
value = _EMPTY_ATTR_VALUE;
}
ListWrapper.push(this.attrs, value);
}
addClassName(name:string) {
ListWrapper.push(this.classNames, 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 += ']';
}
}
if (isPresent(this.notSelector)) {
res += ":not(" + this.notSelector.toString() + ")";
}
return res;
}
}
/**
* Reads a list of CssSelectors and allows to calculate which ones
* are contained in a given CssSelector.
*/
export class SelectorMatcher {
_elementMap:Map;
_elementPartialMap:Map;
_classMap:Map;
_classPartialMap:Map;
_attrValueMap:Map;
_attrValuePartialMap:Map;
_listContexts:List;
constructor() {
this._elementMap = MapWrapper.create();
this._elementPartialMap = MapWrapper.create();
this._classMap = MapWrapper.create();
this._classPartialMap = MapWrapper.create();
this._attrValueMap = MapWrapper.create();
this._attrValuePartialMap = MapWrapper.create();
this._listContexts = ListWrapper.create();
}
addSelectables(cssSelectors:List<CssSelector>, callbackCtxt) {
var listContext = null;
if (cssSelectors.length > 1) {
listContext= new SelectorListContext(cssSelectors);
ListWrapper.push(this._listContexts, 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
*/
addSelectable(cssSelector, callbackCtxt, 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++];
var map = isTerminal ? matcher._attrValueMap : matcher._attrValuePartialMap;
var valuesMap = MapWrapper.get(map, attrName)
if (isBlank(valuesMap)) {
valuesMap = MapWrapper.create();
MapWrapper.set(map, attrName, valuesMap);
}
if (isTerminal) {
this._addTerminal(valuesMap, attrValue, selectable);
} else {
matcher = this._addPartial(valuesMap, attrValue);
}
}
}
}
_addTerminal(map:Map<string,string>, name:string, selectable) {
var terminalList = MapWrapper.get(map, name)
if (isBlank(terminalList)) {
terminalList = ListWrapper.create();
MapWrapper.set(map, name, terminalList);
}
ListWrapper.push(terminalList, selectable);
}
_addPartial(map:Map<string,string>, name:string) {
var matcher = MapWrapper.get(map, name)
if (isBlank(matcher)) {
matcher = new SelectorMatcher();
MapWrapper.set(map, 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:Function):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 valuesMap = MapWrapper.get(this._attrValueMap, attrName);
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
result = this._matchTerminal(valuesMap, _EMPTY_ATTR_VALUE, cssSelector, matchedCallback) || result;
}
result = this._matchTerminal(valuesMap, attrValue, cssSelector, matchedCallback) || result;
valuesMap = MapWrapper.get(this._attrValuePartialMap, attrName)
result = this._matchPartial(valuesMap, attrValue, cssSelector, matchedCallback) || result;
}
}
return result;
}
_matchTerminal(map:Map<string,string> = null, name, cssSelector, matchedCallback):boolean {
if (isBlank(map) || isBlank(name)) {
return false;
}
var selectables = MapWrapper.get(map, name);
var starSelectables = MapWrapper.get(map, "*");
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,string> = null, name, cssSelector, matchedCallback):boolean {
if (isBlank(map) || isBlank(name)) {
return false;
}
var nestedSelector = MapWrapper.get(map, 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);
}
}
class SelectorListContext {
selectors: List<CssSelector>;
alreadyMatched: boolean;
constructor(selectors:List<CssSelector>) {
this.selectors = selectors;
this.alreadyMatched = false;
}
}
// Store context to pass back selector and context when a selector is matched
class SelectorContext {
selector:CssSelector;
notSelector:CssSelector;
cbContext; // callback context
listContext: SelectorListContext;
constructor(selector:CssSelector, cbContext, listContext: SelectorListContext) {
this.selector = selector;
this.notSelector = selector.notSelector;
this.cbContext = cbContext;
this.listContext = listContext;
}
finalize(cssSelector: CssSelector, callback) {
var result = true;
if (isPresent(this.notSelector) && (isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
var notMatcher = new SelectorMatcher();
notMatcher.addSelectable(this.notSelector, null, null);
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,45 @@
import {isBlank, isPresent, BaseException, stringify} from 'angular2/src/facade/lang';
import {Map, MapWrapper, StringMapWrapper, StringMap} from 'angular2/src/facade/collection';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {XHR} from 'angular2/src/services/xhr';
import {Template} from '../../api';
import {UrlResolver} from 'angular2/src/services/url_resolver';
/**
* Strategy to load component templates.
* @publicModule angular2/angular2
*/
export class TemplateLoader {
_xhr: XHR;
_htmlCache: StringMap;
constructor(xhr: XHR, urlResolver: UrlResolver) {
this._xhr = xhr;
this._htmlCache = StringMapWrapper.create();
}
load(template: Template):Promise {
if (isPresent(template.inline)) {
return PromiseWrapper.resolve(DOM.createTemplate(template.inline));
}
var url = template.absUrl;
if (isPresent(url)) {
var promise = StringMapWrapper.get(this._htmlCache, url);
if (isBlank(promise)) {
promise = this._xhr.get(url).then(function (html) {
var template = DOM.createTemplate(html);
return template;
});
StringMapWrapper.set(this._htmlCache, url, promise);
}
return promise;
}
throw new BaseException('Templates should have either their url or inline property set');
}
}

View File

@ -0,0 +1,39 @@
import {RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Parser} from 'angular2/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 extends CompileStep {
_parser:Parser;
constructor(parser:Parser) {
super();
this._parser = parser;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
if (!current.compileChildren || current.ignoreBindings) {
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 text = DOM.nodeValue(node);
var expr = this._parser.parseInterpolation(text, current.elementDescription);
if (isPresent(expr)) {
DOM.setText(node, ' ');
current.bindElement().bindText(i, expr);
}
}
}
}
}

View File

@ -0,0 +1,118 @@
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/change_detection';
import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
/**
* 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 extends CompileStep {
_parser:Parser;
constructor(parser:Parser) {
super();
this._parser = parser;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var attrs = current.attrs();
var templateBindings = MapWrapper.get(attrs, 'template');
var hasTemplateBinding = isPresent(templateBindings);
// look for template shortcuts such as *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 newParent = new CompileElement(DOM.createTemplate(''));
newParent.inheritedProtoView = current.inheritedProtoView;
newParent.inheritedElementBinder = current.inheritedElementBinder;
newParent.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
newParent.elementDescription = current.elementDescription;
current.inheritedProtoView = newParent.bindElement().bindNestedProtoView(current.element);
current.inheritedElementBinder = null;
current.distanceToInheritedBinder = 0;
current.isViewRoot = true;
this._parseTemplateBindings(templateBindings, newParent);
this._addParentElement(current.element, newParent.element);
control.addParent(newParent);
DOM.remove(current.element);
}
}
}
_moveChildNodes(source, target) {
var next = DOM.firstChild(source);
while (isPresent(next)) {
DOM.appendChild(target, next);
next = DOM.firstChild(source);
}
}
_addParentElement(currentElement, newParentElement) {
DOM.insertBefore(currentElement, newParentElement);
DOM.appendChild(newParentElement, currentElement);
}
_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(binding.key, binding.name);
MapWrapper.set(compileElement.attrs(), binding.key, binding.name);
} else if (isPresent(binding.expression)) {
compileElement.bindElement().bindProperty(binding.key, binding.expression);
MapWrapper.set(compileElement.attrs(), binding.key, binding.expression.source);
} else {
DOM.setAttribute(compileElement.element, binding.key, '');
}
}
}
}

View File

@ -0,0 +1,141 @@
import {Promise} from 'angular2/src/facade/async';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import * as api from '../api';
import {View} from './view/view';
import {ProtoView} from './view/proto_view';
import {ViewFactory} from './view/view_factory';
import {Compiler} from './compiler/compiler';
import {ShadowDomStrategy} from './shadow_dom/shadow_dom_strategy';
import {ProtoViewBuilder} from './view/proto_view_builder';
function _resolveViewContainer(vc:api.ViewContainerRef) {
return _resolveView(vc.view).viewContainers[vc.elementIndex];
}
function _resolveView(viewRef:_DirectDomViewRef) {
return isPresent(viewRef) ? viewRef.delegate : null;
}
function _resolveProtoView(protoViewRef:DirectDomProtoViewRef) {
return isPresent(protoViewRef) ? protoViewRef.delegate : null;
}
function _wrapView(view:View) {
return new _DirectDomViewRef(view);
}
function _wrapProtoView(protoView:ProtoView) {
return new DirectDomProtoViewRef(protoView);
}
function _collectComponentChildViewRefs(view, target = null) {
if (isBlank(target)) {
target = [];
}
ListWrapper.push(target, _wrapView(view));
ListWrapper.forEach(view.componentChildViews, (view) => {
if (isPresent(view)) {
_collectComponentChildViewRefs(view, target);
}
});
return target;
}
// public so that the compiler can use it.
export class DirectDomProtoViewRef extends api.ProtoViewRef {
delegate:ProtoView;
constructor(delegate:ProtoView) {
super();
this.delegate = delegate;
}
}
class _DirectDomViewRef extends api.ViewRef {
delegate:View;
constructor(delegate:View) {
super();
this.delegate = delegate;
}
}
export class DirectDomRenderer extends api.Renderer {
_compiler: Compiler;
_viewFactory: ViewFactory;
_shadowDomStrategy: ShadowDomStrategy;
constructor(
compiler: Compiler, viewFactory: ViewFactory, shadowDomStrategy: ShadowDomStrategy) {
super();
this._compiler = compiler;
this._viewFactory = viewFactory;
this._shadowDomStrategy = shadowDomStrategy;
}
compile(template:api.Template):Promise<api.ProtoView> {
// Note: compiler already uses a DirectDomProtoViewRef, so we don't
// need to do anything here
return this._compiler.compile(template);
}
mergeChildComponentProtoViews(protoViewRef:api.ProtoViewRef, protoViewRefs:List<api.ProtoViewRef>):List<api.ProtoViewRef> {
var protoViews = [];
_resolveProtoView(protoViewRef).mergeChildComponentProtoViews(
ListWrapper.map(protoViewRefs, _resolveProtoView),
protoViews
);
return ListWrapper.map(protoViews, _wrapProtoView);
}
createRootProtoView(selectorOrElement):api.ProtoViewRef {
var element = selectorOrElement; // TODO: select the element if it is not a real element...
var rootProtoViewBuilder = new ProtoViewBuilder(element);
rootProtoViewBuilder.setIsRootView(true);
rootProtoViewBuilder.bindElement(element, 'root element').setComponentId('root');
this._shadowDomStrategy.processElement(null, 'root', element);
return rootProtoViewBuilder.build().render;
}
createView(protoViewRef:api.ProtoViewRef):List<api.ViewRef> {
return _collectComponentChildViewRefs(
this._viewFactory.getView(_resolveProtoView(protoViewRef))
);
}
destroyView(viewRef:api.ViewRef) {
this._viewFactory.returnView(_resolveView(viewRef));
}
insertViewIntoContainer(vcRef:api.ViewContainerRef, viewRef:api.ViewRef, atIndex=-1):void {
_resolveViewContainer(vcRef).insert(_resolveView(viewRef), atIndex);
}
detachViewFromContainer(vcRef:api.ViewContainerRef, atIndex:number):void {
_resolveViewContainer(vcRef).detach(atIndex);
}
setElementProperty(viewRef:api.ViewRef, elementIndex:number, propertyName:string, propertyValue:any):void {
_resolveView(viewRef).setElementProperty(elementIndex, propertyName, propertyValue);
}
setDynamicComponentView(viewRef:api.ViewRef, elementIndex:number, nestedViewRef:api.ViewRef):void {
_resolveView(viewRef).setComponentView(
this._shadowDomStrategy,
elementIndex,
_resolveView(nestedViewRef)
);
}
setText(viewRef:api.ViewRef, textNodeIndex:number, text:string):void {
_resolveView(viewRef).setText(textNodeIndex, text);
}
setEventDispatcher(viewRef:api.ViewRef, dispatcher:api.EventDispatcher) {
_resolveView(viewRef).setEventDispatcher(dispatcher);
}
}

View File

@ -0,0 +1,94 @@
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 {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone';
var BUBBLE_SYMBOL = '^';
export class EventManager {
_plugins: List<EventManagerPlugin>;
_zone: VmTurnZone;
constructor(plugins: List<EventManagerPlugin>, zone: VmTurnZone) {
this._zone = zone;
this._plugins = plugins;
for (var i = 0; i < plugins.length; i++) {
plugins[i].manager = this;
}
}
addEventListener(element, eventName: string, handler: Function) {
var shouldSupportBubble = eventName[0] == BUBBLE_SYMBOL;
if (shouldSupportBubble) {
eventName = StringWrapper.substring(eventName, 1);
}
var plugin = this._findPluginFor(eventName);
plugin.addEventListener(element, eventName, handler, shouldSupportBubble);
}
getZone(): VmTurnZone {
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}`);
}
}
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, eventName: string, handler: Function,
shouldSupportBubble: boolean) {
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, eventName: string, handler: Function,
shouldSupportBubble: boolean) {
var outsideHandler = shouldSupportBubble ?
DomEventsPlugin.bubbleCallback(element, handler, this.manager._zone) :
DomEventsPlugin.sameElementCallback(element, handler, this.manager._zone);
this.manager._zone.runOutsideAngular(() => {
DOM.on(element, eventName, outsideHandler);
});
}
static sameElementCallback(element, handler, zone) {
return (event) => {
if (event.target === element) {
zone.run(() => handler(event));
}
};
}
static bubbleCallback(element, handler, zone) {
return (event) => zone.run(() => handler(event));
}
}

View File

@ -0,0 +1,52 @@
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,86 @@
library angular.events;
import 'dart:html';
import './hammer_common.dart';
import '../../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,37 @@
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, 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,94 @@
import * as ldModule from './light_dom';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isPresent} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
class ContentStrategy {
nodes:List;
insert(nodes:List){}
}
/**
* An implementation of the content tag that is used by transcluding components.
* It is used when the content tag is not a direct child of another component,
* and thus does not affect redistribution.
*/
class RenderedContent extends ContentStrategy {
beginScript;
endScript;
constructor(contentEl) {
super();
this.beginScript = contentEl;
this.endScript = DOM.nextSibling(this.beginScript);
this.nodes = [];
}
// Inserts the nodes in between the start and end scripts.
// Previous content is removed.
insert(nodes:List) {
this.nodes = nodes;
DOM.insertAllBefore(this.endScript, nodes);
this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this.endScript : nodes[0]);
}
_removeNodesUntil(node) {
var p = DOM.parentElement(this.beginScript);
for (var next = DOM.nextSibling(this.beginScript);
next !== node;
next = DOM.nextSibling(this.beginScript)) {
DOM.removeChild(p, next);
}
}
}
/**
* An implementation of the content tag that is used by transcluding components.
* It is used when the content tag is a direct child of another component,
* and thus does not get rendered but only affect the distribution of its parent component.
*/
class IntermediateContent extends ContentStrategy {
destinationLightDom:ldModule.LightDom;
constructor(destinationLightDom:ldModule.LightDom) {
super();
this.nodes = [];
this.destinationLightDom = destinationLightDom;
}
insert(nodes:List) {
this.nodes = nodes;
this.destinationLightDom.redistribute();
}
}
export class Content {
select:string;
_strategy:ContentStrategy;
contentStartElement;
constructor(contentStartEl, selector:string) {
this.select = selector;
this.contentStartElement = contentStartEl;
this._strategy = null;
}
hydrate(destinationLightDom:ldModule.LightDom) {
this._strategy = isPresent(destinationLightDom) ?
new IntermediateContent(destinationLightDom) :
new RenderedContent(this.contentStartElement);
}
dehydrate() {
this._strategy = null;
}
nodes():List {
return this._strategy.nodes;
}
insert(nodes:List) {
this._strategy.insert(nodes);
}
}

View File

@ -0,0 +1,67 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {StyleInliner} from './style_inliner';
import {StyleUrlResolver} from './style_url_resolver';
import {EmulatedUnscopedShadowDomStrategy} from './emulated_unscoped_shadow_dom_strategy';
import {
getContentAttribute, getHostAttribute, getComponentId, shimCssForComponent, insertStyleElement
} from './util';
/**
* This strategy emulates the Shadow DOM for the templates, styles **included**:
* - components templates are added as children of their component element,
* - both the template and the styles are modified so that styles are scoped to the component
* they belong to,
* - styles are moved from the templates to the styleHost (i.e. the document head).
*
* Notes:
* - styles are scoped to their component and will apply only to it,
* - a common subset of shadow DOM selectors are supported,
* - see `ShadowCss` for more information and limitations.
*/
export class EmulatedScopedShadowDomStrategy extends EmulatedUnscopedShadowDomStrategy {
styleInliner: StyleInliner;
constructor(styleInliner: StyleInliner, styleUrlResolver: StyleUrlResolver, styleHost) {
super(styleUrlResolver, styleHost);
this.styleInliner = styleInliner;
}
processStyleElement(hostComponentId:string, templateUrl:string, styleEl):Promise {
var cssText = DOM.getText(styleEl);
cssText = this.styleUrlResolver.resolveUrls(cssText, templateUrl);
var css = this.styleInliner.inlineImports(cssText, templateUrl);
if (PromiseWrapper.isPromise(css)) {
DOM.setText(styleEl, '');
return css.then((css) => {
css = shimCssForComponent(css, hostComponentId);
DOM.setText(styleEl, css);
});
} else {
css = shimCssForComponent(css, hostComponentId);
DOM.setText(styleEl, css);
}
DOM.remove(styleEl);
insertStyleElement(this.styleHost, styleEl);
return null;
}
processElement(hostComponentId:string, elementComponentId:string, element) {
// Shim the element as a child of the compiled component
if (isPresent(hostComponentId)) {
var contentAttribute = getContentAttribute(getComponentId(hostComponentId));
DOM.setAttribute(element, contentAttribute, '');
}
// If the current element is also a component, shim it as a host
if (isPresent(elementComponentId)) {
var hostAttribute = getHostAttribute(getComponentId(elementComponentId));
DOM.setAttribute(element, hostAttribute, '');
}
}
}

View File

@ -0,0 +1,54 @@
import {Promise} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import * as viewModule from '../view/view';
import {LightDom} from './light_dom';
import {ShadowDomStrategy} from './shadow_dom_strategy';
import {StyleUrlResolver} from './style_url_resolver';
import {moveViewNodesIntoParent} from './util';
import {insertSharedStyleText} from './util';
/**
* This strategy emulates the Shadow DOM for the templates, styles **excluded**:
* - components templates are added as children of their component element,
* - styles are moved from the templates to the styleHost (i.e. the document head).
*
* Notes:
* - styles are **not** scoped to their component and will apply to the whole document,
* - you can **not** use shadow DOM specific selectors in the styles
*/
export class EmulatedUnscopedShadowDomStrategy extends ShadowDomStrategy {
styleUrlResolver: StyleUrlResolver;
styleHost;
constructor(styleUrlResolver: StyleUrlResolver, styleHost) {
super();
this.styleUrlResolver = styleUrlResolver;
this.styleHost = styleHost;
}
hasNativeContentElement():boolean {
return false;
}
attachTemplate(el, view:viewModule.View) {
DOM.clearNodes(el);
moveViewNodesIntoParent(el, view);
}
constructLightDom(lightDomView:viewModule.View, shadowDomView:viewModule.View, el): LightDom {
return new LightDom(lightDomView, shadowDomView, el);
}
processStyleElement(hostComponentId:string, templateUrl:string, styleEl):Promise {
var cssText = DOM.getText(styleEl);
cssText = this.styleUrlResolver.resolveUrls(cssText, templateUrl);
DOM.setText(styleEl, cssText);
DOM.remove(styleEl);
insertSharedStyleText(cssText, this.styleHost, styleEl);
return null;
}
}

View File

@ -0,0 +1,139 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import * as viewModule from '../view/view';
import {Content} from './content_tag';
export class DestinationLightDom {}
class _Root {
node;
viewContainer;
content;
constructor(node, viewContainer, content) {
this.node = node;
this.viewContainer = viewContainer;
this.content = content;
}
}
// TODO: LightDom should implement DestinationLightDom
// once interfaces are supported
export class LightDom {
// The light DOM of the element is enclosed inside the lightDomView
lightDomView:viewModule.View;
// The shadow DOM
shadowDomView:viewModule.View;
// The nodes of the light DOM
nodes:List;
roots:List<_Root>;
constructor(lightDomView:viewModule.View, shadowDomView:viewModule.View, element) {
this.lightDomView = lightDomView;
this.shadowDomView = shadowDomView;
this.nodes = DOM.childNodesAsList(element);
this.roots = null;
}
redistribute() {
var tags = this.contentTags();
if (tags.length > 0) {
redistributeNodes(tags, this.expandedDomNodes());
}
}
contentTags(): List<Content> {
return this._collectAllContentTags(this.shadowDomView, []);
}
// Collects the Content directives from the view and all its child views
_collectAllContentTags(view: viewModule.View, acc:List<Content>):List<Content> {
var contentTags = view.contentTags;
var vcs = view.viewContainers;
for (var i=0; i<vcs.length; i++) {
var vc = vcs[i];
var contentTag = contentTags[i];
if (isPresent(contentTag)) {
ListWrapper.push(acc, contentTag);
}
if (isPresent(vc)) {
ListWrapper.forEach(vc.contentTagContainers(), (view) => {
this._collectAllContentTags(view, acc);
});
}
}
return acc;
}
// Collects the nodes of the light DOM by merging:
// - nodes from enclosed ViewContainers,
// - nodes from enclosed content tags,
// - plain DOM nodes
expandedDomNodes():List {
var res = [];
var roots = this._roots();
for (var i = 0; i < roots.length; ++i) {
var root = roots[i];
if (isPresent(root.viewContainer)) {
res = ListWrapper.concat(res, root.viewContainer.nodes());
} else if (isPresent(root.content)) {
res = ListWrapper.concat(res, root.content.nodes());
} else {
ListWrapper.push(res, root.node);
}
}
return res;
}
// Returns a list of Roots for all the nodes of the light DOM.
// The Root object contains the DOM node and its corresponding injector (could be null).
_roots() {
if (isPresent(this.roots)) return this.roots;
var viewContainers = this.lightDomView.viewContainers;
var contentTags = this.lightDomView.contentTags;
this.roots = ListWrapper.map(this.nodes, (n) => {
var foundVc = null;
var foundContentTag = null;
for (var i=0; i<viewContainers.length; i++) {
var vc = viewContainers[i];
var contentTag = contentTags[i];
if (isPresent(vc) && vc.templateElement === n) {
foundVc = vc;
}
if (isPresent(contentTag) && contentTag.contentStartElement === n) {
foundContentTag = contentTag;
}
}
return new _Root(n, foundVc, foundContentTag);
});
return this.roots;
}
}
// Projects the light DOM into the shadow DOM
function redistributeNodes(contents:List<Content>, nodes:List) {
for (var i = 0; i < contents.length; ++i) {
var content = contents[i];
var select = content.select;
var matchSelector = (n) => DOM.elementMatches(n, select);
// Empty selector is identical to <content/>
if (select.length === 0) {
content.insert(nodes);
ListWrapper.clear(nodes);
} else {
var matchingNodes = ListWrapper.filter(nodes, matchSelector);
content.insert(matchingNodes);
ListWrapper.removeAll(nodes, matchingNodes);
}
}
}

View File

@ -0,0 +1,35 @@
import {Promise} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import * as viewModule from '../view/view';
import {StyleUrlResolver} from './style_url_resolver';
import {ShadowDomStrategy} from './shadow_dom_strategy';
import {moveViewNodesIntoParent} from './util';
/**
* This strategies uses the native Shadow DOM support.
*
* The templates for the component are inserted in a Shadow Root created on the component element.
* Hence they are strictly isolated.
*/
export class NativeShadowDomStrategy extends ShadowDomStrategy {
styleUrlResolver: StyleUrlResolver;
constructor(styleUrlResolver: StyleUrlResolver) {
super();
this.styleUrlResolver = styleUrlResolver;
}
attachTemplate(el, view:viewModule.View){
moveViewNodesIntoParent(DOM.createShadowRoot(el), view);
}
processStyleElement(hostComponentId:string, templateUrl:string, styleEl):Promise {
var cssText = DOM.getText(styleEl);
cssText = this.styleUrlResolver.resolveUrls(cssText, templateUrl);
DOM.setText(styleEl, cssText);
return null;
}
}

View File

@ -0,0 +1,537 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {
StringWrapper,
RegExp,
RegExpWrapper,
RegExpMatcherWrapper,
isPresent,
isBlank,
BaseException,
int
} from 'angular2/src/facade/lang';
/**
* This file is a port of shadowCSS from webcomponents.js to AtScript.
*
* 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;
constructor() {
this.strictStyling = true;
}
/*
* Shim a style element with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*/
shimStyle(style, 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();
ListWrapper.push(r, 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);
}
ListWrapper.push(r, 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 = RegExpWrapper.create('\\[');
var rre = RegExpWrapper.create('\\]');
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 = RegExpWrapper.create('\\[is=([^\\]]*)\\]');
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 = RegExpWrapper.create('([^:]*)(:*)(.*)');
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 = RegExpWrapper.create('[\'"]+|attr');
if (rule.style.content.length > 0 &&
!isPresent(RegExpWrapper.firstMatch(attrRe, rule.style.content))) {
var contentRe = RegExpWrapper.create('content:[^;]*;');
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 = RegExpWrapper.create(
'polyfill-next-selector[^}]*content:[\\s]*?[\'"](.*?)[\'"][;\\s]*}([^{]*?){', 'im');
var _cssContentRuleRe = RegExpWrapper.create(
'(polyfill-rule)[^}]*(content:[\\s]*[\'"](.*?)[\'"])[;\\s]*[^}]*}', 'im');
var _cssContentUnscopedRuleRe = RegExpWrapper.create(
'(polyfill-unscoped-rule)[^}]*(content:[\\s]*[\'"](.*?)[\'"])[;\\s]*[^}]*}', 'im');
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 = [
RegExpWrapper.create('>>>'),
RegExpWrapper.create('::shadow'),
RegExpWrapper.create('::content'),
// Deprecated selectors
RegExpWrapper.create('/deep/'), // former >>>
RegExpWrapper.create('/shadow-deep/'), // former /deep/
RegExpWrapper.create('/shadow/'), // former ::shadow
];
var _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
var _polyfillHostRe = RegExpWrapper.create(_polyfillHost, 'im');
var _colonHostRe = RegExpWrapper.create(':host', 'im');
var _colonHostContextRe = RegExpWrapper.create(':host-context', 'im');
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,72 @@
import {isBlank, isPresent, assertionsEnabled} from 'angular2/src/facade/lang';
import {MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {CompileStep} from '../compiler/compile_step';
import {CompileElement} from '../compiler/compile_element';
import {CompileControl} from '../compiler/compile_control';
import {Template} from '../../api';
import {ShadowDomStrategy} from './shadow_dom_strategy';
export class ShadowDomCompileStep extends CompileStep {
_shadowDomStrategy: ShadowDomStrategy;
_template: Template;
_subTaskPromises: List<Promise>;
constructor(shadowDomStrategy: ShadowDomStrategy, template: Template, subTaskPromises:List<Promise>) {
super();
this._shadowDomStrategy = shadowDomStrategy;
this._template = template;
this._subTaskPromises = subTaskPromises;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
if (current.ignoreBindings) {
return;
}
var tagName = DOM.tagName(current.element);
if (tagName == 'STYLE') {
this._processStyleElement(current);
} else if (tagName == 'CONTENT') {
this._processContentElement(current);
} else {
var componentId = current.isBound() ? current.inheritedElementBinder.componentId : null;
this._shadowDomStrategy.processElement(
this._template.componentId, componentId, current.element
);
}
}
_processStyleElement(current) {
current.ignoreBindings = true;
var stylePromise = this._shadowDomStrategy.processStyleElement(
this._template.componentId, this._template.absUrl, current.element
);
if (isPresent(stylePromise) && PromiseWrapper.isPromise(stylePromise)) {
ListWrapper.push(this._subTaskPromises, stylePromise);
}
}
_processContentElement(current) {
if (this._shadowDomStrategy.hasNativeContentElement()) {
return;
}
var attrs = current.attrs();
var selector = MapWrapper.get(attrs, 'select');
selector = isPresent(selector) ? selector : '';
var contentStart = DOM.createScriptTag('type', 'ng/contentStart');
if (assertionsEnabled()) {
DOM.setAttribute(contentStart, 'select', selector);
}
var contentEnd = DOM.createScriptTag('type', 'ng/contentEnd');
DOM.insertBefore(current.element, contentStart);
DOM.insertBefore(current.element, contentEnd);
DOM.remove(current.element);
current.element = contentStart;
current.bindElement().setContentTagSelector(selector);
}
}

View File

@ -0,0 +1,29 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {Promise} from 'angular2/src/facade/async';
import * as viewModule from '../view/view';
import {LightDom} from './light_dom';
export class ShadowDomStrategy {
hasNativeContentElement():boolean {
return true;
}
attachTemplate(el, view:viewModule.View) {}
constructLightDom(lightDomView:viewModule.View, shadowDomView:viewModule.View, el): LightDom {
return null;
}
/**
* An optional step that can modify the template style elements.
*/
processStyleElement(hostComponentId:string, templateUrl:string, styleElement):Promise {
return null;
};
/**
* An optional step that can modify the template elements (style elements exlcuded).
*/
processElement(hostComponentId:string, elementComponentId:string, element) {}
}

View File

@ -0,0 +1,147 @@
import {XHR} from 'angular2/src/services/xhr';
import {StyleUrlResolver} from './style_url_resolver';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {ListWrapper} from 'angular2/src/facade/collection';
import {
isBlank,
isPresent,
RegExp,
RegExpWrapper,
StringWrapper,
normalizeBlank,
} 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.
*/
export class StyleInliner {
_xhr: XHR;
_urlResolver: UrlResolver;
_styleUrlResolver: StyleUrlResolver;
constructor(xhr: XHR, styleUrlResolver: StyleUrlResolver, urlResolver: UrlResolver) {
this._xhr = xhr;
this._urlResolver = urlResolver;
this._styleUrlResolver = styleUrlResolver;
}
/**
* 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
*/
// TODO(vicb): Union types: returns either a Promise<string> or a string
// TODO(vicb): commented out @import rules should not be inlined
inlineImports(cssText: string, baseUrl: string) {
return this._inlineImports(cssText, baseUrl, []);
}
_inlineImports(cssText: string, baseUrl: string, inlinedUrls: List<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 {
ListWrapper.push(inlinedUrls, url);
promise = PromiseWrapper.then(
this._xhr.get(url),
(css) => {
// resolve nested @import rules
css = this._inlineImports(css, url, inlinedUrls);
if (PromiseWrapper.isPromise(css)) {
// wait until nested @import are inlined
return css.then((css) => {
return prefix + this._transformImportedCss(css, mediaQuery, url) + '\n'
}) ;
} else {
// there are no nested @import, return the css
return prefix + this._transformImportedCss(css, mediaQuery, url) + '\n';
}
},
(error) => `/* failed to import ${url} */\n`
);
}
ListWrapper.push(promises, 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 = RegExpWrapper.create('@import\\s+([^;]+);');
var _urlRe = RegExpWrapper.create(
'url\\(\\s*?[\'"]?([^\'")]+)[\'"]?|' + // url(url) or url('url') or url("url")
'[\'"]([^\'")]+)[\'"]' // "url" or 'url'
);
var _mediaQueryRe = RegExpWrapper.create('[\'"][^\'"]+[\'"]\\s*\\)?\\s*(.*)');

View File

@ -0,0 +1,38 @@
// Some of the code comes from WebComponents.JS
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
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.
*/
export class StyleUrlResolver {
_resolver: UrlResolver;
constructor(resolver: UrlResolver) {
this._resolver = resolver;
}
resolveUrls(cssText: string, baseUrl: 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 url = StringWrapper.replaceAll(m[2], _quoteRe, '');
var post = m[3];
var resolvedUrl = this._resolver.resolve(baseUrl, url);
return pre + "'" + resolvedUrl + "'" + post;
});
}
}
var _cssUrlRe = RegExpWrapper.create('(url\\()([^)]*)(\\))');
var _cssImportRe = RegExpWrapper.create('(@import[\\s]+(?!url\\())[\'"]([^\'"]*)[\'"](.*;)');
var _quoteRe = RegExpWrapper.create('[\'"]');

View File

@ -0,0 +1,73 @@
import {isBlank, isPresent, int} from 'angular2/src/facade/lang';
import {MapWrapper, Map} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ShadowCss} from './shadow_css';
export function moveViewNodesIntoParent(parent, view) {
for (var i = 0; i < view.rootNodes.length; ++i) {
DOM.appendChild(parent, view.rootNodes[i]);
}
}
var _componentUIDs: Map<string, int> = MapWrapper.create();
var _nextComponentUID: int = 0;
var _sharedStyleTexts: Map<string, boolean> = MapWrapper.create();
var _lastInsertedStyleEl;
export function getComponentId(componentStringId: string) {
var id = MapWrapper.get(_componentUIDs, componentStringId);
if (isBlank(id)) {
id = _nextComponentUID++;
MapWrapper.set(_componentUIDs, componentStringId, id);
}
return id;
}
export function insertSharedStyleText(cssText, styleHost, styleEl) {
if (!MapWrapper.contains(_sharedStyleTexts, cssText)) {
// Styles are unscoped and shared across components, only append them to the head
// when there are not present yet
MapWrapper.set(_sharedStyleTexts, cssText, true);
insertStyleElement(styleHost, styleEl);
}
}
export function insertStyleElement(host, styleEl) {
if (isBlank(_lastInsertedStyleEl)) {
var firstChild = DOM.firstChild(host);
if (isPresent(firstChild)) {
DOM.insertBefore(firstChild, styleEl);
} else {
DOM.appendChild(host, styleEl);
}
} else {
DOM.insertAfter(_lastInsertedStyleEl, styleEl);
}
_lastInsertedStyleEl = styleEl;
}
// Return the attribute to be added to the component
export function getHostAttribute(id: int) {
return `_nghost-${id}`;
}
// Returns the attribute to be added on every single element nodes in the component
export function getContentAttribute(id: int) {
return `_ngcontent-${id}`;
}
export function shimCssForComponent(cssText: string, componentId: string): string {
var id = getComponentId(componentId);
var shadowCss = new ShadowCss();
return shadowCss.shimCssText(cssText, getContentAttribute(id), getHostAttribute(id));
}
// Reset the caches - used for tests only
export function resetShadowDomCache() {
MapWrapper.clear(_componentUIDs);
_nextComponentUID = 0;
MapWrapper.clear(_sharedStyleTexts);
_lastInsertedStyleEl = null;
}

19
modules/angular2/src/render/dom/util.js vendored Normal file
View File

@ -0,0 +1,19 @@
import {StringWrapper, RegExpWrapper, isPresent} from 'angular2/src/facade/lang';
export const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
export const NG_BINDING_CLASS = 'ng-binding';
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
var DASH_CASE_REGEXP = RegExpWrapper.create('-([a-z])');
export function camelCaseToDashCase(input:string) {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, (m) => {
return '-' + m[1].toLowerCase();
});
}
export function dashCaseToCamelCase(input:string) {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => {
return m[1].toUpperCase();
});
}

View File

@ -0,0 +1,59 @@
import {AST} from 'angular2/change_detection';
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import * as protoViewModule from './proto_view';
/**
* Note: Code that uses this class assumes that is immutable!
*/
export class ElementBinder {
contentTagSelector: string;
textNodeIndices: List<number>;
nestedProtoView: protoViewModule.ProtoView;
eventLocals: AST;
eventNames: List<string>;
componentId: string;
parentIndex:number;
distanceToParent:number;
constructor({
textNodeIndices,
contentTagSelector,
nestedProtoView,
componentId,
eventLocals,
eventNames,
parentIndex,
distanceToParent
}) {
this.textNodeIndices = textNodeIndices;
this.contentTagSelector = contentTagSelector;
this.nestedProtoView = nestedProtoView;
this.componentId = componentId;
this.eventLocals = eventLocals;
this.eventNames = eventNames;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
}
mergeChildComponentProtoViews(protoViews:List<protoViewModule.ProtoView>, target:List<protoViewModule.ProtoView>):ElementBinder {
var nestedProtoView;
if (isPresent(this.componentId)) {
nestedProtoView = ListWrapper.removeAt(protoViews, 0);
} else if (isPresent(this.nestedProtoView)) {
nestedProtoView = this.nestedProtoView.mergeChildComponentProtoViews(protoViews, target);
}
return new ElementBinder({
parentIndex: this.parentIndex,
// Don't clone as we assume immutability!
textNodeIndices: this.textNodeIndices,
contentTagSelector: this.contentTagSelector,
nestedProtoView: nestedProtoView,
componentId: this.componentId,
// Don't clone as we assume immutability!
eventLocals: this.eventLocals,
eventNames: this.eventNames,
distanceToParent: this.distanceToParent
});
}
}

View File

@ -0,0 +1,55 @@
import {isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {SetterFn} from 'angular2/src/reflection/types';
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {ElementBinder} from './element_binder';
import {NG_BINDING_CLASS} from '../util';
/**
* Note: Code that uses this class assumes that is immutable!
*/
export class ProtoView {
element;
elementBinders:List<ElementBinder>;
isTemplateElement:boolean;
isRootView:boolean;
rootBindingOffset:int;
propertySetters: Map<string, SetterFn>;
constructor({
elementBinders,
element,
isRootView,
propertySetters
}) {
this.element = element;
this.elementBinders = elementBinders;
this.isTemplateElement = DOM.isTemplateElement(this.element);
this.isRootView = isRootView;
this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0;
this.propertySetters = propertySetters;
}
mergeChildComponentProtoViews(protoViews:List<ProtoView>, target:List<ProtoView>):ProtoView {
var elementBinders = ListWrapper.createFixedSize(this.elementBinders.length);
for (var i=0; i<this.elementBinders.length; i++) {
var eb = this.elementBinders[i];
if (isPresent(eb.componentId) || isPresent(eb.nestedProtoView)) {
elementBinders[i] = eb.mergeChildComponentProtoViews(protoViews, target);
} else {
elementBinders[i] = eb;
}
}
var result = new ProtoView({
elementBinders: elementBinders,
element: this.element,
isRootView: this.isRootView,
// Don't clone as we assume immutability!
propertySetters: this.propertySetters
});
ListWrapper.insert(target, 0, result);
return result
}
}

View File

@ -0,0 +1,297 @@
import {isPresent, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Set, SetWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
ASTWithSource, AST, AstTransformer, AccessMember, LiteralArray, ImplicitReceiver
} from 'angular2/change_detection';
import {SetterFn} from 'angular2/src/reflection/types';
import {ProtoView} from './proto_view';
import {ElementBinder} from './element_binder';
import * as api from '../../api';
import * as directDomRenderer from '../direct_dom_renderer';
import {NG_BINDING_CLASS} from '../util';
export class ProtoViewBuilder {
rootElement;
variableBindings: Map<string, string>;
elements:List<ElementBinderBuilder>;
isRootView:boolean;
propertySetters:Set<string>;
constructor(rootElement) {
this.rootElement = rootElement;
this.elements = [];
this.isRootView = false;
this.variableBindings = MapWrapper.create();
this.propertySetters = new Set();
}
bindElement(element, description = null):ElementBinderBuilder {
var builder = new ElementBinderBuilder(this.elements.length, element, description);
ListWrapper.push(this.elements, builder);
DOM.addClass(element, NG_BINDING_CLASS);
return builder;
}
bindVariable(name, value) {
// Store the variable map from value to variable, reflecting how it will be used later by
// View. 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-repeat 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.
MapWrapper.set(this.variableBindings, value, name);
}
setIsRootView(value) {
this.isRootView = value;
}
build():api.ProtoView {
var renderElementBinders = [];
var apiElementBinders = [];
var propertySetters = MapWrapper.create();
ListWrapper.forEach(this.elements, (ebb) => {
var eventLocalsAstSplitter = new EventLocalsAstSplitter();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
MapWrapper.forEach(db.propertySetters, (setter, propertyName) => {
MapWrapper.set(propertySetters, propertyName, setter);
});
return new api.DirectiveBinder({
directiveIndex: db.directiveIndex,
propertyBindings: db.propertyBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(db.eventBindings)
});
});
MapWrapper.forEach(ebb.propertySetters, (setter, propertyName) => {
MapWrapper.set(propertySetters, propertyName, setter);
});
var nestedProtoView =
isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
var parentWithDirectivesIndex = isPresent(ebb.parentWithDirectives) ? ebb.parentWithDirectives.index : -1;
ListWrapper.push(apiElementBinders, new api.ElementBinder({
index: ebb.index, parentIndex:parentIndex, distanceToParent:ebb.distanceToParent,
parentWithDirectivesIndex: parentWithDirectivesIndex, distanceToParentWithDirectives: ebb.distanceToParentWithDirectives,
directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView,
propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings),
textBindings: ebb.textBindings
}));
ListWrapper.push(renderElementBinders, new ElementBinder({
textNodeIndices: ebb.textBindingIndices,
contentTagSelector: ebb.contentTagSelector,
parentIndex: parentIndex,
distanceToParent: ebb.distanceToParent,
nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null,
componentId: ebb.componentId,
eventLocals: eventLocalsAstSplitter.buildEventLocals(),
eventNames: eventLocalsAstSplitter.buildEventNames()
}));
});
return new api.ProtoView({
render: new directDomRenderer.DirectDomProtoViewRef(new ProtoView({
element: this.rootElement,
elementBinders: renderElementBinders,
isRootView: this.isRootView,
propertySetters: propertySetters
})),
elementBinders: apiElementBinders,
variableBindings: this.variableBindings
});
}
}
export class ElementBinderBuilder {
element;
index:number;
parent:ElementBinderBuilder;
distanceToParent:number;
parentWithDirectives:ElementBinderBuilder;
distanceToParentWithDirectives:number;
directives:List<DirectiveBuilder>;
nestedProtoView:ProtoViewBuilder;
propertyBindings: Map<string, ASTWithSource>;
variableBindings: Map<string, string>;
eventBindings: Map<string, ASTWithSource>;
textBindingIndices: List<number>;
textBindings: List<ASTWithSource>;
contentTagSelector:string;
propertySetters: Map<string, SetterFn>;
componentId: string;
constructor(index, element, description) {
this.element = element;
this.index = index;
this.parent = null;
this.distanceToParent = 0;
this.parentWithDirectives = null;
this.distanceToParentWithDirectives = 0;
this.directives = [];
this.nestedProtoView = null;
this.propertyBindings = MapWrapper.create();
this.variableBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
this.textBindings = [];
this.textBindingIndices = [];
this.contentTagSelector = null;
this.propertySetters = MapWrapper.create();
this.componentId = null;
}
setParent(parent:ElementBinderBuilder, distanceToParent):ElementBinderBuilder {
this.parent = parent;
if (isPresent(parent)) {
this.distanceToParent = distanceToParent;
if (parent.directives.length > 0) {
this.parentWithDirectives = parent;
this.distanceToParentWithDirectives = distanceToParent;
} else {
this.parentWithDirectives = parent.parentWithDirectives;
if (isPresent(this.parentWithDirectives)) {
this.distanceToParentWithDirectives = distanceToParent + parent.distanceToParentWithDirectives;
}
}
}
return this;
}
bindDirective(directiveIndex:number):DirectiveBuilder {
var directive = new DirectiveBuilder(directiveIndex);
ListWrapper.push(this.directives, directive);
return directive;
}
bindNestedProtoView(rootElement):ProtoViewBuilder {
if (isPresent(this.nestedProtoView)) {
throw new BaseException('Only one nested view per element is allowed');
}
this.nestedProtoView = new ProtoViewBuilder(rootElement);
return this.nestedProtoView;
}
bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression);
}
bindVariable(name, value) {
// 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
// View. 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-repeat 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.
MapWrapper.set(this.variableBindings, value, name);
}
}
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
}
bindText(index, expression) {
ListWrapper.push(this.textBindingIndices, index);
ListWrapper.push(this.textBindings, expression);
}
setContentTagSelector(value:string) {
this.contentTagSelector = value;
}
bindPropertySetter(propertyName, setter) {
MapWrapper.set(this.propertySetters, propertyName, setter);
}
setComponentId(componentId:string) {
this.componentId = componentId;
}
}
export class DirectiveBuilder {
directiveIndex:number;
propertyBindings: Map<string, ASTWithSource>;
eventBindings: Map<string, ASTWithSource>;
propertySetters: Map<string, SetterFn>;
constructor(directiveIndex) {
this.directiveIndex = directiveIndex;
this.propertyBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
this.propertySetters = MapWrapper.create();
}
bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression);
}
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
}
bindPropertySetter(propertyName, setter) {
MapWrapper.set(this.propertySetters, propertyName, setter);
}
}
export class EventLocalsAstSplitter extends AstTransformer {
locals:List<AST>;
eventNames:List<string>;
_implicitReceiver:AST;
constructor() {
super();
this.locals = [];
this.eventNames = [];
this._implicitReceiver = new ImplicitReceiver();
}
splitEventAstIntoLocals(eventBindings:Map<string, ASTWithSource>):Map<string, ASTWithSource> {
if (isPresent(eventBindings)) {
var result = MapWrapper.create();
MapWrapper.forEach(eventBindings, (astWithSource, eventName) => {
MapWrapper.set(result, eventName, astWithSource.ast.visit(this));
ListWrapper.push(this.eventNames, eventName);
});
return result;
}
return null;
}
visitAccessMember(ast:AccessMember) {
var isEventAccess = false;
var current = ast;
while (!isEventAccess && (current instanceof AccessMember)) {
if (current.name == '$event') {
isEventAccess = true;
}
current = current.receiver;
}
if (isEventAccess) {
ListWrapper.push(this.locals, ast);
var index = this.locals.length - 1;
return new AccessMember(this._implicitReceiver, `${index}`, (arr) => arr[index], null);
} else {
return ast;
}
}
buildEventLocals() {
return new LiteralArray(this.locals);
}
buildEventNames() {
return this.eventNames;
}
}

View File

@ -0,0 +1,168 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {Locals} from 'angular2/change_detection';
import {ViewContainer} from './view_container';
import {ProtoView} from './proto_view';
import {LightDom} from '../shadow_dom/light_dom';
import {Content} from '../shadow_dom/content_tag';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import {EventDispatcher} from '../../api';
const NG_BINDING_CLASS = 'ng-binding';
/**
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*/
export class View {
boundElements:List;
boundTextNodes:List;
/// When the view is part of render tree, the DocumentFragment is empty, which is why we need
/// to keep track of the nodes.
rootNodes:List;
// TODO(tbosch): move componentChildViews, viewContainers, contentTags, lightDoms into
// a single array with records inside
componentChildViews: List<View>;
viewContainers: List<ViewContainer>;
contentTags: List<Content>;
lightDoms: List<LightDom>;
proto: ProtoView;
_hydrated: boolean;
_eventDispatcher: EventDispatcher;
constructor(
proto:ProtoView, rootNodes:List,
boundTextNodes: List, boundElements:List, viewContainers:List, contentTags:List) {
this.proto = proto;
this.rootNodes = rootNodes;
this.boundTextNodes = boundTextNodes;
this.boundElements = boundElements;
this.viewContainers = viewContainers;
this.contentTags = contentTags;
this.lightDoms = ListWrapper.createFixedSize(boundElements.length);
this.componentChildViews = ListWrapper.createFixedSize(boundElements.length);
this._hydrated = false;
}
hydrated() {
return this._hydrated;
}
setElementProperty(elementIndex:number, propertyName:string, value:any) {
var setter = MapWrapper.get(this.proto.propertySetters, propertyName);
setter(this.boundElements[elementIndex], value);
}
setText(textIndex:number, value:string) {
DOM.setText(this.boundTextNodes[textIndex], value);
}
setComponentView(strategy: ShadowDomStrategy,
elementIndex:number, childView:View) {
var element = this.boundElements[elementIndex];
var lightDom = strategy.constructLightDom(this, childView, element);
strategy.attachTemplate(element, childView);
this.lightDoms[elementIndex] = lightDom;
this.componentChildViews[elementIndex] = childView;
if (this._hydrated) {
childView.hydrate(lightDom);
}
}
getViewContainer(index:number):ViewContainer {
return this.viewContainers[index];
}
_getDestLightDom(binderIndex) {
var binder = this.proto.elementBinders[binderIndex];
var destLightDom = null;
if (binder.parentIndex !== -1 && binder.distanceToParent === 1) {
destLightDom = this.lightDoms[binder.parentIndex];
}
return destLightDom;
}
/**
* A dehydrated view is a state of the view that allows it to be moved around
* the view tree.
*
* A dehydrated view has the following properties:
*
* - all viewcontainers are empty.
*
* A call to hydrate/dehydrate does not attach/detach the view from the view
* tree.
*/
hydrate(hostLightDom: LightDom) {
if (this._hydrated) throw new BaseException('The view is already hydrated.');
this._hydrated = true;
// viewContainers and content tags
for (var i = 0; i < this.viewContainers.length; i++) {
var vc = this.viewContainers[i];
var destLightDom = this._getDestLightDom(i);
if (isPresent(vc)) {
vc.hydrate(destLightDom, hostLightDom);
}
var ct = this.contentTags[i];
if (isPresent(ct)) {
ct.hydrate(destLightDom);
}
}
// componentChildViews
for (var i = 0; i < this.componentChildViews.length; i++) {
var cv = this.componentChildViews[i];
if (isPresent(cv)) {
cv.hydrate(this.lightDoms[i]);
}
}
for (var i = 0; i < this.lightDoms.length; ++i) {
var lightDom = this.lightDoms[i];
if (isPresent(lightDom)) {
lightDom.redistribute();
}
}
}
dehydrate() {
// Note: preserve the opposite order of the hydration process.
// componentChildViews
for (var i = 0; i < this.componentChildViews.length; i++) {
this.componentChildViews[i].dehydrate();
}
// viewContainers and content tags
if (isPresent(this.viewContainers)) {
for (var i = 0; i < this.viewContainers.length; i++) {
var vc = this.viewContainers[i];
if (isPresent(vc)) {
vc.dehydrate();
}
var ct = this.contentTags[i];
if (isPresent(ct)) {
ct.dehydrate();
}
}
}
this._hydrated = false;
}
setEventDispatcher(dispatcher:EventDispatcher) {
this._eventDispatcher = dispatcher;
}
dispatchEvent(elementIndex, eventName, event) {
if (isPresent(this._eventDispatcher)) {
var evalLocals = MapWrapper.create();
MapWrapper.set(evalLocals, '$event', event);
var localValues = this.proto.elementBinders[elementIndex].eventLocals.eval(null, new Locals(null, evalLocals));
this._eventDispatcher.dispatchEvent(elementIndex, eventName, localValues);
}
}
}

View File

@ -0,0 +1,137 @@
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, List} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import * as viewModule from './view';
import * as ldModule from '../shadow_dom/light_dom';
import * as vfModule from './view_factory';
export class ViewContainer {
_viewFactory: vfModule.ViewFactory;
templateElement;
_views: List<viewModule.View>;
_lightDom: ldModule.LightDom;
_hostLightDom: ldModule.LightDom;
_hydrated: boolean;
constructor(viewFactory: vfModule.ViewFactory,
templateElement) {
this._viewFactory = viewFactory;
this.templateElement = templateElement;
// The order in this list matches the DOM order.
this._views = [];
this._hostLightDom = null;
this._hydrated = false;
}
hydrate(destLightDom: ldModule.LightDom, hostLightDom: ldModule.LightDom) {
this._hydrated = true;
this._hostLightDom = hostLightDom;
this._lightDom = destLightDom;
}
dehydrate() {
if (isBlank(this._lightDom)) {
for (var i = this._views.length - 1; i >= 0; i--) {
var view = this._views[i];
ViewContainer.removeViewNodesFromParent(this.templateElement.parentNode, view);
this._viewFactory.returnView(view);
}
this._views = [];
} else {
for (var i=0; i<this._views.length; i++) {
var view = this._views[i];
this._viewFactory.returnView(view);
}
this._views = [];
this._lightDom.redistribute();
}
this._hostLightDom = null;
this._lightDom = null;
this._hydrated = false;
}
get(index: number): viewModule.View {
return this._views[index];
}
size() {
return this._views.length;
}
_siblingToInsertAfter(index: number) {
if (index == 0) return this.templateElement;
return ListWrapper.last(this._views[index - 1].rootNodes);
}
_checkHydrated() {
if (!this._hydrated) throw new BaseException(
'Cannot change dehydrated ViewContainer');
}
insert(view, atIndex=-1): viewModule.View {
this._checkHydrated();
if (atIndex == -1) atIndex = this._views.length;
ListWrapper.insert(this._views, atIndex, view);
if (!view.hydrated()) {
view.hydrate(this._hostLightDom);
}
if (isBlank(this._lightDom)) {
ViewContainer.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view);
} else {
this._lightDom.redistribute();
}
// new content tags might have appeared, we need to redistribute.
if (isPresent(this._hostLightDom)) {
this._hostLightDom.redistribute();
}
return view;
}
/**
* The method can be used together with insert to implement a view move, i.e.
* moving the dom nodes while the directives in the view stay intact.
*/
detach(atIndex:number) {
this._checkHydrated();
var detachedView = this.get(atIndex);
ListWrapper.removeAt(this._views, atIndex);
if (isBlank(this._lightDom)) {
ViewContainer.removeViewNodesFromParent(this.templateElement.parentNode, detachedView);
} else {
this._lightDom.redistribute();
}
// content tags might have disappeared we need to do redistribution.
if (isPresent(this._hostLightDom)) {
this._hostLightDom.redistribute();
}
return detachedView;
}
contentTagContainers() {
return this._views;
}
nodes():List {
var r = [];
for (var i = 0; i < this._views.length; ++i) {
r = ListWrapper.concat(r, this._views[i].rootNodes);
}
return r;
}
static moveViewNodesAfterSibling(sibling, view) {
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
DOM.insertAfter(sibling, view.rootNodes[i]);
}
}
static removeViewNodesFromParent(parent, view) {
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
DOM.removeChild(parent, view.rootNodes[i]);
}
}
}

View File

@ -0,0 +1,158 @@
import {OpaqueToken} from 'angular2/di';
import {int, isPresent, isBlank, BaseException} 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 {Content} from '../shadow_dom/content_tag';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import {EventManager} from '../events/event_manager';
import {ViewContainer} from './view_container';
import {ProtoView} from './proto_view';
import {View} from './view';
import {NG_BINDING_CLASS_SELECTOR, NG_BINDING_CLASS} from '../util';
export var VIEW_POOL_CAPACITY = new OpaqueToken('ViewFactory.viewPoolCapacity');
export class ViewFactory {
_poolCapacity:number;
_pooledViews:List<View>;
_eventManager:EventManager;
_shadowDomStrategy:ShadowDomStrategy;
constructor(capacity, eventManager, shadowDomStrategy) {
this._poolCapacity = capacity;
this._pooledViews = ListWrapper.create();
this._eventManager = eventManager;
this._shadowDomStrategy = shadowDomStrategy;
}
getView(protoView:ProtoView):View {
// TODO(tbosch): benchmark this scanning of views and maybe
// replace it with a fancy LRU Map/List combination...
var view;
for (var i=0; i<this._pooledViews.length; i++) {
var pooledView = this._pooledViews[i];
if (pooledView.proto === protoView) {
view = ListWrapper.removeAt(this._pooledViews, i);
}
}
if (isBlank(view)) {
view = this._createView(protoView);
}
return view;
}
returnView(view:View) {
if (view.hydrated()) {
view.dehydrate();
}
ListWrapper.push(this._pooledViews, view);
while (this._pooledViews.length > this._poolCapacity) {
ListWrapper.removeAt(this._pooledViews, 0);
}
}
_createView(protoView:ProtoView): View {
var rootElementClone = protoView.isRootView ? protoView.element : DOM.importIntoDoc(protoView.element);
var elementsWithBindingsDynamic;
if (protoView.isTemplateElement) {
elementsWithBindingsDynamic = DOM.querySelectorAll(DOM.content(rootElementClone), NG_BINDING_CLASS_SELECTOR);
} else {
elementsWithBindingsDynamic = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS);
}
var elementsWithBindings = ListWrapper.createFixedSize(elementsWithBindingsDynamic.length);
for (var binderIdx = 0; binderIdx < elementsWithBindingsDynamic.length; ++binderIdx) {
elementsWithBindings[binderIdx] = elementsWithBindingsDynamic[binderIdx];
}
var viewRootNodes;
if (protoView.isTemplateElement) {
var childNode = DOM.firstChild(DOM.content(rootElementClone));
viewRootNodes = []; // TODO(perf): Should be fixed size, since we could pre-compute in in ProtoView
// Note: An explicit loop is the fastest way to convert a DOM array into a JS array!
while(childNode != null) {
ListWrapper.push(viewRootNodes, childNode);
childNode = DOM.nextSibling(childNode);
}
} else {
viewRootNodes = [rootElementClone];
}
var binders = protoView.elementBinders;
var boundTextNodes = [];
var boundElements = ListWrapper.createFixedSize(binders.length);
var viewContainers = ListWrapper.createFixedSize(binders.length);
var contentTags = ListWrapper.createFixedSize(binders.length);
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element;
if (binderIdx === 0 && protoView.rootBindingOffset === 1) {
element = rootElementClone;
} else {
element = elementsWithBindings[binderIdx - protoView.rootBindingOffset];
}
boundElements[binderIdx] = element;
// boundTextNodes
var childNodes = DOM.childNodes(DOM.templateAwareRoot(element));
var textNodeIndices = binder.textNodeIndices;
for (var i = 0; i<textNodeIndices.length; i++) {
ListWrapper.push(boundTextNodes, childNodes[textNodeIndices[i]]);
}
// viewContainers
var viewContainer = null;
if (isBlank(binder.componentId) && isPresent(binder.nestedProtoView)) {
viewContainer = new ViewContainer(this, element);
}
viewContainers[binderIdx] = viewContainer;
// contentTags
var contentTag = null;
if (isPresent(binder.contentTagSelector)) {
contentTag = new Content(element, binder.contentTagSelector);
}
contentTags[binderIdx] = contentTag;
}
var view = new View(
protoView, viewRootNodes,
boundTextNodes, boundElements, viewContainers, contentTags
);
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element = boundElements[binderIdx];
// static child components
if (isPresent(binder.componentId) && isPresent(binder.nestedProtoView)) {
var childView = this._createView(binder.nestedProtoView);
view.setComponentView(this._shadowDomStrategy, binderIdx, childView);
}
// events
if (isPresent(binder.eventLocals)) {
ListWrapper.forEach(binder.eventNames, (eventName) => {
this._createEventListener(view, element, binderIdx, eventName, binder.eventLocals);
});
}
}
if (protoView.isRootView) {
view.hydrate(null);
}
return view;
}
_createEventListener(view, element, elementIndex, eventName, eventLocals) {
this._eventManager.addEventListener(element, eventName, (event) => {
view.dispatchEvent(elementIndex, eventName, event);
});
}
}