feat(render): add initial implementation of render layer
This commit is contained in:
parent
814d389b6e
commit
6c60c3e547
|
@ -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 {Lexer} from './src/change_detection/parser/lexer';
|
||||||
export {Parser} from './src/change_detection/parser/parser';
|
export {Parser} from './src/change_detection/parser/parser';
|
||||||
export {Locals}
|
export {Locals}
|
||||||
|
|
|
@ -446,6 +446,72 @@ export class AstVisitor {
|
||||||
visitPrefixNot(ast:PrefixNot) {}
|
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],
|
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,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 +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 {}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + '"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
67
modules/angular2/src/render/dom/shadow_dom/emulated_scoped_shadow_dom_strategy.js
vendored
Normal file
67
modules/angular2/src/render/dom/shadow_dom/emulated_scoped_shadow_dom_strategy.js
vendored
Normal 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, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
modules/angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy.js
vendored
Normal file
54
modules/angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy.js
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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*(.*)');
|
|
@ -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('[\'"]');
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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('^([^:/?#]+:)?');
|
|
@ -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;
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* Runs compiler tests using in-browser DOM adapter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {runCompilerCommonTests} from './compiler_common_tests';
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
runCompilerCommonTests();
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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()'
|
||||||
|
})
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
|
@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
|
@ -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'
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
151
modules/angular2/test/render/dom/shadow_dom/emulated_scoped_shadow_dom_strategy_spec.js
vendored
Normal file
151
modules/angular2/test/render/dom/shadow_dom/emulated_scoped_shadow_dom_strategy_spec.js
vendored
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
92
modules/angular2/test/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy_spec.js
vendored
Normal file
92
modules/angular2/test/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy_spec.js
vendored
Normal 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*/");
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
60
modules/angular2/test/render/dom/shadow_dom/native_shadow_dom_strategy_spec.js
vendored
Normal file
60
modules/angular2/test/render/dom/shadow_dom/native_shadow_dom_strategy_spec.js
vendored
Normal 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';");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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] {}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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]
|
||||||
|
})
|
||||||
|
];
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue