diff --git a/modules/angular2/change_detection.js b/modules/angular2/change_detection.js index a2beb5df26..f8555f9dd3 100644 --- a/modules/angular2/change_detection.js +++ b/modules/angular2/change_detection.js @@ -1,4 +1,6 @@ -export {AST} from './src/change_detection/parser/ast'; +export { + ASTWithSource, AST, AstTransformer, AccessMember, LiteralArray, ImplicitReceiver +} from './src/change_detection/parser/ast'; export {Lexer} from './src/change_detection/parser/lexer'; export {Parser} from './src/change_detection/parser/parser'; export {Locals} diff --git a/modules/angular2/src/change_detection/parser/ast.js b/modules/angular2/src/change_detection/parser/ast.js index db8cde892e..c2541cec32 100644 --- a/modules/angular2/src/change_detection/parser/ast.js +++ b/modules/angular2/src/change_detection/parser/ast.js @@ -446,6 +446,72 @@ export class AstVisitor { visitPrefixNot(ast:PrefixNot) {} } +export class AstTransformer { + visitImplicitReceiver(ast:ImplicitReceiver) { + return new ImplicitReceiver(); + } + + visitInterpolation(ast:Interpolation) { + return new Interpolation(ast.strings, this.visitAll(ast.expressions)); + } + + visitLiteralPrimitive(ast:LiteralPrimitive) { + return new LiteralPrimitive(ast.value); + } + + visitAccessMember(ast:AccessMember) { + return new AccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter); + } + + visitMethodCall(ast:MethodCall) { + return new MethodCall(ast.receiver.visit(this), ast.name, ast.fn, this.visitAll(ast.args)); + } + + visitFunctionCall(ast:FunctionCall) { + return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args)); + } + + visitLiteralArray(ast:LiteralArray) { + return new LiteralArray(this.visitAll(ast.expressions)); + } + + visitLiteralMap(ast:LiteralMap) { + return new LiteralMap(ast.keys, this.visitAll(ast.values)); + } + + visitBinary(ast:Binary) { + return new Binary(ast.operation, ast.left.visit(this), ast.right.visit(this)); + } + + visitPrefixNot(ast:PrefixNot) { + return new PrefixNot(ast.expression.visit(this)); + } + + visitConditional(ast:Conditional) { + return new Conditional( + ast.condition.visit(this), + ast.trueExp.visit(this), + ast.falseExp.visit(this) + ); + } + + visitPipe(ast:Pipe) { + return new Pipe(ast.exp.visit(this), ast.name, this.visitAll(ast.args), ast.inBinding); + } + + visitKeyedAccess(ast:KeyedAccess) { + return new KeyedAccess(ast.obj.visit(this), ast.key.visit(this)); + } + + visitAll(asts:List) { + var res = ListWrapper.createFixedSize(asts.length); + for (var i = 0; i < asts.length; ++i) { + res[i] = asts[i].visit(this); + } + return res; + } +} + var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0]]; diff --git a/modules/angular2/src/render/api.js b/modules/angular2/src/render/api.js new file mode 100644 index 0000000000..fdd1e3ded2 --- /dev/null +++ b/modules/angular2/src/render/api.js @@ -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; + nestedProtoView:ProtoView; + propertyBindings: Map; + variableBindings: Map; + // Note: this contains a preprocessed AST + // that replaced the values that should be extracted from the element + // with a local name + eventBindings: Map; + textBindings: List; + + 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; + // Note: this contains a preprocessed AST + // that replaced the values that should be extracted from the element + // with a local name + eventBindings: Map; + constructor({ + directiveIndex, propertyBindings, eventBindings + }) { + this.directiveIndex = directiveIndex; + this.propertyBindings = propertyBindings; + this.eventBindings = eventBindings; + } +} + +export class ProtoView { + render: ProtoViewRef; + elementBinders:List; + variableBindings: Map; + + 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; + bind:Map; + setters:List; + 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; + + 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 { return null; } + + /** + * Creates a new ProtoView with preset nested components, + * which will be instantiated when this protoView is instantiated. + * @param {List} protoViewRefs + * ProtoView for every element with a component in this protoView or in a view container's protoView + * @return {List} + * new ProtoViewRef for the given protoView and all of its view container's protoViews + */ + mergeChildComponentProtoViews(protoViewRef:ProtoViewRef, protoViewRefs:List):List { 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} depth first list of nested child components + */ + createView(protoView:ProtoViewRef):List { 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} locals Locals to be used to evaluate the + * event expressions + */ + dispatchEvent( + elementIndex:number, eventName:string, locals:List + ):void {} +} diff --git a/modules/angular2/src/render/dom/compiler/compile_control.js b/modules/angular2/src/render/dom/compiler/compile_control.js new file mode 100644 index 0000000000..89faa46e03 --- /dev/null +++ b/modules/angular2/src/render/dom/compiler/compile_control.js @@ -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; + _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' : 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 { + if (isBlank(this._attrs)) { + this._attrs = DOM.attributeMap(this.element); + } + return this._attrs; + } + + refreshClassList() { + this._classList = null; + } + + classList():List { + 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 + '"'); + } + } +} diff --git a/modules/angular2/src/render/dom/compiler/compile_pipeline.js b/modules/angular2/src/render/dom/compiler/compile_pipeline.js new file mode 100644 index 0000000000..dc2ad41719 --- /dev/null +++ b/modules/angular2/src/render/dom/compiler/compile_pipeline.js @@ -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) { + 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):List { + 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) { + 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) + ]; + } +} \ No newline at end of file diff --git a/modules/angular2/src/render/dom/compiler/compiler.js b/modules/angular2/src/render/dom/compiler/compiler.js new file mode 100644 index 0000000000..3362c4db12 --- /dev/null +++ b/modules/angular2/src/render/dom/compiler/compiler.js @@ -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 { + 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 { + 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); + } + } +} \ No newline at end of file diff --git a/modules/angular2/src/render/dom/compiler/directive_parser.js b/modules/angular2/src/render/dom/compiler/directive_parser.js new file mode 100644 index 0000000000..55712286ad --- /dev/null +++ b/modules/angular2/src/render/dom/compiler/directive_parser.js @@ -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 + *