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

View File

@ -1,4 +1,6 @@
export {AST} from './src/change_detection/parser/ast';
export {
ASTWithSource, AST, AstTransformer, AccessMember, LiteralArray, ImplicitReceiver
} from './src/change_detection/parser/ast';
export {Lexer} from './src/change_detection/parser/lexer';
export {Parser} from './src/change_detection/parser/parser';
export {Locals}

View File

@ -446,6 +446,72 @@ export class AstVisitor {
visitPrefixNot(ast:PrefixNot) {}
}
export class AstTransformer {
visitImplicitReceiver(ast:ImplicitReceiver) {
return new ImplicitReceiver();
}
visitInterpolation(ast:Interpolation) {
return new Interpolation(ast.strings, this.visitAll(ast.expressions));
}
visitLiteralPrimitive(ast:LiteralPrimitive) {
return new LiteralPrimitive(ast.value);
}
visitAccessMember(ast:AccessMember) {
return new AccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter);
}
visitMethodCall(ast:MethodCall) {
return new MethodCall(ast.receiver.visit(this), ast.name, ast.fn, this.visitAll(ast.args));
}
visitFunctionCall(ast:FunctionCall) {
return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args));
}
visitLiteralArray(ast:LiteralArray) {
return new LiteralArray(this.visitAll(ast.expressions));
}
visitLiteralMap(ast:LiteralMap) {
return new LiteralMap(ast.keys, this.visitAll(ast.values));
}
visitBinary(ast:Binary) {
return new Binary(ast.operation, ast.left.visit(this), ast.right.visit(this));
}
visitPrefixNot(ast:PrefixNot) {
return new PrefixNot(ast.expression.visit(this));
}
visitConditional(ast:Conditional) {
return new Conditional(
ast.condition.visit(this),
ast.trueExp.visit(this),
ast.falseExp.visit(this)
);
}
visitPipe(ast:Pipe) {
return new Pipe(ast.exp.visit(this), ast.name, this.visitAll(ast.args), ast.inBinding);
}
visitKeyedAccess(ast:KeyedAccess) {
return new KeyedAccess(ast.obj.visit(this), ast.key.visit(this));
}
visitAll(asts:List) {
var res = ListWrapper.createFixedSize(asts.length);
for (var i = 0; i < asts.length; ++i) {
res[i] = asts[i].visit(this);
}
return res;
}
}
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0],
[0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0]];

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);
});
}
}

View File

@ -0,0 +1,36 @@
import {isPresent, isBlank, RegExpWrapper, BaseException} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
export class UrlResolver {
static a;
constructor() {
if (isBlank(UrlResolver.a)) {
UrlResolver.a = DOM.createElement('a');
}
}
resolve(baseUrl: string, url: string): string {
if (isBlank(baseUrl)) {
DOM.resolveAndSetHref(UrlResolver.a, url, null);
return DOM.getHref(UrlResolver.a);
}
if (isBlank(url) || url == '') return baseUrl;
if (url[0] == '/') {
throw new BaseException(`Could not resolve the url ${url} from ${baseUrl}`);
}
var m = RegExpWrapper.firstMatch(_schemeRe, url);
if (isPresent(m[1])) {
return url;
}
DOM.resolveAndSetHref(UrlResolver.a, baseUrl, url);
return DOM.getHref(UrlResolver.a);
}
}
var _schemeRe = RegExpWrapper.create('^([^:/?#]+:)?');

10
modules/angular2/src/services/xhr.js vendored Normal file
View File

@ -0,0 +1,10 @@
// import {Promise} from 'angular2/src/facade/async';
export {XHR} from 'angular2/src/core/compiler/xhr/xhr';
// TODO: export an own interface as soon as core/compiler/xhr is no more needed...
// export class XHR {
// get(url: string): Promise<string> {
// return null;
// }
// }

View File

@ -0,0 +1,12 @@
import 'dart:async';
import 'dart:html';
import './xhr.dart' show XHR;
class XHRImpl extends XHR {
Future<String> get(String url) {
return HttpRequest.request(url).then(
(HttpRequest request) => request.responseText,
onError: (Error e) => throw 'Failed to load $url'
);
}
}

View File

@ -0,0 +1,27 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {XHR} from './xhr';
export class XHRImpl extends XHR {
get(url: string): Promise<string> {
var completer = PromiseWrapper.completer();
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onload = function() {
var status = xhr.status;
if (200 <= status && status <= 300) {
completer.resolve(xhr.responseText);
} else {
completer.reject(`Failed to load ${url}`);
}
};
xhr.onerror = function() {
completer.reject(`Failed to load ${url}`);
};
xhr.send();
return completer.promise;
}
}

View File

@ -0,0 +1,9 @@
/*
* Runs compiler tests using in-browser DOM adapter.
*/
import {runCompilerCommonTests} from './compiler_common_tests';
export function main() {
runCompilerCommonTests();
}

View File

@ -0,0 +1,176 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
IS_DARTIUM,
it,
} from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {List, ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {Type, isBlank, stringify, isPresent} from 'angular2/src/facade/lang';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {Compiler, CompilerCache} from 'angular2/src/render/dom/compiler/compiler';
import {ProtoView, Template} from 'angular2/src/render/api';
import {CompileElement} from 'angular2/src/render/dom/compiler/compile_element';
import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step'
import {CompileStepFactory} from 'angular2/src/render/dom/compiler/compile_step_factory';
import {CompileControl} from 'angular2/src/render/dom/compiler/compile_control';
import {TemplateLoader} from 'angular2/src/render/dom/compiler/template_loader';
import {UrlResolver} from 'angular2/src/services/url_resolver';
export function runCompilerCommonTests() {
describe('compiler', function() {
var mockStepFactory;
function createCompiler(processClosure, urlData = null) {
if (isBlank(urlData)) {
urlData = MapWrapper.create();
}
var tplLoader = new FakeTemplateLoader(urlData);
mockStepFactory = new MockStepFactory([new MockStep(processClosure)]);
return new Compiler(mockStepFactory, tplLoader);
}
it('should run the steps and build the ProtoView of the root element', inject([AsyncTestCompleter], (async) => {
var compiler = createCompiler((parent, current, control) => {
current.inheritedProtoView.bindVariable('b', 'a');
});
compiler.compile(new Template({
componentId: 'someComponent',
inline: '<div></div>'
})).then( (protoView) => {
expect(protoView.variableBindings).toEqual(MapWrapper.createFromStringMap({
'a': 'b'
}));
async.done();
});
}));
it('should use the inline template and compile in sync', inject([AsyncTestCompleter], (async) => {
var compiler = createCompiler(EMPTY_STEP);
compiler.compile(new Template({
componentId: 'someId',
inline: 'inline component'
})).then( (protoView) => {
expect(DOM.getInnerHTML(protoView.render.delegate.element)).toEqual('inline component');
async.done();
});
}));
it('should load url templates', inject([AsyncTestCompleter], (async) => {
var urlData = MapWrapper.createFromStringMap({
'someUrl': 'url component'
});
var compiler = createCompiler(EMPTY_STEP, urlData);
compiler.compile(new Template({
componentId: 'someId',
absUrl: 'someUrl'
})).then( (protoView) => {
expect(DOM.getInnerHTML(protoView.render.delegate.element)).toEqual('url component');
async.done();
});
}));
it('should report loading errors', inject([AsyncTestCompleter], (async) => {
var compiler = createCompiler(EMPTY_STEP, MapWrapper.create());
PromiseWrapper.catchError(compiler.compile(new Template({
componentId: 'someId',
absUrl: 'someUrl'
})), (e) => {
expect(e.message).toContain(`Failed to load the template "someId"`);
async.done();
});
}));
it('should wait for async subtasks to be resolved', inject([AsyncTestCompleter], (async) => {
var subTasksCompleted = false;
var completer = PromiseWrapper.completer();
var compiler = createCompiler( (parent, current, control) => {
ListWrapper.push(mockStepFactory.subTaskPromises, completer.promise.then((_) => {
subTasksCompleted = true;
}));
});
// It should always return a Promise because the subtask is async
var pvPromise = compiler.compile(new Template({
componentId: 'someId',
inline: 'some component'
}));
expect(pvPromise).toBePromise();
expect(subTasksCompleted).toEqual(false);
// The Promise should resolve after the subtask is ready
completer.resolve(null);
pvPromise.then((protoView) => {
expect(subTasksCompleted).toEqual(true);
async.done();
});
}));
});
}
class MockStepFactory extends CompileStepFactory {
steps:List<CompileStep>;
subTaskPromises:List<Promise>;
constructor(steps) {
super();
this.steps = steps;
}
createSteps(template, subTaskPromises) {
this.subTaskPromises = subTaskPromises;
ListWrapper.forEach(this.subTaskPromises, (p) => ListWrapper.push(subTaskPromises, p) );
return this.steps;
}
}
class MockStep extends CompileStep {
processClosure:Function;
constructor(process) {
super();
this.processClosure = process;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
this.processClosure(parent, current, control);
}
}
var EMPTY_STEP = (parent, current, control) => {
if (isPresent(parent)) {
current.inheritedProtoView = parent.inheritedProtoView;
}
};
class FakeTemplateLoader extends TemplateLoader {
_urlData: Map<string, string>;
constructor(urlData) {
super(null, new UrlResolver());
this._urlData = urlData;
}
load(template: Template) {
if (isPresent(template.inline)) {
return PromiseWrapper.resolve(DOM.createTemplate(template.inline));
}
if (isPresent(template.absUrl)) {
var content = MapWrapper.get(this._urlData, template.absUrl);
if (isPresent(content)) {
return PromiseWrapper.resolve(DOM.createTemplate(content));
}
}
return PromiseWrapper.reject('Load failed');
}
}

View File

@ -0,0 +1,11 @@
library angular2.compiler.html5lib_dom_adapter.test;
import 'package:angular2/src/dom/html_adapter.dart';
import 'package:angular2/src/test_lib/test_lib.dart' show testSetup;
import 'compiler_common_tests.dart';
void main() {
Html5LibDomAdapter.makeCurrent();
testSetup();
runCompilerCommonTests();
}

View File

@ -0,0 +1,228 @@
import {describe, beforeEach, it, xit, expect, iit, ddescribe, el} from 'angular2/test_lib';
import {isPresent, assertionsEnabled} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DirectiveParser} from 'angular2/src/render/dom/compiler/directive_parser';
import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline';
import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step';
import {CompileElement} from 'angular2/src/render/dom/compiler/compile_element';
import {CompileControl} from 'angular2/src/render/dom/compiler/compile_control';
import {Template, DirectiveMetadata} from 'angular2/src/render/api';
import {Lexer, Parser} from 'angular2/change_detection';
export function main() {
describe('DirectiveParser', () => {
var parser, annotatedDirectives;
beforeEach( () => {
annotatedDirectives = [
someComponent,
someComponent2,
someViewport,
someViewport2,
someDecorator,
someDecoratorIgnoringChildren,
someDecoratorWithProps,
someDecoratorWithEvents
];
parser = new Parser(new Lexer());
});
function createPipeline(propertyBindings = null) {
return new CompilePipeline([
new MockStep( (parent, current, control) => {
if (isPresent(propertyBindings)) {
StringMapWrapper.forEach(propertyBindings, (ast, name) => {
current.bindElement().bindProperty(name, ast);
});
}
}),
new DirectiveParser(parser, annotatedDirectives)
]);
}
function process(el, propertyBindings = null) {
var pipeline = createPipeline(propertyBindings);
return ListWrapper.map(pipeline.process(el), (ce) => ce.inheritedElementBinder );
}
it('should not add directives if they are not used', () => {
var results = process(el('<div></div>'));
expect(results[0]).toBe(null);
});
it('should detect directives in attributes', () => {
var results = process(el('<div some-decor></div>'));
expect(results[0].directives[0].directiveIndex).toBe(
annotatedDirectives.indexOf(someDecorator)
);
});
it('should compile children by default', () => {
var results = createPipeline().process(el('<div some-decor></div>'));
expect(results[0].compileChildren).toEqual(true);
});
it('should stop compiling children when specified in the directive config', () => {
var results = createPipeline().process(el('<div some-decor-ignoring-children></div>'));
expect(results[0].compileChildren).toEqual(false);
});
it('should bind directive properties from bound properties', () => {
var results = process(
el('<div some-decor-props></div>'),
{
'elProp': parser.parseBinding('someExpr', '')
}
);
var directiveBinding = results[0].directives[0];
expect(MapWrapper.get(directiveBinding.propertyBindings, 'dirProp').source)
.toEqual('someExpr');
});
it('should bind directive properties with pipes', () => {
var results = process(
el('<div some-decor-props></div>'),
{
'elProp': parser.parseBinding('someExpr', '')
}
);
var directiveBinding = results[0].directives[0];
var pipedProp = MapWrapper.get(directiveBinding.propertyBindings, 'doubleProp');
var simpleProp = MapWrapper.get(directiveBinding.propertyBindings, 'dirProp');
expect(pipedProp.ast.name).toEqual('double');
expect(pipedProp.ast.exp).toEqual(simpleProp.ast);
expect(simpleProp.source).toEqual('someExpr');
});
it('should bind directive properties from attribute values', () => {
var results = process(
el('<div some-decor-props el-prop="someValue"></div>')
);
var directiveBinding = results[0].directives[0];
var simpleProp = MapWrapper.get(directiveBinding.propertyBindings, 'dirProp');
expect(simpleProp.source).toEqual('someValue');
});
it('should store working property setters', () => {
var element = el('<input some-decor-props>');
var results = process(element);
var directiveBinding = results[0].directives[0];
var setter = MapWrapper.get(directiveBinding.propertySetters, 'value');
setter(element, 'abc');
expect(element.value).toEqual('abc');
});
it('should bind directive events', () => {
var results = process(
el('<div some-decor-events></div>')
);
var directiveBinding = results[0].directives[0];
expect(MapWrapper.get(directiveBinding.eventBindings, 'click').source)
.toEqual('doIt()');
});
describe('viewport directives', () => {
it('should not allow multiple viewport directives on the same element', () => {
expect( () => {
process(
el('<template some-vp some-vp2></template>')
);
}).toThrowError('Only one viewport directive is allowed per element - check <template some-vp some-vp2>');
});
it('should not allow viewport directives on non <template> elements', () => {
expect( () => {
process(
el('<div some-vp></div>')
);
}).toThrowError('Viewport directives need to be placed on <template> elements or elements with template attribute - check <div some-vp>');
});
});
describe('component directives', () => {
it('should save the component id', () => {
var results = process(
el('<div some-comp></div>')
);
expect(results[0].componentId).toEqual('someComponent');
});
it('should not allow multiple component directives on the same element', () => {
expect( () => {
process(
el('<div some-comp some-comp2></div>')
);
}).toThrowError('Only one component directive is allowed per element - check <div some-comp some-comp2>');
});
it('should not allow component directives on <template> elements', () => {
expect( () => {
process(
el('<template some-comp></template>')
);
}).toThrowError('Only template directives are allowed on template elements - check <template some-comp>');
});
});
});
}
class MockStep extends CompileStep {
processClosure:Function;
constructor(process) {
super();
this.processClosure = process;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
this.processClosure(parent, current, control);
}
}
var someComponent = new DirectiveMetadata({
selector: '[some-comp]',
id: 'someComponent',
type: DirectiveMetadata.COMPONENT_TYPE
});
var someComponent2 = new DirectiveMetadata({
selector: '[some-comp2]',
id: 'someComponent2',
type: DirectiveMetadata.COMPONENT_TYPE
});
var someViewport = new DirectiveMetadata({
selector: '[some-vp]',
id: 'someViewport',
type: DirectiveMetadata.VIEWPORT_TYPE
});
var someViewport2 = new DirectiveMetadata({
selector: '[some-vp2]',
id: 'someViewport2',
type: DirectiveMetadata.VIEWPORT_TYPE
});
var someDecorator = new DirectiveMetadata({
selector: '[some-decor]'
});
var someDecoratorIgnoringChildren = new DirectiveMetadata({
selector: '[some-decor-ignoring-children]',
compileChildren: false
});
var someDecoratorWithProps = new DirectiveMetadata({
selector: '[some-decor-props]',
bind: MapWrapper.createFromStringMap({
'dirProp': 'elProp',
'doubleProp': 'elProp | double'
}),
setters: ['value']
});
var someDecoratorWithEvents = new DirectiveMetadata({
selector: '[some-decor-events]',
events: MapWrapper.createFromStringMap({
'click': 'doIt()'
})
});

View File

@ -0,0 +1,226 @@
import {describe, beforeEach, it, expect, iit, ddescribe, el} from 'angular2/test_lib';
import {ListWrapper, List, MapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isPresent, NumberWrapper, StringWrapper} from 'angular2/src/facade/lang';
import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline';
import {CompileElement} from 'angular2/src/render/dom/compiler/compile_element';
import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step'
import {CompileControl} from 'angular2/src/render/dom/compiler/compile_control';
import {ProtoViewBuilder} from 'angular2/src/render/dom/view/proto_view_builder';
export function main() {
describe('compile_pipeline', () => {
describe('children compilation', () => {
it('should walk the tree in depth first order including template contents', () => {
var element = el('<div id="1"><template id="2"><span id="3"></span></template></div>');
var step0Log = [];
var results = new CompilePipeline([createLoggerStep(step0Log)]).process(element);
expect(step0Log).toEqual(['1', '1<2', '2<3']);
expect(resultIdLog(results)).toEqual(['1', '2', '3']);
});
it('should stop walking the tree when compileChildren is false', () => {
var element = el('<div id="1"><template id="2" ignore-children><span id="3"></span></template></div>');
var step0Log = [];
var pipeline = new CompilePipeline([new IgnoreChildrenStep(), createLoggerStep(step0Log)]);
var results = pipeline.process(element);
expect(step0Log).toEqual(['1', '1<2']);
expect(resultIdLog(results)).toEqual(['1', '2']);
});
});
it('should inherit protoViewBuilders to children', () => {
var element = el('<div><div><span viewroot><span></span></span></div></div>');
var pipeline = new CompilePipeline([new MockStep((parent, current, control) => {
if (isPresent(DOM.getAttribute(current.element, 'viewroot'))) {
current.inheritedProtoView = new ProtoViewBuilder(current.element);
}
})]);
var results = pipeline.process(element);
expect(results[0].inheritedProtoView).toBe(results[1].inheritedProtoView);
expect(results[2].inheritedProtoView).toBe(results[3].inheritedProtoView);
});
it('should inherit elementBinderBuilders to children', () => {
var element = el('<div bind><div><span bind><span></span></span></div></div>');
var pipeline = new CompilePipeline([new MockStep((parent, current, control) => {
if (isPresent(DOM.getAttribute(current.element, 'bind'))) {
current.bindElement();
}
})]);
var results = pipeline.process(element);
expect(results[0].inheritedElementBinder).toBe(results[1].inheritedElementBinder);
expect(results[2].inheritedElementBinder).toBe(results[3].inheritedElementBinder);
});
it('should mark root elements as viewRoot', () => {
var rootElement = el('<div></div>');
var results = new CompilePipeline([]).process(rootElement);
expect(results[0].isViewRoot).toBe(true);
});
it('should calculate distanceToParent / parent correctly', () => {
var element = el('<div bind><div bind></div><div><div bind></div></div></div>');
var pipeline = new CompilePipeline([new MockStep((parent, current, control) => {
if (isPresent(DOM.getAttribute(current.element, 'bind'))) {
current.bindElement();
}
})]);
var results = pipeline.process(element);
expect(results[0].inheritedElementBinder.distanceToParent).toBe(0);
expect(results[1].inheritedElementBinder.distanceToParent).toBe(1);
expect(results[3].inheritedElementBinder.distanceToParent).toBe(2);
expect(results[1].inheritedElementBinder.parent).toBe(results[0].inheritedElementBinder);
expect(results[3].inheritedElementBinder.parent).toBe(results[0].inheritedElementBinder);
});
describe('control.addParent', () => {
it('should report the new parent to the following processor and the result', () => {
var element = el('<div id="1"><span wrap0="1" id="2"><b id="3"></b></span></div>');
var step0Log = [];
var step1Log = [];
var pipeline = new CompilePipeline([
createWrapperStep('wrap0', step0Log),
createLoggerStep(step1Log)
]);
var result = pipeline.process(element);
expect(step0Log).toEqual(['1', '1<2', '2<3']);
expect(step1Log).toEqual(['1', '1<wrap0#0', 'wrap0#0<2', '2<3']);
expect(resultIdLog(result)).toEqual(['1', 'wrap0#0', '2', '3']);
});
it('should allow to add a parent by multiple processors to the same element', () => {
var element = el('<div id="1"><span wrap0="1" wrap1="1" id="2"><b id="3"></b></span></div>');
var step0Log = [];
var step1Log = [];
var step2Log = [];
var pipeline = new CompilePipeline([
createWrapperStep('wrap0', step0Log),
createWrapperStep('wrap1', step1Log),
createLoggerStep(step2Log)
]);
var result = pipeline.process(element);
expect(step0Log).toEqual(['1', '1<2', '2<3']);
expect(step1Log).toEqual(['1', '1<wrap0#0', 'wrap0#0<2', '2<3']);
expect(step2Log).toEqual(['1', '1<wrap0#0', 'wrap0#0<wrap1#0', 'wrap1#0<2', '2<3']);
expect(resultIdLog(result)).toEqual(['1', 'wrap0#0', 'wrap1#0', '2', '3']);
});
it('should allow to add a parent by multiple processors to different elements', () => {
var element = el('<div id="1"><span wrap0="1" id="2"><b id="3" wrap1="1"></b></span></div>');
var step0Log = [];
var step1Log = [];
var step2Log = [];
var pipeline = new CompilePipeline([
createWrapperStep('wrap0', step0Log),
createWrapperStep('wrap1', step1Log),
createLoggerStep(step2Log)
]);
var result = pipeline.process(element);
expect(step0Log).toEqual(['1', '1<2', '2<3']);
expect(step1Log).toEqual(['1', '1<wrap0#0', 'wrap0#0<2', '2<3']);
expect(step2Log).toEqual(['1', '1<wrap0#0', 'wrap0#0<2', '2<wrap1#0', 'wrap1#0<3']);
expect(resultIdLog(result)).toEqual(['1', 'wrap0#0', '2', 'wrap1#0', '3']);
});
it('should allow to add multiple parents by the same processor', () => {
var element = el('<div id="1"><span wrap0="2" id="2"><b id="3"></b></span></div>');
var step0Log = [];
var step1Log = [];
var pipeline = new CompilePipeline([
createWrapperStep('wrap0', step0Log),
createLoggerStep(step1Log)
]);
var result = pipeline.process(element);
expect(step0Log).toEqual(['1', '1<2', '2<3']);
expect(step1Log).toEqual(['1', '1<wrap0#0', 'wrap0#0<wrap0#1', 'wrap0#1<2', '2<3']);
expect(resultIdLog(result)).toEqual(['1', 'wrap0#0', 'wrap0#1', '2', '3']);
});
});
describe('control.addChild', () => {
it('should report the new child to all processors and the result', () => {
var element = el('<div id="1"><div id="2"></div></div>');
var resultLog = [];
var newChild = new CompileElement(el('<div id="3"></div>'));
var pipeline = new CompilePipeline([
new MockStep((parent, current, control) => {
if (StringWrapper.equals(DOM.getAttribute(current.element, 'id'), '1')) {
control.addChild(newChild);
}
}),
createLoggerStep(resultLog)
]);
var result = pipeline.process(element);
expect(result[2]).toBe(newChild);
expect(resultLog).toEqual(['1', '1<2', '1<3']);
expect(resultIdLog(result)).toEqual(['1', '2', '3']);
});
});
});
}
class MockStep extends CompileStep {
processClosure:Function;
constructor(process) {
super();
this.processClosure = process;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
this.processClosure(parent, current, control);
}
}
export class IgnoreChildrenStep extends CompileStep {
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var attributeMap = DOM.attributeMap(current.element);
if (MapWrapper.contains(attributeMap, 'ignore-children')) {
current.compileChildren = false;
}
}
}
function logEntry(log, parent, current) {
var parentId = '';
if (isPresent(parent)) {
parentId = DOM.getAttribute(parent.element, 'id') + '<';
}
ListWrapper.push(log, parentId + DOM.getAttribute(current.element, 'id'));
}
function createLoggerStep(log) {
return new MockStep((parent, current, control) => {
logEntry(log, parent, current);
});
}
function createWrapperStep(wrapperId, log) {
var nextElementId = 0;
return new MockStep((parent, current, control) => {
var parentCountStr = DOM.getAttribute(current.element, wrapperId);
if (isPresent(parentCountStr)) {
var parentCount = NumberWrapper.parseInt(parentCountStr, 10);
while (parentCount > 0) {
control.addParent(new CompileElement(el(`<a id="${wrapperId}#${nextElementId++}"></a>`)));
parentCount--;
}
}
logEntry(log, parent, current);
});
}
function resultIdLog(result) {
var idLog = [];
ListWrapper.forEach(result, (current) => {
logEntry(idLog, null, current);
});
return idLog;
}

View File

@ -0,0 +1,168 @@
import {describe, beforeEach, it, expect, iit, ddescribe, el} from 'angular2/test_lib';
import {PropertyBindingParser} from 'angular2/src/render/dom/compiler/property_binding_parser';
import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline';
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {CompileElement} from 'angular2/src/render/dom/compiler/compile_element';
import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step'
import {CompileControl} from 'angular2/src/render/dom/compiler/compile_control';
import {Lexer, Parser} from 'angular2/change_detection';
var EMPTY_MAP = MapWrapper.create();
export function main() {
describe('PropertyBindingParser', () => {
function createPipeline(ignoreBindings = false, hasNestedProtoView = false) {
return new CompilePipeline([
new MockStep((parent, current, control) => {
current.ignoreBindings = ignoreBindings;
if (hasNestedProtoView) {
current.bindElement().bindNestedProtoView(el('<template></template>'));
}
}),
new PropertyBindingParser(new Parser(new Lexer()))]);
}
function process(element, ignoreBindings = false, hasNestedProtoView = false) {
return ListWrapper.map(
createPipeline(ignoreBindings, hasNestedProtoView).process(element),
(compileElement) => compileElement.inheritedElementBinder
);
}
it('should not parse bindings when ignoreBindings is true', () => {
var results = process(el('<div [a]="b"></div>'), true);
expect(results[0]).toEqual(null);
});
it('should detect [] syntax', () => {
var results = process(el('<div [a]="b"></div>'));
expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('b');
});
it('should detect [] syntax only if an attribute name starts and ends with []', () => {
expect(process(el('<div z[a]="b"></div>'))[0]).toBe(null);
expect(process(el('<div [a]v="b"></div>'))[0]).toBe(null);
});
it('should detect bind- syntax', () => {
var results = process(el('<div bind-a="b"></div>'));
expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('b');
});
it('should detect bind- syntax only if an attribute name starts with bind', () => {
expect(process(el('<div _bind-a="b"></div>'))[0]).toEqual(null);
});
it('should detect interpolation syntax', () => {
var results = process(el('<div a="{{b}}"></div>'));
expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('{{b}}');
});
it('should detect var- syntax', () => {
var results = process(el('<template var-a="b"></template>'));
expect(MapWrapper.get(results[0].variableBindings, 'b')).toEqual('a');
});
it('should store variable binding for a template element on the nestedProtoView', () => {
var results = process(el('<template var-george="washington"></p>'), false, true);
expect(results[0].variableBindings).toEqual(EMPTY_MAP);
expect(MapWrapper.get(results[0].nestedProtoView.variableBindings, 'washington')).toEqual('george');
});
it('should store variable binding for a non-template element using shorthand syntax on the nestedProtoView', () => {
var results = process(el('<template #george="washington"></template>'), false, true);
expect(results[0].variableBindings).toEqual(EMPTY_MAP);
expect(MapWrapper.get(results[0].nestedProtoView.variableBindings, 'washington')).toEqual('george');
});
it('should store variable binding for a non-template element', () => {
var results = process(el('<p var-george="washington"></p>'));
expect(MapWrapper.get(results[0].variableBindings, 'washington')).toEqual('george');
});
it('should store variable binding for a non-template element using shorthand syntax', () => {
var results = process(el('<p #george="washington"></p>'));
expect(MapWrapper.get(results[0].variableBindings, 'washington')).toEqual('george');
});
it('should store a variable binding with an implicit value', () => {
var results = process(el('<p var-george></p>'));
expect(MapWrapper.get(results[0].variableBindings, '\$implicit')).toEqual('george');
});
it('should store a variable binding with an implicit value using shorthand syntax', () => {
var results = process(el('<p #george></p>'));
expect(MapWrapper.get(results[0].variableBindings, '\$implicit')).toEqual('george');
});
it('should detect variable bindings only if an attribute name starts with #', () => {
var results = process(el('<p b#george></p>'));
expect(results[0]).toEqual(null);
});
it('should detect () syntax', () => {
var results = process(el('<div (click)="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()');
// "(click[])" is not an expected syntax and is only used to validate the regexp
results = process(el('<div (click[])="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click[]').source).toEqual('b()');
});
it('should detect () syntax only if an attribute name starts and ends with ()', () => {
expect(process(el('<div z(a)="b()"></div>'))[0]).toEqual(null);
expect(process(el('<div (a)v="b()"></div>'))[0]).toEqual(null);
});
it('should parse event handlers using () syntax as actions', () => {
var results = process(el('<div (click)="foo=bar"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('foo=bar');
});
it('should detect on- syntax', () => {
var results = process(el('<div on-click="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()');
});
it('should parse event handlers using on- syntax as actions', () => {
var results = process(el('<div on-click="foo=bar"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('foo=bar');
});
it('should store bound properties as temporal attributes', () => {
var results = createPipeline().process(el('<div bind-a="b" [c]="d"></div>'));
expect(MapWrapper.get(results[0].attrs(), 'a')).toEqual('b');
expect(MapWrapper.get(results[0].attrs(), 'c')).toEqual('d');
});
it('should store variables as temporal attributes', () => {
var results = createPipeline().process(el('<div var-a="b" #c="d"></div>'));
expect(MapWrapper.get(results[0].attrs(), 'a')).toEqual('b');
expect(MapWrapper.get(results[0].attrs(), 'c')).toEqual('d');
});
it('should store working property setters', () => {
var element = el('<input bind-value="1">');
var results = process(element);
var setter = MapWrapper.get(results[0].propertySetters, 'value');
setter(element, 'abc');
expect(element.value).toEqual('abc');
});
it('should store property setters as camel case', () => {
var element = el('<div bind-some-prop="1">');
var results = process(element);
expect(MapWrapper.get(results[0].propertySetters, 'someProp')).toBeTruthy();
});
});
}
class MockStep extends CompileStep {
processClosure:Function;
constructor(process) {
super();
this.processClosure = process;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
this.processClosure(parent, current, control);
}
}

View File

@ -0,0 +1,78 @@
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
import {setterFactory} from 'angular2/src/render/dom/compiler/property_setter_factory';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
var div;
beforeEach( () => {
div = el('<div></div>');
});
describe('property setter factory', () => {
it('should return a setter for a property', () => {
var setterFn = setterFactory('title');
setterFn(div, 'Hello');
expect(div.title).toEqual('Hello');
var otherSetterFn = setterFactory('title');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for an attribute', () => {
var setterFn = setterFactory('attr.role');
setterFn(div, 'button');
expect(DOM.getAttribute(div, 'role')).toEqual('button');
setterFn(div, null);
expect(DOM.getAttribute(div, 'role')).toEqual(null);
expect(() => {
setterFn(div, 4);
}).toThrowError("Invalid role attribute, only string values are allowed, got '4'");
var otherSetterFn = setterFactory('attr.role');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a class', () => {
var setterFn = setterFactory('class.active');
setterFn(div, true);
expect(DOM.hasClass(div, 'active')).toEqual(true);
setterFn(div, false);
expect(DOM.hasClass(div, 'active')).toEqual(false);
var otherSetterFn = setterFactory('class.active');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a style', () => {
var setterFn = setterFactory('style.width');
setterFn(div, '40px');
expect(DOM.getStyle(div, 'width')).toEqual('40px');
setterFn(div, null);
expect(DOM.getStyle(div, 'width')).toEqual('');
var otherSetterFn = setterFactory('style.width');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a style with a unit', () => {
var setterFn = setterFactory('style.height.px');
setterFn(div, 40);
expect(DOM.getStyle(div, 'height')).toEqual('40px');
setterFn(div, null);
expect(DOM.getStyle(div, 'height')).toEqual('');
var otherSetterFn = setterFactory('style.height.px');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for innerHtml', () => {
var setterFn = setterFactory('innerHtml');
setterFn(div, '<span></span>');
expect(DOM.getInnerHTML(div)).toEqual('<span></span>');
var otherSetterFn = setterFactory('innerHtml');
expect(setterFn).toBe(otherSetterFn);
});
});
}

View File

@ -0,0 +1,262 @@
import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {SelectorMatcher} from 'angular2/src/render/dom/compiler/selector';
import {CssSelector} from 'angular2/src/render/dom/compiler/selector';
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
export function main() {
describe('SelectorMatcher', () => {
var matcher, matched, selectableCollector, s1, s2, s3, s4;
function reset() {
matched = ListWrapper.create();
}
beforeEach(() => {
reset();
s1 = s2 = s3 = s4 = null;
selectableCollector = (selector, context) => {
ListWrapper.push(matched, selector);
ListWrapper.push(matched, context);
}
matcher = new SelectorMatcher();
});
it('should select by element name case insensitive', () => {
matcher.addSelectables(s1 = CssSelector.parse('someTag'), 1);
expect(matcher.match(CssSelector.parse('SOMEOTHERTAG')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('SOMETAG')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1]);
});
it('should select by class name case insensitive', () => {
matcher.addSelectables(s1 = CssSelector.parse('.someClass'), 1);
matcher.addSelectables(s2 = CssSelector.parse('.someClass.class2'), 2);
expect(matcher.match(CssSelector.parse('.SOMEOTHERCLASS')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('.SOMECLASS')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1]);
reset();
expect(matcher.match(CssSelector.parse('.someClass.class2')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1,s2[0],2]);
});
it('should select by attr name case insensitive independent of the value', () => {
matcher.addSelectables(s1 = CssSelector.parse('[someAttr]'), 1);
matcher.addSelectables(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2);
expect(matcher.match(CssSelector.parse('[SOMEOTHERATTR]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('[SOMEATTR]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1]);
reset();
expect(matcher.match(CssSelector.parse('[SOMEATTR=someValue]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1]);
reset();
expect(matcher.match(CssSelector.parse('[someAttr][someAttr2]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1,s2[0],2]);
});
it('should select by attr name only once if the value is from the DOM', () => {
matcher.addSelectables(s1 = CssSelector.parse('[some-decor]'), 1);
var elementSelector = new CssSelector();
var element = el('<div attr></div>');
var empty = DOM.getAttribute(element, 'attr');
elementSelector.addAttribute('some-decor', empty);
matcher.match(elementSelector, selectableCollector);
expect(matched).toEqual([s1[0],1]);
});
it('should select by attr name and value case insensitive', () => {
matcher.addSelectables(s1 = CssSelector.parse('[someAttr=someValue]'), 1);
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1]);
});
it('should select by element name, class name and attribute name with value', () => {
matcher.addSelectables(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1);
expect(matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1]);
});
it('should select independent of the order in the css selector', () => {
matcher.addSelectables(s1 = CssSelector.parse('[someAttr].someClass'), 1);
matcher.addSelectables(s2 = CssSelector.parse('.someClass[someAttr]'), 2);
matcher.addSelectables(s3 = CssSelector.parse('.class1.class2'), 3);
matcher.addSelectables(s4 = CssSelector.parse('.class2.class1'), 4);
expect(matcher.match(CssSelector.parse('[someAttr].someClass')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1,s2[0],2]);
reset();
expect(matcher.match(CssSelector.parse('.someClass[someAttr]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1,s2[0],2]);
reset();
expect(matcher.match(CssSelector.parse('.class1.class2')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s3[0],3,s4[0],4]);
reset();
expect(matcher.match(CssSelector.parse('.class2.class1')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s4[0],4,s3[0],3]);
});
it('should not select with a matching :not selector', () => {
matcher.addSelectables(CssSelector.parse('p:not(.someClass)'), 1);
matcher.addSelectables(CssSelector.parse('p:not([someAttr])'), 2);
matcher.addSelectables(CssSelector.parse(':not(.someClass)'), 3);
matcher.addSelectables(CssSelector.parse(':not(p)'), 4);
matcher.addSelectables(CssSelector.parse(':not(p[someAttr])'), 5);
expect(matcher.match(CssSelector.parse('p.someClass[someAttr]')[0], selectableCollector)).toEqual(false);
expect(matched).toEqual([]);
});
it('should select with a non matching :not selector', () => {
matcher.addSelectables(s1 = CssSelector.parse('p:not(.someClass)'), 1);
matcher.addSelectables(s2 = CssSelector.parse('p:not(.someOtherClass[someAttr])'), 2);
matcher.addSelectables(s3 = CssSelector.parse(':not(.someClass)'), 3);
matcher.addSelectables(s4 = CssSelector.parse(':not(.someOtherClass[someAttr])'), 4);
expect(matcher.match(CssSelector.parse('p[someOtherAttr].someOtherClass')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1,s2[0],2,s3[0],3,s4[0],4]);
});
it('should select with one match in a list', () => {
matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1);
expect(matcher.match(CssSelector.parse('textbox')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[1],1]);
reset();
expect(matcher.match(CssSelector.parse('input[type=text]')[0], selectableCollector)).toEqual(true);
expect(matched).toEqual([s1[0],1]);
});
it('should not select twice with two matches in a list', () => {
matcher.addSelectables(s1 = CssSelector.parse('input, .someClass'), 1);
expect(matcher.match(CssSelector.parse('input.someclass')[0], selectableCollector)).toEqual(true);
expect(matched.length).toEqual(2);
expect(matched).toEqual([s1[0],1]);
});
});
describe('CssSelector.parse', () => {
it('should detect element names', () => {
var cssSelector = CssSelector.parse('sometag')[0];
expect(cssSelector.element).toEqual('sometag');
expect(cssSelector.toString()).toEqual('sometag');
});
it('should detect class names', () => {
var cssSelector = CssSelector.parse('.someClass')[0];
expect(cssSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('.someclass');
});
it('should detect attr names', () => {
var cssSelector = CssSelector.parse('[attrname]')[0];
expect(cssSelector.attrs).toEqual(['attrname', '']);
expect(cssSelector.toString()).toEqual('[attrname]');
});
it('should detect attr values', () => {
var cssSelector = CssSelector.parse('[attrname=attrvalue]')[0];
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelector.toString()).toEqual('[attrname=attrvalue]');
});
it('should detect multiple parts', () => {
var cssSelector = CssSelector.parse('sometag[attrname=attrvalue].someclass')[0];
expect(cssSelector.element).toEqual('sometag');
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('sometag.someclass[attrname=attrvalue]');
});
it('should detect :not', () => {
var cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)')[0];
expect(cssSelector.element).toEqual('sometag');
expect(cssSelector.attrs.length).toEqual(0);
expect(cssSelector.classNames.length).toEqual(0);
var notSelector = cssSelector.notSelector;
expect(notSelector.element).toEqual(null);
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('sometag:not(.someclass[attrname=attrvalue])');
});
it('should detect :not without truthy', () => {
var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0];
expect(cssSelector.element).toEqual("*");
var notSelector = cssSelector.notSelector;
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
expect(notSelector.classNames).toEqual(['someclass']);
expect(cssSelector.toString()).toEqual('*:not(.someclass[attrname=attrvalue])');
});
it('should throw when nested :not', () => {
expect(() => {
CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')[0];
}).toThrowError('Nesting :not is not allowed in a selector');
});
it('should detect lists of selectors', () => {
var cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag');
expect(cssSelectors.length).toEqual(3);
expect(cssSelectors[0].classNames).toEqual(['someclass']);
expect(cssSelectors[1].attrs).toEqual(['attrname', 'attrvalue']);
expect(cssSelectors[2].element).toEqual('sometag');
});
it('should detect lists of selectors with :not', () => {
var cssSelectors = CssSelector.parse('input[type=text], :not(textarea), textbox:not(.special)');
expect(cssSelectors.length).toEqual(3);
expect(cssSelectors[0].element).toEqual('input');
expect(cssSelectors[0].attrs).toEqual(['type', 'text']);
expect(cssSelectors[1].element).toEqual('*');
expect(cssSelectors[1].notSelector.element).toEqual('textarea');
expect(cssSelectors[2].element).toEqual('textbox');
expect(cssSelectors[2].notSelector.classNames).toEqual(['special']);
});
});
}

View File

@ -0,0 +1,98 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
} from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {TemplateLoader} from 'angular2/src/render/dom/compiler/template_loader';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {Template} from 'angular2/src/render/api';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {XHRMock} from 'angular2/src/mock/xhr_mock';
export function main() {
describe('TemplateLoader', () => {
var loader, xhr;
beforeEach(() => {
xhr = new XHRMock()
loader = new TemplateLoader(xhr, new FakeUrlResolver());
});
it('should load inline templates', inject([AsyncTestCompleter], (async) => {
var template = new Template({inline: 'inline template'});
loader.load(template).then( (el) => {
expect(DOM.content(el)).toHaveText('inline template');
async.done();
});
}));
it('should load templates through XHR', inject([AsyncTestCompleter], (async) => {
xhr.expect('base/foo', 'xhr template');
var template = new Template({absUrl: 'base/foo'});
loader.load(template).then((el) => {
expect(DOM.content(el)).toHaveText('xhr template');
async.done();
});
xhr.flush();
}));
it('should cache template loaded through XHR', inject([AsyncTestCompleter], (async) => {
var firstEl;
xhr.expect('base/foo', 'xhr template');
var template = new Template({absUrl: 'base/foo'});
loader.load(template)
.then((el) => {
firstEl = el;
return loader.load(template);
})
.then((el) =>{
expect(el).toBe(firstEl);
expect(DOM.content(el)).toHaveText('xhr template');
async.done();
});
xhr.flush();
}));
it('should throw when no template is defined', () => {
var template = new Template({inline: null, absUrl: null});
expect(() => loader.load(template))
.toThrowError('Templates should have either their url or inline property set');
});
it('should return a rejected Promise when xhr loading fails', inject([AsyncTestCompleter], (async) => {
xhr.expect('base/foo', null);
var template = new Template({absUrl: 'base/foo'});
PromiseWrapper.then(loader.load(template),
function(_) { throw 'Unexpected response'; },
function(error) {
expect(error).toEqual('Failed to load base/foo');
async.done();
}
)
xhr.flush();
}));
});
}
class SomeComponent {
}
class FakeUrlResolver extends UrlResolver {
constructor() {
super();
}
resolve(baseUrl: string, url: string): string {
return baseUrl + url;
}
}

View File

@ -0,0 +1,82 @@
import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/test_lib';
import {TextInterpolationParser} from 'angular2/src/render/dom/compiler/text_interpolation_parser';
import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline';
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {Lexer, Parser} from 'angular2/change_detection';
import {CompileElement} from 'angular2/src/render/dom/compiler/compile_element';
import {CompileStep} from 'angular2/src/render/dom/compiler/compile_step'
import {CompileControl} from 'angular2/src/render/dom/compiler/compile_control';
import {IgnoreChildrenStep} from './pipeline_spec';
export function main() {
describe('TextInterpolationParser', () => {
function createPipeline(ignoreBindings = false) {
return new CompilePipeline([
new MockStep((parent, current, control) => { current.ignoreBindings = ignoreBindings; }),
new IgnoreChildrenStep(),
new TextInterpolationParser(new Parser(new Lexer()))
]);
}
function process(element, ignoreBindings = false) {
return ListWrapper.map(
createPipeline(ignoreBindings).process(element),
(compileElement) => compileElement.inheritedElementBinder
);
}
function assertTextBinding(elementBinder, bindingIndex, nodeIndex, expression) {
expect(elementBinder.textBindings[bindingIndex].source).toEqual(expression);
expect(elementBinder.textBindingIndices[bindingIndex]).toEqual(nodeIndex);
}
it('should not look for text interpolation when ignoreBindings is true', () => {
var results = process(el('<div>{{expr1}}<span></span>{{expr2}}</div>'), true);
expect(results[0]).toEqual(null);
});
it('should find text interpolation in normal elements', () => {
var result = process(el('<div>{{expr1}}<span></span>{{expr2}}</div>'))[0];
assertTextBinding(result, 0, 0, "{{expr1}}");
assertTextBinding(result, 1, 2, "{{expr2}}");
});
it('should find text interpolation in template elements', () => {
var result = process(el('<template>{{expr1}}<span></span>{{expr2}}</template>'))[0];
assertTextBinding(result, 0, 0, "{{expr1}}");
assertTextBinding(result, 1, 2, "{{expr2}}");
});
it('should allow multiple expressions', () => {
var result = process(el('<div>{{expr1}}{{expr2}}</div>'))[0];
assertTextBinding(result, 0, 0, "{{expr1}}{{expr2}}");
});
it('should not interpolate when compileChildren is false', () => {
var results = process(el('<div>{{included}}<span ignore-children>{{excluded}}</span></div>'));
assertTextBinding(results[0], 0, 0, "{{included}}");
expect(results[1]).toBe(results[0]);
});
it('should allow fixed text before, in between and after expressions', () => {
var result = process(el('<div>a{{expr1}}b{{expr2}}c</div>'))[0];
assertTextBinding(result, 0, 0, "a{{expr1}}b{{expr2}}c");
});
it('should escape quotes in fixed parts', () => {
var result = process(el("<div>'\"a{{expr1}}</div>"))[0];
assertTextBinding(result, 0, 0, "'\"a{{expr1}}");
});
});
}
class MockStep extends CompileStep {
processClosure:Function;
constructor(process) {
super();
this.processClosure = process;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
this.processClosure(parent, current, control);
}
}

View File

@ -0,0 +1,225 @@
import {describe, beforeEach, it, expect, iit, ddescribe, el} from 'angular2/test_lib';
import {MapWrapper} from 'angular2/src/facade/collection';
import {ViewSplitter} from 'angular2/src/render/dom/compiler/view_splitter';
import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Lexer, Parser} from 'angular2/change_detection';
export function main() {
describe('ViewSplitter', () => {
function createPipeline() {
return new CompilePipeline([new ViewSplitter(new Parser(new Lexer()))]);
}
describe('<template> elements', () => {
it('should move the content into a new <template> element and mark that as viewRoot', () => {
var rootElement = el('<div><template if="true">a</template></div>');
var results = createPipeline().process(rootElement);
expect(DOM.getOuterHTML(results[1].element)).toEqual('<template if="true" class="ng-binding"></template>');
expect(results[1].isViewRoot).toBe(false);
expect(DOM.getOuterHTML(results[2].element)).toEqual('<template>a</template>');
expect(results[2].isViewRoot).toBe(true);
});
it('should mark the new <template> element as viewRoot', () => {
var rootElement = el('<div><template if="true">a</template></div>');
var results = createPipeline().process(rootElement);
expect(results[2].isViewRoot).toBe(true);
});
it('should not wrap the root element', () => {
var rootElement = el('<div></div>');
var results = createPipeline().process(rootElement);
expect(results.length).toBe(1);
expect(DOM.getOuterHTML(rootElement)).toEqual('<div></div>');
});
it('should copy over the elementDescription', () => {
var rootElement = el('<div><template if="true">a</template></div>');
var results = createPipeline().process(rootElement);
expect(results[2].elementDescription).toBe(results[1].elementDescription);
});
it('should clean out the inheritedElementBinder', () => {
var rootElement = el('<div><template if="true">a</template></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedElementBinder).toBe(null);
});
it('should create a nestedProtoView', () => {
var rootElement = el('<div><template if="true">a</template></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedProtoView).not.toBe(null);
expect(results[2].inheritedProtoView).toBe(results[1].inheritedElementBinder.nestedProtoView);
expect(DOM.getOuterHTML(results[2].inheritedProtoView.rootElement)).toEqual('<template>a</template>');
});
});
describe('elements with template attribute', () => {
it('should replace the element with an empty <template> element', () => {
var rootElement = el('<div><span template=""></span></div>');
var originalChild = rootElement.childNodes[0];
var results = createPipeline().process(rootElement);
expect(results[0].element).toBe(rootElement);
expect(DOM.getOuterHTML(results[0].element)).toEqual('<div><template class="ng-binding"></template></div>');
expect(DOM.getOuterHTML(results[2].element)).toEqual('<span template=""></span>')
expect(results[2].element).toBe(originalChild);
});
it('should work with top-level template node', () => {
var rootElement = el('<template><div template>x</div></template>');
var originalChild = DOM.content(rootElement).childNodes[0];
var results = createPipeline().process(rootElement);
expect(results[0].element).toBe(rootElement);
expect(results[0].isViewRoot).toBe(true);
expect(results[2].isViewRoot).toBe(true);
expect(DOM.getOuterHTML(results[0].element)).toEqual('<template><template class="ng-binding"></template></template>');
expect(results[2].element).toBe(originalChild);
});
it('should mark the element as viewRoot', () => {
var rootElement = el('<div><div template></div></div>');
var results = createPipeline().process(rootElement);
expect(results[2].isViewRoot).toBe(true);
});
it('should add property bindings from the template attribute', () => {
var rootElement = el('<div><div template="prop:expr"></div></div>');
var results = createPipeline().process(rootElement);
expect(MapWrapper.get(results[1].inheritedElementBinder.propertyBindings, 'prop').source).toEqual('expr');
expect(MapWrapper.get(results[1].attrs(), 'prop')).toEqual('expr');
});
it('should add variable mappings from the template attribute to the nestedProtoView', () => {
var rootElement = el('<div><div template="var varName=mapName"></div></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedProtoView.variableBindings).toEqual(MapWrapper.createFromStringMap({'mapName': 'varName'}));
});
it('should add entries without value as attributes to the element', () => {
var rootElement = el('<div><div template="varname"></div></div>');
var results = createPipeline().process(rootElement);
expect(MapWrapper.get(results[1].attrs(), 'varname')).toEqual('');
expect(results[1].inheritedElementBinder.propertyBindings).toEqual(MapWrapper.create());
expect(results[1].inheritedElementBinder.variableBindings).toEqual(MapWrapper.create());
});
it('should iterate properly after a template dom modification', () => {
var rootElement = el('<div><div template></div><after></after></div>');
var results = createPipeline().process(rootElement);
// 1 root + 2 initial + 1 generated template elements
expect(results.length).toEqual(4);
});
it('should copy over the elementDescription', () => {
var rootElement = el('<div><span template=""></span></div>');
var results = createPipeline().process(rootElement);
expect(results[2].elementDescription).toBe(results[1].elementDescription);
});
it('should clean out the inheritedElementBinder', () => {
var rootElement = el('<div><span template=""></span></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedElementBinder).toBe(null);
});
it('should create a nestedProtoView', () => {
var rootElement = el('<div><span template=""></span></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedProtoView).not.toBe(null);
expect(results[2].inheritedProtoView).toBe(results[1].inheritedElementBinder.nestedProtoView);
expect(DOM.getOuterHTML(results[2].inheritedProtoView.rootElement)).toEqual('<span template=""></span>');
});
});
describe('elements with *directive_name attribute', () => {
it('should replace the element with an empty <template> element', () => {
var rootElement = el('<div><span *if></span></div>');
var originalChild = rootElement.childNodes[0];
var results = createPipeline().process(rootElement);
expect(results[0].element).toBe(rootElement);
expect(DOM.getOuterHTML(results[0].element)).toEqual('<div><template class="ng-binding" if=""></template></div>');
expect(DOM.getOuterHTML(results[2].element)).toEqual('<span *if=""></span>')
expect(results[2].element).toBe(originalChild);
});
it('should mark the element as viewRoot', () => {
var rootElement = el('<div><div *foo="bar"></div></div>');
var results = createPipeline().process(rootElement);
expect(results[2].isViewRoot).toBe(true);
});
it('should work with top-level template node', () => {
var rootElement = el('<template><div *foo>x</div></template>');
var originalChild = DOM.content(rootElement).childNodes[0];
var results = createPipeline().process(rootElement);
expect(results[0].element).toBe(rootElement);
expect(results[0].isViewRoot).toBe(true);
expect(results[2].isViewRoot).toBe(true);
expect(DOM.getOuterHTML(results[0].element)).toEqual('<template><template class="ng-binding" foo=""></template></template>');
expect(results[2].element).toBe(originalChild);
});
it('should add property bindings from the template attribute', () => {
var rootElement = el('<div><div *prop="expr"></div></div>');
var results = createPipeline().process(rootElement);
expect(MapWrapper.get(results[1].inheritedElementBinder.propertyBindings, 'prop').source).toEqual('expr');
expect(MapWrapper.get(results[1].attrs(), 'prop')).toEqual('expr');
});
it('should add variable mappings from the template attribute to the nestedProtoView', () => {
var rootElement = el('<div><div *foreach="var varName=mapName"></div></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedProtoView.variableBindings).toEqual(MapWrapper.createFromStringMap({'mapName': 'varName'}));
});
it('should add entries without value as attribute to the element', () => {
var rootElement = el('<div><div *varname></div></div>');
var results = createPipeline().process(rootElement);
expect(MapWrapper.get(results[1].attrs(), 'varname')).toEqual('');
expect(results[1].inheritedElementBinder.propertyBindings).toEqual(MapWrapper.create());
expect(results[1].inheritedElementBinder.variableBindings).toEqual(MapWrapper.create());
});
it('should iterate properly after a template dom modification', () => {
var rootElement = el('<div><div *foo></div><after></after></div>');
var results = createPipeline().process(rootElement);
// 1 root + 2 initial + 1 generated template elements
expect(results.length).toEqual(4);
});
it('should copy over the elementDescription', () => {
var rootElement = el('<div><span *foo></span></div>');
var results = createPipeline().process(rootElement);
expect(results[2].elementDescription).toBe(results[1].elementDescription);
});
it('should clean out the inheritedElementBinder', () => {
var rootElement = el('<div><span *foo></span></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedElementBinder).toBe(null);
});
it('should create a nestedProtoView', () => {
var rootElement = el('<div><span *foo></span></div>');
var results = createPipeline().process(rootElement);
expect(results[2].inheritedProtoView).not.toBe(null);
expect(results[2].inheritedProtoView).toBe(results[1].inheritedElementBinder.nestedProtoView);
expect(DOM.getOuterHTML(results[2].inheritedProtoView.rootElement)).toEqual('<span *foo=""></span>');
});
});
});
}

View File

@ -0,0 +1,155 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
elementText,
expect,
iit,
inject,
it,
xit,
SpyObject,
} from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ProtoView, Template, ViewContainerRef, EventDispatcher, DirectiveMetadata} from 'angular2/src/render/api';
import {IntegrationTestbed, LoggingEventDispatcher, FakeEvent} from './integration_testbed';
export function main() {
describe('DirectDomRenderer integration', () => {
var testbed, renderer, rootEl, rootProtoViewRef, eventPlugin, compile;
function createRenderer({urlData, viewCacheCapacity, shadowDomStrategy, templates}={}) {
testbed = new IntegrationTestbed({
urlData: urlData,
viewCacheCapacity: viewCacheCapacity,
shadowDomStrategy: shadowDomStrategy,
templates: templates
});
renderer = testbed.renderer;
rootEl = testbed.rootEl;
rootProtoViewRef = testbed.rootProtoViewRef;
eventPlugin = testbed.eventPlugin;
compile = (template, directives) => testbed.compile(template, directives);
}
it('should create root views while using the given elements in place', () => {
createRenderer();
var viewRefs = renderer.createView(rootProtoViewRef);
expect(viewRefs.length).toBe(1);
expect(viewRefs[0].delegate.rootNodes[0]).toEqual(rootEl);
});
it('should add a static component', inject([AsyncTestCompleter], (async) => {
createRenderer();
var template = new Template({
componentId: 'someComponent',
inline: 'hello',
directives: []
});
renderer.compile(template).then( (pv) => {
var mergedProtoViewRefs = renderer.mergeChildComponentProtoViews(rootProtoViewRef, [pv.render]);
renderer.createView(mergedProtoViewRefs[0]);
expect(rootEl).toHaveText('hello');
async.done();
});
}));
it('should add a a dynamic component', inject([AsyncTestCompleter], (async) => {
createRenderer();
var template = new Template({
componentId: 'someComponent',
inline: 'hello',
directives: []
});
renderer.compile(template).then( (pv) => {
var rootViewRef = renderer.createView(rootProtoViewRef)[0];
var childComponentViewRef = renderer.createView(pv.render)[0];
renderer.setDynamicComponentView(rootViewRef, 0, childComponentViewRef);
expect(rootEl).toHaveText('hello');
async.done();
});
}));
it('should update text nodes', inject([AsyncTestCompleter], (async) => {
createRenderer();
compile('{{a}}', [someComponent]).then( (pvRefs) => {
var viewRefs = renderer.createView(pvRefs[0]);
renderer.setText(viewRefs[1], 0, 'hello');
expect(rootEl).toHaveText('hello');
async.done();
});
}));
it('should update element properties', inject([AsyncTestCompleter], (async) => {
createRenderer();
compile('<input [value]="someProp">', []).then( (pvRefs) => {
var viewRefs = renderer.createView(pvRefs[0]);
renderer.setElementProperty(viewRefs[1], 0, 'value', 'hello');
expect(DOM.childNodes(rootEl)[0].value).toEqual('hello');
async.done();
});
}));
it('should add and remove views to and from containers', inject([AsyncTestCompleter], (async) => {
createRenderer();
compile('<template>hello</template>', []).then( (pvRefs) => {
var viewRef = renderer.createView(pvRefs[0])[1];
var vcProtoViewRef = pvRefs[2];
var vcRef = new ViewContainerRef(viewRef, 0);
var childViewRef = renderer.createView(vcProtoViewRef)[0];
expect(rootEl).toHaveText('');
renderer.insertViewIntoContainer(vcRef, childViewRef);
expect(rootEl).toHaveText('hello');
renderer.detachViewFromContainer(vcRef, 0);
expect(rootEl).toHaveText('');
async.done();
});
}));
it('should cache views', inject([AsyncTestCompleter], (async) => {
createRenderer({
viewCacheCapacity: 2
});
compile('<template>hello</template>', []).then( (pvRefs) => {
var vcProtoViewRef = pvRefs[2];
var viewRef1 = renderer.createView(vcProtoViewRef)[0];
renderer.destroyView(viewRef1);
var viewRef2 = renderer.createView(vcProtoViewRef)[0];
var viewRef3 = renderer.createView(vcProtoViewRef)[0];
expect(viewRef2.delegate).toBe(viewRef1.delegate);
expect(viewRef3.delegate).not.toBe(viewRef1.delegate);
async.done();
});
}));
it('should handle events', inject([AsyncTestCompleter], (async) => {
createRenderer();
compile('<input (change)="$event.target.value">', []).then( (pvRefs) => {
var viewRef = renderer.createView(pvRefs[0])[1];
var dispatcher = new LoggingEventDispatcher();
renderer.setEventDispatcher(viewRef, dispatcher);
var inputEl = DOM.childNodes(rootEl)[0];
inputEl.value = 'hello';
eventPlugin.dispatchEvent('change', new FakeEvent(inputEl));
expect(dispatcher.log).toEqual([[0, 'change', ['hello']]]);
async.done();
});
}));
});
}
var someComponent = new DirectiveMetadata({
id: 'someComponent',
type: DirectiveMetadata.COMPONENT_TYPE,
selector: 'some-comp'
});

View File

@ -0,0 +1,223 @@
import {
el
} from 'angular2/test_lib';
import {isBlank, isPresent, BaseException} from 'angular2/src/facade/lang';
import {MapWrapper, ListWrapper, List} from 'angular2/src/facade/collection';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Parser, Lexer} from 'angular2/change_detection';
import {DirectDomRenderer} from 'angular2/src/render/dom/direct_dom_renderer';
import {Compiler} from 'angular2/src/render/dom/compiler/compiler';
import {ProtoViewRef, ProtoView, Template, ViewContainerRef, EventDispatcher, DirectiveMetadata} from 'angular2/src/render/api';
import {DefaultStepFactory} from 'angular2/src/render/dom/compiler/compile_step_factory';
import {TemplateLoader} from 'angular2/src/render/dom/compiler/template_loader';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {EmulatedUnscopedShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy';
import {EventManager, EventManagerPlugin} from 'angular2/src/render/dom/events/event_manager';
import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone';
import {StyleUrlResolver} from 'angular2/src/render/dom/shadow_dom/style_url_resolver';
import {ViewFactory} from 'angular2/src/render/dom/view/view_factory';
export class IntegrationTestbed {
renderer;
parser;
rootEl;
rootProtoViewRef;
eventPlugin;
_templates:Map<string, Template>;
_compileCache:Map<string, Promise<List>>;
constructor({urlData, viewCacheCapacity, shadowDomStrategy, templates}) {
this._templates = MapWrapper.create();
if (isPresent(templates)) {
ListWrapper.forEach(templates, (template) => {
MapWrapper.set(this._templates, template.componentId, template);
});
}
this._compileCache = MapWrapper.create();
var parser = new Parser(new Lexer());
var urlResolver = new UrlResolver();
if (isBlank(shadowDomStrategy)) {
shadowDomStrategy = new EmulatedUnscopedShadowDomStrategy(new StyleUrlResolver(urlResolver), null);
}
var compiler = new Compiler(new DefaultStepFactory(parser, shadowDomStrategy), new FakeTemplateLoader(urlResolver, urlData));
if (isBlank(viewCacheCapacity)) {
viewCacheCapacity = 1;
}
if (isBlank(urlData)) {
urlData = MapWrapper.create();
}
this.eventPlugin = new FakeEventManagerPlugin();
var eventManager = new EventManager([this.eventPlugin], new FakeVmTurnZone());
var viewFactory = new ViewFactory(viewCacheCapacity, eventManager, shadowDomStrategy);
this.renderer = new DirectDomRenderer(compiler, viewFactory, shadowDomStrategy);
this.rootEl = el('<div></div>');
this.rootProtoViewRef = this.renderer.createRootProtoView(this.rootEl);
}
compile(templateHtml, directives):Promise<List<ProtoViewRef>> {
return this._compileRecurse(new Template({
componentId: 'root',
inline: templateHtml,
directives: directives
})).then( (protoViewRefs) => {
return this._flattenList([
this.renderer.mergeChildComponentProtoViews(this.rootProtoViewRef, [protoViewRefs[0]]),
protoViewRefs
]);
});
}
_compileRecurse(template):Promise<List<ProtoViewRef>> {
var result = MapWrapper.get(this._compileCache, template.componentId);
if (isPresent(result)) {
return result;
}
result = this.renderer.compile(template).then( (pv) => {
var childComponentPromises = ListWrapper.map(
this._findNestedComponentIds(template, pv),
(componentId) => {
var childTemplate = MapWrapper.get(this._templates, componentId);
if (isBlank(childTemplate)) {
throw new BaseException(`Could not find template for ${componentId}!`);
}
return this._compileRecurse(childTemplate);
}
);
return PromiseWrapper.all(childComponentPromises).then(
(protoViewRefsWithChildren) => {
var protoViewRefs =
ListWrapper.map(protoViewRefsWithChildren, (arr) => arr[0]);
return this._flattenList([
this.renderer.mergeChildComponentProtoViews(pv.render, protoViewRefs),
protoViewRefsWithChildren
]);
}
);
});
MapWrapper.set(this._compileCache, template.componentId, result);
return result;
}
_findNestedComponentIds(template, pv, target = null):List<string> {
if (isBlank(target)) {
target = [];
}
for (var binderIdx=0; binderIdx<pv.elementBinders.length; binderIdx++) {
var eb = pv.elementBinders[binderIdx];
var componentDirective;
ListWrapper.forEach(eb.directives, (db) => {
var meta = template.directives[db.directiveIndex];
if (meta.type === DirectiveMetadata.COMPONENT_TYPE) {
componentDirective = meta;
}
});
if (isPresent(componentDirective)) {
ListWrapper.push(target, componentDirective.id);
} else if (isPresent(eb.nestedProtoView)) {
this._findNestedComponentIds(template, eb.nestedProtoView, target);
}
}
return target;
}
_flattenList(tree:List, out:List = null):List {
if (isBlank(out)) {
out = [];
}
for (var i = 0; i < tree.length; i++) {
var item = tree[i];
if (ListWrapper.isList(item)) {
this._flattenList(item, out);
} else {
ListWrapper.push(out, item);
}
}
return out;
}
}
class FakeTemplateLoader extends TemplateLoader {
_urlData: Map<string, string>;
constructor(urlResolver, urlData) {
super(null, urlResolver);
this._urlData = urlData;
}
load(template: Template) {
if (isPresent(template.inline)) {
return PromiseWrapper.resolve(DOM.createTemplate(template.inline));
}
if (isPresent(template.absUrl)) {
var content = this._urlData[template.absUrl];
if (isPresent(content)) {
return PromiseWrapper.resolve(DOM.createTemplate(content));
}
}
return PromiseWrapper.reject('Load failed');
}
}
export class FakeVmTurnZone extends VmTurnZone {
constructor() {
super({enableLongStackTrace: false});
}
run(fn) {
fn();
}
runOutsideAngular(fn) {
fn();
}
}
export class FakeEventManagerPlugin extends EventManagerPlugin {
_eventHandlers: Map;
constructor() {
super();
this._eventHandlers = MapWrapper.create();
}
dispatchEvent(eventName, event) {
MapWrapper.get(this._eventHandlers, eventName)(event);
}
supports(eventName: string): boolean {
return true;
}
addEventListener(element, eventName: string, handler: Function, shouldSupportBubble: boolean) {
MapWrapper.set(this._eventHandlers, eventName, handler);
}
}
export class LoggingEventDispatcher extends EventDispatcher {
log:List;
constructor() {
super();
this.log = [];
}
dispatchEvent(
elementIndex:number, eventName:string, locals:List<any>
) {
ListWrapper.push(this.log, [elementIndex, eventName, locals]);
}
}
export class FakeEvent {
target;
constructor(target) {
this.target = target;
}
}

View File

@ -0,0 +1,50 @@
import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el, proxy} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Content} from 'angular2/src/render/dom/shadow_dom/content_tag';
import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom';
@proxy
@IMPLEMENTS(LightDom)
class DummyLightDom extends SpyObject {noSuchMethod(m){super.noSuchMethod(m)}}
var _scriptStart = `<script start=""></script>`;
var _scriptEnd = `<script end=""></script>`;
export function main() {
describe('Content', function() {
var parent;
var content;
beforeEach(() => {
parent = el(`<div>${_scriptStart}${_scriptEnd}`);
content = DOM.firstChild(parent);
});
it("should insert the nodes", () => {
var c = new Content(content, '');
c.hydrate(null);
c.insert([el("<a></a>"), el("<b></b>")])
expect(DOM.getInnerHTML(parent)).toEqual(`${_scriptStart}<a></a><b></b>${_scriptEnd}`);
});
it("should remove the nodes from the previous insertion", () => {
var c = new Content(content, '');
c.hydrate(null);
c.insert([el("<a></a>")]);
c.insert([el("<b></b>")]);
expect(DOM.getInnerHTML(parent)).toEqual(`${_scriptStart}<b></b>${_scriptEnd}`);
});
it("should insert empty list", () => {
var c = new Content(content, '');
c.hydrate(null);
c.insert([el("<a></a>")]);
c.insert([]);
expect(DOM.getInnerHTML(parent)).toEqual(`${_scriptStart}${_scriptEnd}`);
});
});
}

View File

@ -0,0 +1,151 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
SpyObject,
} from 'angular2/test_lib';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Map, MapWrapper} from 'angular2/src/facade/collection';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {XHR} from 'angular2/src/services/xhr';
import {
EmulatedScopedShadowDomStrategy,
} from 'angular2/src/render/dom/shadow_dom/emulated_scoped_shadow_dom_strategy';
import {
resetShadowDomCache,
} from 'angular2/src/render/dom/shadow_dom/util';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {StyleUrlResolver} from 'angular2/src/render/dom/shadow_dom/style_url_resolver';
import {StyleInliner} from 'angular2/src/render/dom/shadow_dom/style_inliner';
import {View} from 'angular2/src/render/dom/view/view';
export function main() {
describe('EmulatedScoped', () => {
var xhr, styleHost, strategy;
beforeEach(() => {
var urlResolver = new UrlResolver();
var styleUrlResolver = new StyleUrlResolver(urlResolver);
xhr = new FakeXHR();
var styleInliner = new StyleInliner(xhr, styleUrlResolver, urlResolver);
styleHost = el('<div></div>');
strategy = new EmulatedScopedShadowDomStrategy(styleInliner, styleUrlResolver, styleHost);
resetShadowDomCache();
});
it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>');
var view = new View(null, [nodes], [], [], [], []);
strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host);
expect(DOM.tagName(firstChild).toLowerCase()).toEqual('div');
expect(firstChild).toHaveText('view');
expect(host).toHaveText('view');
});
it('should rewrite style urls', () => {
var styleElement = el('<style>.foo {background-image: url("img.jpg");}</style>');
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(styleElement).toHaveText(".foo[_ngcontent-0] {\n" +
"background-image: url(http://base/img.jpg);\n" +
"}");
});
it('should scope styles', () => {
var styleElement = el('<style>.foo {} :host {}</style>');
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(styleElement).toHaveText(".foo[_ngcontent-0] {\n\n}\n\n[_nghost-0] {\n\n}");
});
it('should inline @import rules', inject([AsyncTestCompleter], (async) => {
xhr.reply('http://base/one.css', '.one {}');
var styleElement = el('<style>@import "one.css";</style>');
var stylePromise = strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(stylePromise).toBePromise();
expect(styleElement).toHaveText('');
stylePromise.then((_) => {
expect(styleElement).toHaveText('.one[_ngcontent-0] {\n\n}');
async.done();
});
}));
it('should return the same style given the same component', () => {
var styleElement = el('<style>.foo {} :host {}</style>');
strategy.processStyleElement('someComponent', 'http://base', styleElement);
var styleElement2 = el('<style>.foo {} :host {}</style>');
strategy.processStyleElement('someComponent', 'http://base', styleElement2);
expect(DOM.getText(styleElement)).toEqual(DOM.getText(styleElement2));
});
it('should return different styles given different components', () => {
var styleElement = el('<style>.foo {} :host {}</style>');
strategy.processStyleElement('someComponent1', 'http://base', styleElement);
var styleElement2 = el('<style>.foo {} :host {}</style>');
strategy.processStyleElement('someComponent2', 'http://base', styleElement2);
expect(DOM.getText(styleElement)).not.toEqual(DOM.getText(styleElement2));
});
it('should move the style element to the style host', () => {
var compileElement = el('<div><style>.one {}</style></div>');
var styleElement = DOM.firstChild(compileElement);
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(compileElement).toHaveText('');
expect(styleHost).toHaveText('.one[_ngcontent-0] {\n\n}');
});
it('should add an attribute to component elements', () => {
var element = el('<div></div>');
strategy.processElement(null, 'elComponent', element);
expect(DOM.getAttribute(element, '_nghost-0')).toEqual('');
});
it('should add an attribute to the content elements', () => {
var element = el('<div></div>');
strategy.processElement('hostComponent', null, element);
expect(DOM.getAttribute(element, '_ngcontent-0')).toEqual('');
});
});
}
class FakeXHR extends XHR {
_responses: Map;
constructor() {
super();
this._responses = MapWrapper.create();
}
get(url: string): Promise<string> {
var response = MapWrapper.get(this._responses, url);
if (isBlank(response)) {
return PromiseWrapper.reject('xhr error');
}
return PromiseWrapper.resolve(response);
}
reply(url: string, response: string) {
MapWrapper.set(this._responses, url, response);
}
}

View File

@ -0,0 +1,92 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
SpyObject,
} from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Map, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {
EmulatedUnscopedShadowDomStrategy,
} from 'angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy';
import {
resetShadowDomCache,
} from 'angular2/src/render/dom/shadow_dom/util';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {StyleUrlResolver} from 'angular2/src/render/dom/shadow_dom/style_url_resolver';
import {View} from 'angular2/src/render/dom/view/view';
export function main() {
var strategy;
describe('EmulatedUnscoped', () => {
var styleHost;
beforeEach(() => {
var urlResolver = new UrlResolver();
var styleUrlResolver = new StyleUrlResolver(urlResolver);
styleHost = el('<div></div>');
strategy = new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, styleHost);
resetShadowDomCache();
});
it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>');
var view = new View(null, [nodes], [], [], [], []);
strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host);
expect(DOM.tagName(firstChild).toLowerCase()).toEqual('div');
expect(firstChild).toHaveText('view');
expect(host).toHaveText('view');
});
it('should rewrite style urls', () => {
var styleElement = el('<style>.foo {background-image: url("img.jpg");}</style>');
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(styleElement).toHaveText(".foo {" +
"background-image: url('http://base/img.jpg');" +
"}");
});
it('should not inline import rules', () => {
var styleElement = el('<style>@import "other.css";</style>')
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(styleElement).toHaveText("@import 'http://base/other.css';");
});
it('should move the style element to the style host', () => {
var compileElement = el('<div><style>.one {}</style></div>');
var styleElement = DOM.firstChild(compileElement);
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(compileElement).toHaveText('');
expect(styleHost).toHaveText('.one {}');
});
it('should insert the same style only once in the style host', () => {
var styleEls = [
el('<style>/*css1*/</style>'),
el('<style>/*css2*/</style>'),
el('<style>/*css1*/</style>')
];
ListWrapper.forEach(styleEls, (styleEl) => {
strategy.processStyleElement('someComponent', 'http://base', styleEl);
});
expect(styleHost).toHaveText("/*css1*//*css2*/");
});
});
}

View File

@ -0,0 +1,219 @@
import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el, proxy} from 'angular2/test_lib';
import {IMPLEMENTS, isBlank, isPresent} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Content} from 'angular2/src/render/dom/shadow_dom/content_tag';
import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom';
import {View} from 'angular2/src/render/dom/view/view';
import {ViewContainer} from 'angular2/src/render/dom/view/view_container';
@proxy
@IMPLEMENTS(View)
class FakeView {
contentTags;
viewContainers;
constructor(containers = null) {
this.contentTags = [];
this.viewContainers = [];
if (isPresent(containers)) {
ListWrapper.forEach(containers, (c) => {
if (c instanceof FakeContentTag) {
ListWrapper.push(this.contentTags, c);
} else {
ListWrapper.push(this.contentTags, null);
}
if (c instanceof FakeViewContainer) {
ListWrapper.push(this.viewContainers, c);
} else {
ListWrapper.push(this.viewContainers, null);
}
});
}
}
noSuchMethod(i) {
super.noSuchMethod(i);
}
}
@proxy
@IMPLEMENTS(ViewContainer)
class FakeViewContainer {
templateElement;
_nodes;
_contentTagContainers;
constructor(templateEl, nodes = null, views = null) {
this.templateElement = templateEl;
this._nodes = nodes;
this._contentTagContainers = views;
}
nodes(){
return this._nodes;
}
contentTagContainers(){
return this._contentTagContainers;
}
noSuchMethod(i) {
super.noSuchMethod(i);
}
}
@proxy
@IMPLEMENTS(Content)
class FakeContentTag {
select;
_nodes;
contentStartElement;
constructor(contentEl, select = '', nodes = null) {
this.contentStartElement = contentEl;
this.select = select;
this._nodes = nodes;
}
insert(nodes){
this._nodes = ListWrapper.clone(nodes);
}
nodes() {
return this._nodes;
}
noSuchMethod(i) {
super.noSuchMethod(i);
}
}
export function main() {
describe('LightDom', function() {
var lightDomView;
beforeEach(() => {
lightDomView = new FakeView();
});
describe("contentTags", () => {
it("should collect content tags from element injectors", () => {
var tag = new FakeContentTag(el('<script></script>'));
var shadowDomView = new FakeView([tag]);
var lightDom = new LightDom(lightDomView, shadowDomView,
el("<div></div>"));
expect(lightDom.contentTags()).toEqual([tag]);
});
it("should collect content tags from ViewContainers", () => {
var tag = new FakeContentTag(el('<script></script>'));
var vc = new FakeViewContainer(null, null, [
new FakeView([tag])
]);
var shadowDomView = new FakeView([vc]);
var lightDom = new LightDom(lightDomView, shadowDomView,
el("<div></div>"));
expect(lightDom.contentTags()).toEqual([tag]);
});
});
describe("expandedDomNodes", () => {
it("should contain root nodes", () => {
var lightDomEl = el("<div><a></a></div>")
var lightDom = new LightDom(lightDomView, new FakeView(), lightDomEl);
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
});
it("should include view container nodes", () => {
var lightDomEl = el("<div><template></template></div>");
var lightDom = new LightDom(
new FakeView([
new FakeViewContainer(
DOM.firstChild(lightDomEl), // template element
[el('<a></a>')] // light DOM nodes of view container
)
]),
null,
lightDomEl);
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
});
it("should include content nodes", () => {
var lightDomEl = el("<div><content></content></div>");
var lightDom = new LightDom(
new FakeView([
new FakeContentTag(
DOM.firstChild(lightDomEl), // content element
'', // selector
[el('<a></a>')] // light DOM nodes of content tag
)
]),
null,
lightDomEl);
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
});
it("should work when the element injector array contains nulls", () => {
var lightDomEl = el("<div><a></a></div>")
var lightDomView = new FakeView();
var lightDom = new LightDom(
lightDomView,
new FakeView(),
lightDomEl);
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
});
});
describe("redistribute", () => {
it("should redistribute nodes between content tags with select property set", () => {
var contentA = new FakeContentTag(null, "a");
var contentB = new FakeContentTag(null, "b");
var lightDomEl = el("<div><a>1</a><b>2</b><a>3</a></div>")
var lightDom = new LightDom(lightDomView, new FakeView([
contentA,
contentB
]), lightDomEl);
lightDom.redistribute();
expect(toHtml(contentA.nodes())).toEqual(["<a>1</a>", "<a>3</a>"]);
expect(toHtml(contentB.nodes())).toEqual(["<b>2</b>"]);
});
it("should support wildcard content tags", () => {
var wildcard = new FakeContentTag(null, '');
var contentB = new FakeContentTag(null, "b");
var lightDomEl = el("<div><a>1</a><b>2</b><a>3</a></div>")
var lightDom = new LightDom(lightDomView, new FakeView([
wildcard,
contentB
]), lightDomEl);
lightDom.redistribute();
expect(toHtml(wildcard.nodes())).toEqual(["<a>1</a>", "<b>2</b>", "<a>3</a>"]);
expect(toHtml(contentB.nodes())).toEqual([]);
});
});
});
}
function toHtml(nodes) {
if (isBlank(nodes)) return [];
return ListWrapper.map(nodes, DOM.getOuterHTML);
}

View File

@ -0,0 +1,60 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
SpyObject,
} from 'angular2/test_lib';
import {
NativeShadowDomStrategy
} from 'angular2/src/render/dom/shadow_dom/native_shadow_dom_strategy';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {StyleUrlResolver} from 'angular2/src/render/dom/shadow_dom/style_url_resolver';
import {View} from 'angular2/src/render/dom/view/view';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
var strategy;
describe('NativeShadowDomStrategy', () => {
beforeEach(() => {
var urlResolver = new UrlResolver();
var styleUrlResolver = new StyleUrlResolver(urlResolver);
strategy = new NativeShadowDomStrategy(styleUrlResolver);
});
it('should attach the view nodes to the shadow root', () => {
var host = el('<div><span>original content</span></div>');
var nodes = el('<div>view</div>');
var view = new View(null, [nodes], [], [], [], []);
strategy.attachTemplate(host, view);
var shadowRoot = DOM.getShadowRoot(host);
expect(isPresent(shadowRoot)).toBeTruthy();
expect(shadowRoot).toHaveText('view');
});
it('should rewrite style urls', () => {
var styleElement = el('<style>.foo {background-image: url("img.jpg");}</style>');
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(styleElement).toHaveText(".foo {" +
"background-image: url('http://base/img.jpg');" +
"}");
});
it('should not inline import rules', () => {
var styleElement = el('<style>@import "other.css";</style>')
strategy.processStyleElement('someComponent', 'http://base', styleElement);
expect(styleElement).toHaveText("@import 'http://base/other.css';");
});
});
}

View File

@ -0,0 +1,117 @@
import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el} from 'angular2/test_lib';
import {ShadowCss} from 'angular2/src/render/dom/shadow_dom/shadow_css';
import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
export function main() {
describe('ShadowCss', function() {
function s(css: string, contentAttr:string, hostAttr:string = '') {
var shadowCss = new ShadowCss();
var shim = shadowCss.shimCssText(css, contentAttr, hostAttr);
var nlRegexp = RegExpWrapper.create('\\n');
return StringWrapper.replaceAll(shim, nlRegexp, '');
}
it('should handle empty string', () => {
expect(s('', 'a')).toEqual('');
});
it('should add an attribute to every rule', () => {
var css = 'one {color: red;}two {color: red;}';
var expected = 'one[a] {color: red;}two[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should hanlde invalid css', () => {
var css = 'one {color: red;}garbage';
var expected = 'one[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should add an attribute to every selector', () => {
var css = 'one, two {color: red;}';
var expected = 'one[a], two[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules', () => {
var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}}';
var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules with simple rules', () => {
var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}} div {}';
var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}div[a] {}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle complicated selectors', () => {
expect(s('one::before {}', 'a')).toEqual('one[a]::before {}');
expect(s('one two {}', 'a')).toEqual('one[a] two[a] {}');
expect(s('one>two {}', 'a')).toEqual('one[a] > two[a] {}');
expect(s('one+two {}', 'a')).toEqual('one[a] + two[a] {}');
expect(s('one~two {}', 'a')).toEqual('one[a] ~ two[a] {}');
expect(s('.one.two > three {}', 'a')).toEqual('.one.two[a] > three[a] {}');
expect(s('one[attr="value"] {}', 'a')).toEqual('one[attr="value"][a] {}');
expect(s('one[attr=value] {}', 'a')).toEqual('one[attr="value"][a] {}');
expect(s('one[attr^="value"] {}', 'a')).toEqual('one[attr^="value"][a] {}');
expect(s('one[attr$="value"] {}', 'a')).toEqual('one[attr$="value"][a] {}');
expect(s('one[attr*="value"] {}', 'a')).toEqual('one[attr*="value"][a] {}');
expect(s('one[attr|="value"] {}', 'a')).toEqual('one[attr|="value"][a] {}');
expect(s('one[attr] {}', 'a')).toEqual('one[attr][a] {}');
expect(s('[is="one"] {}', 'a')).toEqual('[is="one"][a] {}');
});
it('should handle :host', () => {
expect(s(':host {}', 'a', 'a-host')).toEqual('[a-host] {}');
expect(s(':host(.x,.y) {}', 'a', 'a-host')).toEqual('[a-host].x, [a-host].y {}');
expect(s(':host(.x,.y) > .z {}', 'a', 'a-host')).toEqual('[a-host].x > .z, [a-host].y > .z {}');
});
it('should handle :host-context', () => {
expect(s(':host-context(.x) {}', 'a', 'a-host')).toEqual('[a-host].x, .x [a-host] {}');
expect(s(':host-context(.x) > .y {}', 'a', 'a-host')).toEqual('[a-host].x > .y, .x [a-host] > .y {}');
});
it('should support polyfill-next-selector', () => {
var css = s("polyfill-next-selector {content: 'x > y'} z {}", 'a');
expect(css).toEqual('x[a] > y[a] {}');
css = s('polyfill-next-selector {content: "x > y"} z {}', 'a');
expect(css).toEqual('x[a] > y[a] {}');
});
it('should support polyfill-unscoped-rule', () => {
var css = s("polyfill-unscoped-rule {content: '#menu > .bar';background: blue;}", 'a');
expect(StringWrapper.contains(css, '#menu > .bar {;background: blue;}')).toBeTruthy();
css = s('polyfill-unscoped-rule {content: "#menu > .bar";background: blue;}', 'a');
expect(StringWrapper.contains(css, '#menu > .bar {;background: blue;}')).toBeTruthy();
});
it('should support polyfill-rule', () => {
var css = s("polyfill-rule {content: ':host.foo .bar';background: blue;}", 'a', 'a-host');
expect(css).toEqual('[a-host].foo .bar {background: blue;}');
css = s('polyfill-rule {content: ":host.foo .bar";background: blue;}', 'a', 'a-host');
expect(css).toEqual('[a-host].foo .bar {background: blue;}');
});
it('should handle ::shadow', () => {
var css = s('x::shadow > y {}', 'a');
expect(css).toEqual('x[a] > y[a] {}');
});
it('should handle /deep/', () => {
var css = s('x /deep/ y {}', 'a');
expect(css).toEqual('x[a] y[a] {}');
});
it('should handle >>>', () => {
var css = s('x >>> y {}', 'a');
expect(css).toEqual('x[a] y[a] {}');
});
});
}

View File

@ -0,0 +1,339 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
elementText,
expect,
iit,
inject,
it,
xit,
SpyObject,
} from 'angular2/test_lib';
import {MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
ProtoView, Template, ViewContainerRef, DirectiveMetadata
} from 'angular2/src/render/api';
import {EmulatedScopedShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/emulated_scoped_shadow_dom_strategy';
import {EmulatedUnscopedShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy';
import {NativeShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/native_shadow_dom_strategy';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {StyleUrlResolver} from 'angular2/src/render/dom/shadow_dom/style_url_resolver';
import {StyleInliner} from 'angular2/src/render/dom/shadow_dom/style_inliner';
import {IntegrationTestbed} from './integration_testbed';
export function main() {
describe('ShadowDom integration tests', function() {
var urlResolver, styleUrlResolver, styleInliner, styleHost;
var strategies = {
"scoped" : () => new EmulatedScopedShadowDomStrategy(styleInliner, styleUrlResolver, styleHost),
"unscoped" : () => new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, styleHost)
}
if (DOM.supportsNativeShadowDOM()) {
StringMapWrapper.set(strategies, "native", () => new NativeShadowDomStrategy(styleUrlResolver));
}
StringMapWrapper.forEach(strategies,
(strategyFactory, name) => {
describe(`${name} shadow dom strategy`, () => {
var testbed, renderer, rootEl, compile, strategy;
beforeEach( () => {
styleHost = el('<div></div>');
urlResolver = new UrlResolver();
styleUrlResolver = new StyleUrlResolver(urlResolver);
styleInliner = new StyleInliner(null, styleUrlResolver, urlResolver);
strategy = strategyFactory();
testbed = new IntegrationTestbed({
shadowDomStrategy: strategy,
templates: templates
});
renderer = testbed.renderer;
rootEl = testbed.rootEl;
compile = (template, directives) => testbed.compile(template, directives);
});
it('should support simple components', inject([AsyncTestCompleter], (async) => {
var temp = '<simple>' +
'<div>A</div>' +
'</simple>';
compile(temp, [simple]).then( (pvRefs) => {
renderer.createView(pvRefs[0]);
expect(rootEl).toHaveText('SIMPLE(A)');
async.done();
});
}));
it('should support multiple content tags', inject([AsyncTestCompleter], (async) => {
var temp = '<multiple-content-tags>' +
'<div>B</div>' +
'<div>C</div>' +
'<div class="left">A</div>' +
'</multiple-content-tags>';
compile(temp, [multipleContentTagsComponent]).then( (pvRefs) => {
renderer.createView(pvRefs[0]);
expect(rootEl).toHaveText('(A, BC)');
async.done();
});
}));
it('should redistribute only direct children', inject([AsyncTestCompleter], (async) => {
var temp = '<multiple-content-tags>' +
'<div>B<div class="left">A</div></div>' +
'<div>C</div>' +
'</multiple-content-tags>';
compile(temp, [multipleContentTagsComponent]).then( (pvRefs) => {
renderer.createView(pvRefs[0]);
expect(rootEl).toHaveText('(, BAC)');
async.done();
});
}));
it("should redistribute direct child viewcontainers when the light dom changes", inject([AsyncTestCompleter], (async) => {
var temp = '<multiple-content-tags>' +
'<div><div template="manual" class="left">A</div></div>' +
'<div>B</div>' +
'</multiple-content-tags>';
compile(temp, [multipleContentTagsComponent, manualViewportDirective]).then( (pvRefs) => {
var viewRefs = renderer.createView(pvRefs[0]);
var vcRef = new ViewContainerRef(viewRefs[1], 1);
var vcProtoViewRef = pvRefs[2];
var childViewRef = renderer.createView(vcProtoViewRef)[0];
expect(rootEl).toHaveText('(, B)');
renderer.insertViewIntoContainer(vcRef, childViewRef);
expect(rootEl).toHaveText('(, AB)');
renderer.detachViewFromContainer(vcRef, 0);
expect(rootEl).toHaveText('(, B)');
async.done();
});
}));
it("should redistribute when the light dom changes", inject([AsyncTestCompleter], (async) => {
var temp = '<multiple-content-tags>' +
'<div template="manual" class="left">A</div>' +
'<div>B</div>' +
'</multiple-content-tags>';
compile(temp, [multipleContentTagsComponent, manualViewportDirective]).then( (pvRefs) => {
var viewRefs = renderer.createView(pvRefs[0]);
var vcRef = new ViewContainerRef(viewRefs[1], 1);
var vcProtoViewRef = pvRefs[2];
var childViewRef = renderer.createView(vcProtoViewRef)[0];
expect(rootEl).toHaveText('(, B)');
renderer.insertViewIntoContainer(vcRef, childViewRef);
expect(rootEl).toHaveText('(A, B)');
renderer.detachViewFromContainer(vcRef, 0);
expect(rootEl).toHaveText('(, B)');
async.done();
});
}));
it("should support nested components", inject([AsyncTestCompleter], (async) => {
var temp = '<outer-with-indirect-nested>' +
'<div>A</div>' +
'<div>B</div>' +
'</outer-with-indirect-nested>';
compile(temp, [outerWithIndirectNestedComponent]).then( (pvRefs) => {
renderer.createView(pvRefs[0]);
expect(rootEl).toHaveText('OUTER(SIMPLE(AB))');
async.done();
});
}));
it("should support nesting with content being direct child of a nested component", inject([AsyncTestCompleter], (async) => {
var temp = '<outer>' +
'<div template="manual" class="left">A</div>' +
'<div>B</div>' +
'<div>C</div>' +
'</outer>';
compile(temp, [outerComponent, manualViewportDirective]).then( (pvRefs) => {
var viewRefs = renderer.createView(pvRefs[0]);
var vcRef = new ViewContainerRef(viewRefs[1], 1);
var vcProtoViewRef = pvRefs[2];
var childViewRef = renderer.createView(vcProtoViewRef)[0];
expect(rootEl).toHaveText('OUTER(INNER(INNERINNER(,BC)))');
renderer.insertViewIntoContainer(vcRef, childViewRef);
expect(rootEl).toHaveText('OUTER(INNER(INNERINNER(A,BC)))');
async.done();
});
}));
it('should redistribute when the shadow dom changes', inject([AsyncTestCompleter], (async) => {
var temp = '<conditional-content>' +
'<div class="left">A</div>' +
'<div>B</div>' +
'<div>C</div>' +
'</conditional-content>';
compile(temp, [conditionalContentComponent, autoViewportDirective]).then( (pvRefs) => {
var viewRefs = renderer.createView(pvRefs[0]);
var vcRef = new ViewContainerRef(viewRefs[2], 0);
var vcProtoViewRef = pvRefs[3];
var childViewRef = renderer.createView(vcProtoViewRef)[0];
expect(rootEl).toHaveText('(, ABC)');
renderer.insertViewIntoContainer(vcRef, childViewRef);
expect(rootEl).toHaveText('(A, BC)');
renderer.detachViewFromContainer(vcRef, 0);
expect(rootEl).toHaveText('(, ABC)');
async.done();
});
}));
//Implement once NgElement support changing a class
//it("should redistribute when a class has been added or removed");
//it('should not lose focus', () => {
// var temp = `<simple>aaa<input type="text" id="focused-input" ng-class="{'aClass' : showClass}"> bbb</simple>`;
//
// compile(temp, (view, lc) => {
// var input = view.nodes[1];
// input.focus();
//
// expect(document.activeElement.id).toEqual("focused-input");
//
// // update class of input
//
// expect(document.activeElement.id).toEqual("focused-input");
// });
//});
});
});
});
}
var simple = new DirectiveMetadata({
selector: 'simple',
id: 'simple',
type: DirectiveMetadata.COMPONENT_TYPE
});
var multipleContentTagsComponent = new DirectiveMetadata({
selector: 'multiple-content-tags',
id: 'multiple-content-tags',
type: DirectiveMetadata.COMPONENT_TYPE
});
var manualViewportDirective = new DirectiveMetadata({
selector: '[manual]',
id: 'manual',
type: DirectiveMetadata.VIEWPORT_TYPE
});
var outerWithIndirectNestedComponent = new DirectiveMetadata({
selector: 'outer-with-indirect-nested',
id: 'outer-with-indirect-nested',
type: DirectiveMetadata.COMPONENT_TYPE
});
var outerComponent = new DirectiveMetadata({
selector: 'outer',
id: 'outer',
type: DirectiveMetadata.COMPONENT_TYPE
});
var innerComponent = new DirectiveMetadata({
selector: 'inner',
id: 'inner',
type: DirectiveMetadata.COMPONENT_TYPE
});
var innerInnerComponent = new DirectiveMetadata({
selector: 'innerinner',
id: 'innerinner',
type: DirectiveMetadata.COMPONENT_TYPE
});
var conditionalContentComponent = new DirectiveMetadata({
selector: 'conditional-content',
id: 'conditional-content',
type: DirectiveMetadata.COMPONENT_TYPE
});
var autoViewportDirective = new DirectiveMetadata({
selector: '[auto]',
id: '[auto]',
type: DirectiveMetadata.VIEWPORT_TYPE
});
var templates = [
new Template({
componentId: 'simple',
inline: 'SIMPLE(<content></content>)',
directives: []
}),
new Template({
componentId: 'multiple-content-tags',
inline: '(<content select=".left"></content>, <content></content>)',
directives: []
}),
new Template({
componentId: 'outer-with-indirect-nested',
inline: 'OUTER(<simple><div><content></content></div></simple>)',
directives: [simple]
}),
new Template({
componentId: 'outer',
inline: 'OUTER(<inner><content></content></inner>)',
directives: [innerComponent]
}),
new Template({
componentId: 'inner',
inline: 'INNER(<innerinner><content></content></innerinner>)',
directives: [innerInnerComponent]
}),
new Template({
componentId: 'innerinner',
inline: 'INNERINNER(<content select=".left"></content>,<content></content>)',
directives: []
}),
new Template({
componentId: 'conditional-content',
inline: '<div>(<div *auto="cond"><content select=".left"></content></div>, <content></content>)</div>',
directives: [autoViewportDirective]
})
];

View File

@ -0,0 +1,42 @@
import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/test_lib';
import {UrlResolver} from 'angular2/src/services/url_resolver';
export function main() {
describe('UrlResolver', () => {
var resolver = new UrlResolver();
it('should add a relative path to the base url', () => {
expect(resolver.resolve('http://www.foo.com', 'bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/', 'bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com', './bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/', './bar')).toEqual('http://www.foo.com/bar');
});
it('should replace the base path', () => {
expect(resolver.resolve('http://www.foo.com/baz', 'bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/baz', './bar')).toEqual('http://www.foo.com/bar');
});
it('should append to the base path', () => {
expect(resolver.resolve('http://www.foo.com/baz/', 'bar')).toEqual('http://www.foo.com/baz/bar');
expect(resolver.resolve('http://www.foo.com/baz/', './bar')).toEqual('http://www.foo.com/baz/bar');
});
it('should support ".." in the path', () => {
expect(resolver.resolve('http://www.foo.com/baz/', '../bar')).toEqual('http://www.foo.com/bar');
expect(resolver.resolve('http://www.foo.com/1/2/3/', '../../bar')).toEqual('http://www.foo.com/1/bar');
expect(resolver.resolve('http://www.foo.com/1/2/3/', '../biz/bar')).toEqual('http://www.foo.com/1/2/biz/bar');
expect(resolver.resolve('http://www.foo.com/1/2/baz', '../../bar')).toEqual('http://www.foo.com/bar');
});
it('should ignore the base path when the url has a scheme', () => {
expect(resolver.resolve('http://www.foo.com', 'http://www.bar.com')).toEqual('http://www.bar.com');
})
it('should throw when the url start with "/"', () => {
expect(() => {
resolver.resolve('http://www.foo.com/1/2', '/test');
}).toThrowError();
});
});
}