From 7a70f8f92d5186da824fb7f85e724f8ff07214c9 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 11 Nov 2014 17:33:47 -0800 Subject: [PATCH] feat(compiler): initial version of the compiler. Supports: - binds text nodes, element properties and directive properties - locates decorator, component and template directives. - inline templates of components The compiler is built using a pipeline design, see core/src/compiler/pipeline package. Integration tests to show how the compiler, change_detection and DI work together: core/test/compiler/integration_spec.js --- .../src/compiler/selector_benchmark.js | 3 +- modules/change_detection/src/watch_group.js | 13 +- modules/core/src/annotations/component.js | 7 +- modules/core/src/annotations/decorator.js | 25 ++ modules/core/src/annotations/directive.js | 3 + modules/core/src/annotations/template.js | 25 ++ .../core/src/annotations/template_config.js | 3 + modules/core/src/compiler/annotated_type.js | 5 +- .../core/src/compiler/annotation_extractor.js | 16 - modules/core/src/compiler/compiler.js | 79 +++- modules/core/src/compiler/element_binder.js | 20 +- modules/core/src/compiler/element_injector.js | 3 +- .../src/compiler/pipeline/compile_control.js | 47 +++ .../src/compiler/pipeline/compile_element.js | 90 +++++ .../src/compiler/pipeline/compile_pipeline.js | 39 ++ .../src/compiler/pipeline/compile_step.js | 11 + .../src/compiler/pipeline/default_steps.js | 33 ++ .../src/compiler/pipeline/directive_parser.js | 66 ++++ .../pipeline/element_binder_builder.js | 123 ++++++ .../pipeline/element_binding_marker.js | 40 ++ .../pipeline/property_binding_parser.js | 31 ++ .../proto_element_injector_builder.js | 74 ++++ .../compiler/pipeline/proto_view_builder.js | 36 ++ .../pipeline/text_interpolation_parser.js | 54 +++ .../src/compiler/pipeline/view_splitter.js | 62 +++ modules/core/src/compiler/reflector.dart | 27 ++ modules/core/src/compiler/reflector.es6 | 26 ++ modules/core/src/compiler/selector.js | 238 ++++++------ modules/core/src/compiler/template_loader.js | 8 +- modules/core/src/compiler/view.js | 122 ++++-- modules/core/test/compiler/compiler_spec.js | 118 +++++- .../core/test/compiler/integration_spec.js | 97 +++++ .../pipeline/directive_parser_spec.js | 129 +++++++ .../pipeline/element_binder_builder_spec.js | 274 +++++++++++++ .../pipeline/element_binding_marker_spec.js | 105 +++++ .../test/compiler/pipeline/pipeline_spec.js | 165 ++++++++ .../pipeline/property_binding_parser_spec.js | 27 ++ .../proto_element_injector_builder_spec.js | 142 +++++++ .../pipeline/proto_view_builder_spec.js | 71 ++++ .../text_interpolation_parser_spec.js | 31 ++ .../compiler/pipeline/view_splitter_spec.js | 135 +++++++ modules/core/test/compiler/reflector_spec.js | 36 ++ modules/core/test/compiler/selector_spec.js | 42 +- modules/core/test/compiler/view_spec.js | 363 ++++++++++-------- modules/facade/src/collection.dart | 7 + modules/facade/src/collection.es6 | 20 +- modules/facade/src/dom.dart | 36 +- modules/facade/src/dom.es6 | 33 +- modules/facade/src/lang.dart | 21 + modules/facade/src/lang.es6 | 27 +- modules/test_lib/test/test_lib_spec.js | 4 + tools/transpiler/spec/annotations_spec.js | 23 +- .../outputgeneration/DartParseTreeWriter.js | 28 ++ 53 files changed, 2877 insertions(+), 386 deletions(-) create mode 100644 modules/core/src/annotations/decorator.js create mode 100644 modules/core/src/annotations/template.js delete mode 100644 modules/core/src/compiler/annotation_extractor.js create mode 100644 modules/core/src/compiler/pipeline/compile_control.js create mode 100644 modules/core/src/compiler/pipeline/compile_element.js create mode 100644 modules/core/src/compiler/pipeline/compile_pipeline.js create mode 100644 modules/core/src/compiler/pipeline/compile_step.js create mode 100644 modules/core/src/compiler/pipeline/default_steps.js create mode 100644 modules/core/src/compiler/pipeline/directive_parser.js create mode 100644 modules/core/src/compiler/pipeline/element_binder_builder.js create mode 100644 modules/core/src/compiler/pipeline/element_binding_marker.js create mode 100644 modules/core/src/compiler/pipeline/property_binding_parser.js create mode 100644 modules/core/src/compiler/pipeline/proto_element_injector_builder.js create mode 100644 modules/core/src/compiler/pipeline/proto_view_builder.js create mode 100644 modules/core/src/compiler/pipeline/text_interpolation_parser.js create mode 100644 modules/core/src/compiler/pipeline/view_splitter.js create mode 100644 modules/core/src/compiler/reflector.dart create mode 100644 modules/core/src/compiler/reflector.es6 create mode 100644 modules/core/test/compiler/integration_spec.js create mode 100644 modules/core/test/compiler/pipeline/directive_parser_spec.js create mode 100644 modules/core/test/compiler/pipeline/element_binder_builder_spec.js create mode 100644 modules/core/test/compiler/pipeline/element_binding_marker_spec.js create mode 100644 modules/core/test/compiler/pipeline/pipeline_spec.js create mode 100644 modules/core/test/compiler/pipeline/property_binding_parser_spec.js create mode 100644 modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js create mode 100644 modules/core/test/compiler/pipeline/proto_view_builder_spec.js create mode 100644 modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js create mode 100644 modules/core/test/compiler/pipeline/view_splitter_spec.js create mode 100644 modules/core/test/compiler/reflector_spec.js diff --git a/modules/benchmarks/src/compiler/selector_benchmark.js b/modules/benchmarks/src/compiler/selector_benchmark.js index 991cd981da..a711f9c53f 100644 --- a/modules/benchmarks/src/compiler/selector_benchmark.js +++ b/modules/benchmarks/src/compiler/selector_benchmark.js @@ -1,4 +1,5 @@ -import {SelectorMatcher, CssSelector} from "core/compiler/selector"; +import {SelectorMatcher} from "core/compiler/selector"; +import {CssSelector} from "core/compiler/selector"; import {StringWrapper, Math} from 'facade/lang'; import {ListWrapper} from 'facade/collection'; diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index e44ac18a5a..d327b72a69 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -1,6 +1,6 @@ import {ProtoRecord, Record} from './record'; import {FIELD, IMPLEMENTS, isBlank, isPresent} from 'facade/lang'; -import {AST, AccessMember, ImplicitReceiver, AstVisitor} from './parser/ast'; +import {AST, AccessMember, ImplicitReceiver, AstVisitor, Binary, LiteralPrimitive} from './parser/ast'; export class ProtoWatchGroup { @FIELD('headRecord:ProtoRecord') @@ -126,6 +126,17 @@ class ProtoRecordCreator { //do nothing } + // TODO: add tests for this method! + visitLiteralPrimitive(ast:LiteralPrimitive) { + // do nothing + } + + // TODO: add tests for this method! + visitBinary(ast:Binary) { + ast.left.visit(this); + ast.right.visit(this); + } + visitAccessMember(ast:AccessMember) { ast.receiver.visit(this); this.add(new ProtoRecord(this.protoWatchGroup, ast.name, null)); diff --git a/modules/core/src/annotations/component.js b/modules/core/src/annotations/component.js index 17d81b8c58..91a67a2344 100644 --- a/modules/core/src/annotations/component.js +++ b/modules/core/src/annotations/component.js @@ -1,10 +1,11 @@ import {Directive} from './directive'; -import {ABSTRACT, CONST} from 'facade/lang'; +import {CONST} from 'facade/lang'; export class Component extends Directive { @CONST() constructor({ selector, + bind, lightDomServices, implementsTypes, template, @@ -12,15 +13,17 @@ export class Component extends Directive { componentServices }:{ selector:String, + bind:Object, template:TemplateConfig, lightDomServices:DomServicesFunction, shadowDomServices:DomServicesFunction, componentServices:ComponentServicesFunction, implementsTypes:Array - }) + }={}) { super({ selector: selector, + bind: bind, lightDomServices: lightDomServices, implementsTypes: implementsTypes}); this.template = template; diff --git a/modules/core/src/annotations/decorator.js b/modules/core/src/annotations/decorator.js new file mode 100644 index 0000000000..dc214f3f81 --- /dev/null +++ b/modules/core/src/annotations/decorator.js @@ -0,0 +1,25 @@ +import {Directive} from './directive'; +import {CONST} from 'facade/lang'; + +export class Decorator extends Directive { + @CONST() + constructor({ + selector, + bind, + lightDomServices, + implementsTypes + }:{ + selector:String, + bind:Object, + lightDomServices:ElementServicesFunction, + implementsTypes:Array + }={}) + { + super({ + selector: selector, + bind: bind, + lightDomServices: lightDomServices, + implementsTypes: implementsTypes + }); + } +} \ No newline at end of file diff --git a/modules/core/src/annotations/directive.js b/modules/core/src/annotations/directive.js index bbeb0bdd92..8a08c185b9 100644 --- a/modules/core/src/annotations/directive.js +++ b/modules/core/src/annotations/directive.js @@ -8,10 +8,12 @@ export class Directive { @CONST() constructor({ selector, + bind, lightDomServices, implementsTypes }:{ selector:String, + bind:Object, lightDomServices:ElementServicesFunction, implementsTypes:Array }) @@ -19,5 +21,6 @@ export class Directive { this.selector = selector; this.lightDomServices = lightDomServices; this.implementsTypes = implementsTypes; + this.bind = bind; } } diff --git a/modules/core/src/annotations/template.js b/modules/core/src/annotations/template.js new file mode 100644 index 0000000000..cbcd60e971 --- /dev/null +++ b/modules/core/src/annotations/template.js @@ -0,0 +1,25 @@ +import {Directive} from './directive'; +import {CONST} from 'facade/lang'; + +export class Template extends Directive { + @CONST() + constructor({ + selector, + bind, + lightDomServices, + implementsTypes + }:{ + selector:String, + bind:Object, + lightDomServices:ElementServicesFunction, + implementsTypes:Array + }={}) + { + super({ + selector: selector, + bind: bind, + lightDomServices: lightDomServices, + implementsTypes: implementsTypes + }); + } +} \ No newline at end of file diff --git a/modules/core/src/annotations/template_config.js b/modules/core/src/annotations/template_config.js index e733165ae2..d180dbc2e5 100644 --- a/modules/core/src/annotations/template_config.js +++ b/modules/core/src/annotations/template_config.js @@ -5,17 +5,20 @@ export class TemplateConfig { @CONST() constructor({ url, + inline, directives, formatters, source }: { url: String, + inline: String, directives: List, formatters: List, source: List }) { this.url = url; + this.inline = inline; this.directives = directives; this.formatters = formatters; this.source = source; diff --git a/modules/core/src/compiler/annotated_type.js b/modules/core/src/compiler/annotated_type.js index 527c8d498b..f2ec86cc3e 100644 --- a/modules/core/src/compiler/annotated_type.js +++ b/modules/core/src/compiler/annotated_type.js @@ -1,8 +1,11 @@ import {Type, FIELD} from 'facade/lang'; import {Directive} from '../annotations/directive' +/** + * Combination of a type with the Directive annotation + */ export class AnnotatedType { - constructor(annotation:Directive, type:Type) { + constructor(type:Type, annotation:Directive) { this.annotation = annotation; this.type = type; } diff --git a/modules/core/src/compiler/annotation_extractor.js b/modules/core/src/compiler/annotation_extractor.js deleted file mode 100644 index 98ad3633a5..0000000000 --- a/modules/core/src/compiler/annotation_extractor.js +++ /dev/null @@ -1,16 +0,0 @@ -import {Type} from 'facade/lang'; -import {Directive} from '../annotations/directive' - -/** - * Interface representing a way of extracting [Directive] annotations from - * [Type]. This interface has three native implementations: - * - * 1) JavaScript native implementation - * 2) Dart reflective implementation - * 3) Dart transformer generated implementation - */ -export class AnnotationsExtractor { - extract(type:Type):Directive { - return null; - } -} diff --git a/modules/core/src/compiler/compiler.js b/modules/core/src/compiler/compiler.js index f3898032b2..426a4e24d2 100644 --- a/modules/core/src/compiler/compiler.js +++ b/modules/core/src/compiler/compiler.js @@ -1,28 +1,71 @@ -import {Type} from 'facade/lang'; -import {Promise} from 'facade/async'; -import {Element} from 'facade/dom'; -//import {ProtoView} from './view'; +import {Type, FIELD, isBlank, isPresent} from 'facade/lang'; +import {Promise, PromiseWrapper} from 'facade/async'; +import {List, ListWrapper} from 'facade/collection'; +import {DOM, Element} from 'facade/dom'; + +import {Parser} from 'change_detection/parser/parser'; +import {ClosureMap} from 'change_detection/parser/closure_map'; + +import {Reflector} from './reflector'; +import {ProtoView} from './view'; +import {CompilePipeline} from './pipeline/compile_pipeline'; +import {CompileElement} from './pipeline/compile_element'; +import {createDefaultSteps} from './pipeline/default_steps'; import {TemplateLoader} from './template_loader'; -import {FIELD} from 'facade/lang'; +import {AnnotatedType} from './annotated_type'; +/** + * The compiler loads and translates the html templates of components into + * nested ProtoViews. To decompose its functionality it uses + * the CompilePipeline and the CompileSteps. + */ export class Compiler { - - @FIELD('final _templateLoader:TemplateLoader') - constructor(templateLoader:TemplateLoader) { + constructor(templateLoader:TemplateLoader, reflector: Reflector, parser:Parser, closureMap:ClosureMap) { this._templateLoader = templateLoader; + this._reflector = reflector; + this._parser = parser; + this._closureMap = closureMap; } - /** - * # Why promise? - * - compilation will load templates. Instantiating views before templates are loaded will - * complicate the Directive code. BENEFIT: view instantiation become synchrnous. - * # Why result that is independent of injector? - * - don't know about injector in deserialization - * - compile does not need the injector, only the ViewFactory does - */ - compile(component:Type, element:Element/* = null*/):Promise/**/ { - return null; + createSteps(component:AnnotatedType):List { + var directives = component.annotation.template.directives; + var annotatedDirectives = ListWrapper.create(); + for (var i=0; i { + // TODO load all components transitively from the cache first + var cache = null; + return PromiseWrapper.resolve(this._compileAllCached( + this._reflector.annotatedType(component), + cache, + templateRoot) + ); + } + _compileAllCached(component:AnnotatedType, cache, templateRoot:Element = null):ProtoView { + if (isBlank(templateRoot)) { + // TODO: read out the cache if templateRoot = null. Could contain: + // - templateRoot string + // - precompiled template + // - ProtoView + templateRoot = DOM.createTemplate(component.annotation.template.inline); + } + var pipeline = new CompilePipeline(this.createSteps(component)); + var compileElements = pipeline.process(templateRoot); + var rootProtoView = compileElements[0].inheritedProtoView; + // TODO: put the rootProtoView into the cache to support recursive templates! + + for (var i=0; i') - @FIELD('final hasElementPropertyBindings:bool') - constructor(protoElementInjector: ProtoElementInjector, - textNodeIndices:List, hasElementPropertyBindings:boolean) { - this.protoElementInjector = protoElementInjector; - this.textNodeIndices = textNodeIndices; - this.hasElementPropertyBindings = hasElementPropertyBindings; + @FIELD('hasElementPropertyBindings:bool') + @FIELD('nestedProtoView:ProtoView') + constructor(protoElementInjector: ProtoElementInjector) { + this.protoElementInjector = protoElementInjector; + // updated later when text nodes are bound + this.textNodeIndices = []; + // updated later when element properties are bound + this.hasElementPropertyBindings = false; + // updated later, so we are able to resolve cycles + this.nestedProtoView = null; } } diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index 099210952c..5448a83394 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -3,8 +3,9 @@ import {Math} from 'facade/math'; import {List, ListWrapper} from 'facade/collection'; import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'di/di'; import {Parent, Ancestor} from 'core/annotations/visibility'; -import {View} from './view'; import {StaticKeys} from './static_keys'; +// Comment out as dartanalyzer does not look into @FIELD +// import {View} from './view'; var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10; diff --git a/modules/core/src/compiler/pipeline/compile_control.js b/modules/core/src/compiler/pipeline/compile_control.js new file mode 100644 index 0000000000..a1bb786bf5 --- /dev/null +++ b/modules/core/src/compiler/pipeline/compile_control.js @@ -0,0 +1,47 @@ +import {ListWrapper} from 'facade/collection'; +import {DOM} from 'facade/dom'; +import {CompileElement} from './compile_element'; + +/** + * Controls the processing order of elements. + * Right now it only allows to add a parent element. + */ +export class CompileControl { + constructor(steps) { + this._steps = steps; + this._currentStepIndex = 0; + this._parent = null; + this._current = null; + this._results = 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 { + 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; + } + + addTextNodeBinding(indexInParent:int, expression:string) { + if (isBlank(this.textNodeBindings)) { + this.textNodeBindings = MapWrapper.create(); + } + MapWrapper.set(this.textNodeBindings, indexInParent, expression); + } + + addPropertyBinding(property:string, expression:string) { + if (isBlank(this.propertyBindings)) { + this.propertyBindings = MapWrapper.create(); + } + MapWrapper.set(this.propertyBindings, property, expression); + } + + addDirective(directive:AnnotatedType) { + var annotation = directive.annotation; + if (annotation instanceof Decorator) { + if (isBlank(this.decoratorDirectives)) { + this.decoratorDirectives = ListWrapper.create(); + } + ListWrapper.push(this.decoratorDirectives, directive); + } else if (annotation instanceof Template) { + this.templateDirective = directive; + } else if (annotation instanceof Component) { + this.componentDirective = directive; + } + } +} diff --git a/modules/core/src/compiler/pipeline/compile_pipeline.js b/modules/core/src/compiler/pipeline/compile_pipeline.js new file mode 100644 index 0000000000..1eca885b13 --- /dev/null +++ b/modules/core/src/compiler/pipeline/compile_pipeline.js @@ -0,0 +1,39 @@ +import {List, ListWrapper} from 'facade/collection'; +import {Element, TemplateElement, Node} from 'facade/dom'; +import {CompileElement} from './compile_element'; +import {CompileControl} from './compile_control'; +import {CompileStep} from './compile_step'; +import {AnnotatedType} from '../annotated_type'; + +/** + * CompilePipeline for executing CompileSteps recursively for + * all elements in a template. + */ +export class CompilePipeline { + constructor(steps:List) { + this._control = new CompileControl(steps); + } + + process(rootElement:Element):List { + var results = ListWrapper.create(); + this._process(results, null, rootElement); + return results; + } + + _process(results, parent:CompileElement, element:Element) { + var current = new CompileElement(element); + this._control.internalProcess(results, 0, parent, current); + var childNodes; + if (element instanceof TemplateElement) { + childNodes = element.content.childNodes; + } else { + childNodes = element.childNodes; + } + for (var i=0; i + ) { + return [ + new PropertyBindingParser(), + new TextInterpolationParser(), + new DirectiveParser(directives), + new ViewSplitter(), + new ElementBindingMarker(), + new ProtoViewBuilder(), + new ProtoElementInjectorBuilder(), + new ElementBinderBuilder(parser, closureMap) + ]; +} \ No newline at end of file diff --git a/modules/core/src/compiler/pipeline/directive_parser.js b/modules/core/src/compiler/pipeline/directive_parser.js new file mode 100644 index 0000000000..75cb2efd3d --- /dev/null +++ b/modules/core/src/compiler/pipeline/directive_parser.js @@ -0,0 +1,66 @@ +import {isPresent, BaseException} from 'facade/lang'; +import {List, MapWrapper} from 'facade/collection'; +import {SelectorMatcher} from '../selector'; +import {CssSelector} from '../selector'; + +import {AnnotatedType} from '../annotated_type'; +import {Template} from '../../annotations/template'; +import {Component} from '../../annotations/component'; +import {CompileStep} from './compile_step'; +import {CompileElement} from './compile_element'; +import {CompileControl} from './compile_control'; +import {Reflector} from '../reflector'; + +/** + * Parses the directives on a single element. + * + * Fills: + * - CompileElement#decoratorDirectives + * - CompileElement#templateDirecitve + * - CompileElement#componentDirective. + * + * Reads: + * - CompileElement#propertyBindings (to find directives contained + * in the property bindings) + */ +export class DirectiveParser extends CompileStep { + constructor(directives:List) { + this._selectorMatcher = new SelectorMatcher(); + for (var i=0; i { + cssSelector.addAttribute(attrName, attrValue); + }); + // Allow to find directives even though the attribute is bound + if (isPresent(current.propertyBindings)) { + MapWrapper.forEach(current.propertyBindings, (expression, boundProp) => { + cssSelector.addAttribute(boundProp, expression); + }); + } + this._selectorMatcher.match(cssSelector, (directive) => { + if (isPresent(current.templateDirective) && (directive.annotation instanceof Template)) { + throw new BaseException('Only one template directive per element is allowed!'); + } + if (isPresent(current.componentDirective) && (directive.annotation instanceof Component)) { + throw new BaseException('Only one component directive per element is allowed!'); + } + current.addDirective(directive); + }); + } +} diff --git a/modules/core/src/compiler/pipeline/element_binder_builder.js b/modules/core/src/compiler/pipeline/element_binder_builder.js new file mode 100644 index 0000000000..2165d0a3d0 --- /dev/null +++ b/modules/core/src/compiler/pipeline/element_binder_builder.js @@ -0,0 +1,123 @@ +import {int, isPresent, isBlank, Type, BaseException, stringify} from 'facade/lang'; +import {Element} from 'facade/dom'; +import {ListWrapper, List, MapWrapper, StringMapWrapper} from 'facade/collection'; + +import {Parser} from 'change_detection/parser/parser'; +import {ClosureMap} from 'change_detection/parser/closure_map'; +import {ProtoWatchGroup} from 'change_detection/watch_group'; + +import {Directive} from '../../annotations/directive'; +import {Component} from '../../annotations/component'; +import {AnnotatedType} from '../annotated_type'; +import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from '../view'; +import {ProtoElementInjector} from '../element_injector'; +import {ElementBinder} from '../element_binder'; +import {Reflector} from '../reflector'; + +import {CompileStep} from './compile_step'; +import {CompileElement} from './compile_element'; +import {CompileControl} from './compile_control'; + +/** + * Creates the ElementBinders and adds watches to the + * ProtoWatchGroup. + * + * Fills: + * - CompileElement#inheritedElementBinder + * + * Reads: + * - (in parent) CompileElement#inheritedElementBinder + * - CompileElement#hasBindings + * - CompileElement#isViewRoot + * - CompileElement#inheritedViewRoot + * - CompileElement#inheritedProtoElementInjector + * - CompileElement#textNodeBindings + * - CompileElement#propertyBindings + * - CompileElement#decoratorDirectives + * - CompileElement#componentDirective + * - CompileElement#templateDirective + * + * Note: This actually only needs the CompileElements with the flags + * `hasBindings` and `isViewRoot`, + * and only needs the actual HTMLElement for the ones + * with the flag `isViewRoot`. + */ +export class ElementBinderBuilder extends CompileStep { + constructor(parser:Parser, closureMap:ClosureMap) { + this._parser = parser; + this._closureMap = closureMap; + } + + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + var elementBinder; + if (current.hasBindings) { + var protoView = current.inheritedProtoView; + elementBinder = protoView.bindElement(current.inheritedProtoElementInjector); + + if (isPresent(current.textNodeBindings)) { + this._bindTextNodes(protoView, current.textNodeBindings); + } + if (isPresent(current.propertyBindings)) { + this._bindElementProperties(protoView, current.propertyBindings); + } + this._bindDirectiveProperties(this._collectDirectives(current), current); + } else if (isPresent(parent)) { + elementBinder = parent.inheritedElementBinder; + } + current.inheritedElementBinder = elementBinder; + } + + _bindTextNodes(protoView, textNodeBindings) { + MapWrapper.forEach(textNodeBindings, (expression, indexInParent) => { + protoView.bindTextNode(indexInParent, this._parser.parseBinding(expression)); + }); + } + + _bindElementProperties(protoView, propertyBindings) { + MapWrapper.forEach(propertyBindings, (expression, property) => { + protoView.bindElementProperty(property, this._parser.parseBinding(expression)); + }); + } + + _collectDirectives(pipelineElement) { + var directives; + if (isPresent(pipelineElement.decoratorDirectives)) { + directives = ListWrapper.clone(pipelineElement.decoratorDirectives); + } else { + directives = []; + } + if (isPresent(pipelineElement.templateDirective)) { + ListWrapper.push(directives, pipelineElement.templateDirective); + } + if (isPresent(pipelineElement.componentDirective)) { + ListWrapper.push(directives, pipelineElement.componentDirective); + } + return directives; + } + + _bindDirectiveProperties(typesWithAnnotations, pipelineElement) { + var protoView = pipelineElement.inheritedProtoView; + var directiveIndex = 0; + ListWrapper.forEach(typesWithAnnotations, (typeWithAnnotation) => { + var annotation = typeWithAnnotation.annotation; + if (isBlank(annotation.bind)) { + return; + } + StringMapWrapper.forEach(annotation.bind, (dirProp, elProp) => { + var expression = isPresent(pipelineElement.propertyBindings) ? + MapWrapper.get(pipelineElement.propertyBindings, elProp) : + null; + if (isBlank(expression)) { + throw new BaseException('No element binding found for property '+elProp + +' which is required by directive '+stringify(typeWithAnnotation.type)); + } + protoView.bindDirectiveProperty( + directiveIndex++, + this._parser.parseBinding(expression), + dirProp, + this._closureMap.setter(dirProp) + ); + }); + }); + } +} diff --git a/modules/core/src/compiler/pipeline/element_binding_marker.js b/modules/core/src/compiler/pipeline/element_binding_marker.js new file mode 100644 index 0000000000..fab83141b7 --- /dev/null +++ b/modules/core/src/compiler/pipeline/element_binding_marker.js @@ -0,0 +1,40 @@ +import {isPresent} from 'facade/lang'; +import {MapWrapper} from 'facade/collection'; +import {DOM} from 'facade/dom'; + +import {CompileStep} from './compile_step'; +import {CompileElement} from './compile_element'; +import {CompileControl} from './compile_control'; + +const NG_BINDING_CLASS = 'ng-binding'; + +/** + * Marks elements that have bindings with a css class + * and sets the CompileElement.hasBindings flag. + * + * Fills: + * - CompileElement#hasBindings + * + * Reads: + * - CompileElement#textNodeBindings + * - CompileElement#propertyBindings + * - CompileElement#decoratorDirectives + * - CompileElement#componentDirective + * - CompileElement#templateDirective + */ +export class ElementBindingMarker extends CompileStep { + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + var hasBindings = + (isPresent(current.textNodeBindings) && MapWrapper.size(current.textNodeBindings)>0) || + (isPresent(current.propertyBindings) && MapWrapper.size(current.propertyBindings)>0) || + (isPresent(current.decoratorDirectives) && current.decoratorDirectives.length > 0) || + isPresent(current.templateDirective) || + isPresent(current.componentDirective); + + if (hasBindings) { + var element = current.element; + DOM.addClass(element, NG_BINDING_CLASS); + current.hasBindings = true; + } + } +} diff --git a/modules/core/src/compiler/pipeline/property_binding_parser.js b/modules/core/src/compiler/pipeline/property_binding_parser.js new file mode 100644 index 0000000000..75a44400d5 --- /dev/null +++ b/modules/core/src/compiler/pipeline/property_binding_parser.js @@ -0,0 +1,31 @@ +import {isPresent, isBlank, RegExpWrapper} from 'facade/lang'; +import {MapWrapper} from 'facade/collection'; + +import {CompileStep} from './compile_step'; +import {CompileElement} from './compile_element'; +import {CompileControl} from './compile_control'; + +// TODO(tbosch): Cannot make this const/final right now because of the transpiler... +var BIND_DASH_REGEXP = RegExpWrapper.create('bind-((?:[^-]|-(?!-))+)(?:--(.+))?'); +var PROP_BIND_REGEXP = RegExpWrapper.create('\\[([^|]+)(?:\\|(.+))?\\]'); + +/** + * Parses the property bindings on a single element. + * + * Fills: + * - CompileElement#propertyBindings + */ +export class PropertyBindingParser extends CompileStep { + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + var attrs = current.attrs(); + MapWrapper.forEach(attrs, (attrValue, attrName) => { + var parts = RegExpWrapper.firstMatch(BIND_DASH_REGEXP, attrName); + if (isBlank(parts)) { + parts = RegExpWrapper.firstMatch(PROP_BIND_REGEXP, attrName); + } + if (isPresent(parts)) { + current.addPropertyBinding(parts[1], attrValue); + } + }); + } +} diff --git a/modules/core/src/compiler/pipeline/proto_element_injector_builder.js b/modules/core/src/compiler/pipeline/proto_element_injector_builder.js new file mode 100644 index 0000000000..82fcb8c2a4 --- /dev/null +++ b/modules/core/src/compiler/pipeline/proto_element_injector_builder.js @@ -0,0 +1,74 @@ +import {isPresent,} from 'facade/lang'; +import {ListWrapper} from 'facade/collection'; + +import {ProtoElementInjector} from '../element_injector'; + +import {CompileStep} from './compile_step'; +import {CompileElement} from './compile_element'; +import {CompileControl} from './compile_control'; + +/** + * Creates the ProtoElementInjectors. + * + * Fills: + * - CompileElement#inheriteProtoElementInjector + * + * Reads: + * - (in parent) CompileElement#inheriteProtoElementInjector + * - CompileElement#isViewRoot + * - CompileElement#inheritedProtoView + * - CompileElement#decoratorDirectives + * - CompileElement#componentDirective + * - CompileElement#templateDirective + */ +export class ProtoElementInjectorBuilder extends CompileStep { + // public so that we can overwrite it in tests + internalCreateProtoElementInjector(parent, index, directives) { + return new ProtoElementInjector(parent, index, directives); + } + + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + var inheritedProtoElementInjector = null; + var parentProtoElementInjector = this._getParentProtoElementInjector(parent, current); + var injectorBindings = this._collectDirectiveTypes(current); + // TODO: add lightDomServices as well, + // but after the directives as we rely on that order + // in the element_binder_builder. + + if (injectorBindings.length > 0) { + var protoView = current.inheritedProtoView; + inheritedProtoElementInjector = this.internalCreateProtoElementInjector( + parentProtoElementInjector, protoView.elementBinders.length, injectorBindings + ); + } else { + inheritedProtoElementInjector = parentProtoElementInjector; + } + current.inheritedProtoElementInjector = inheritedProtoElementInjector; + } + + _getParentProtoElementInjector(parent, current) { + var parentProtoElementInjector = null; + if (current.isViewRoot) { + parentProtoElementInjector = null; + } else if (isPresent(parent)) { + parentProtoElementInjector = parent.inheritedProtoElementInjector; + } + return parentProtoElementInjector; + } + + _collectDirectiveTypes(pipelineElement) { + var directiveTypes = []; + if (isPresent(pipelineElement.decoratorDirectives)) { + for (var i=0; i 1) { + for (var i=0; i element that contains the + * template directive and all property bindings needed for the template directive. + * + * Fills: + * - CompileElement#isViewRoot + * + * Updates: + * - CompileElement#templateDirective + * - CompileElement#propertyBindings + * + * Reads: + * - CompileElement#templateDirective + * - CompileElement#propertyBindings + */ +export class ViewSplitter extends CompileStep { + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + var element = current.element; + if (isPresent(current.templateDirective)) { + var templateElement = DOM.createTemplate(''); + var templateBoundProperties = MapWrapper.create(); + var nonTemplateBoundProperties = MapWrapper.create(); + this._splitElementPropertyBindings(current, templateBoundProperties, nonTemplateBoundProperties); + + var newParentElement = new CompileElement(templateElement); + newParentElement.propertyBindings = templateBoundProperties; + newParentElement.templateDirective = current.templateDirective; + control.addParent(newParentElement); + + // disconnect child view from their parent view + element.remove(); + + current.templateDirective = null; + current.propertyBindings = nonTemplateBoundProperties; + current.isViewRoot = true; + } else if (isBlank(parent)) { + current.isViewRoot = true; + } + } + + _splitElementPropertyBindings(compileElement, templateBoundProperties, nonTemplateBoundProperties) { + var dirBindings = compileElement.templateDirective.annotation.bind; + if (isPresent(dirBindings) && isPresent(compileElement.propertyBindings)) { + MapWrapper.forEach(compileElement.propertyBindings, (expr, elProp) => { + if (isPresent(StringMapWrapper.get(dirBindings, elProp))) { + MapWrapper.set(templateBoundProperties, elProp, expr); + } else { + MapWrapper.set(nonTemplateBoundProperties, elProp, expr); + } + }); + } + } +} diff --git a/modules/core/src/compiler/reflector.dart b/modules/core/src/compiler/reflector.dart new file mode 100644 index 0000000000..b75ac3acbb --- /dev/null +++ b/modules/core/src/compiler/reflector.dart @@ -0,0 +1,27 @@ +library facade.di.reflector; + +import 'dart:mirrors'; +import '../annotations/directive.dart'; +import './annotated_type.dart'; +import 'package:facade/lang.dart'; + +/** + * Interface representing a way of extracting [Directive] annotations from + * [Type]. This interface has three native implementations: + * + * 1) JavaScript native implementation + * 2) Dart reflective implementation + * 3) Dart transformer generated implementation + */ +class Reflector { + AnnotatedType annotatedType(Type type) { + var directiveAnnotations = reflectType(type).metadata + .map( (im) => im.reflectee) + .where( (annotation) => annotation is Directive); + if (directiveAnnotations.isEmpty) { + throw new BaseException('No Directive annotation found on '+stringify(type)); + } + return new AnnotatedType(type, directiveAnnotations.first); + } + +} diff --git a/modules/core/src/compiler/reflector.es6 b/modules/core/src/compiler/reflector.es6 new file mode 100644 index 0000000000..c64d3332cd --- /dev/null +++ b/modules/core/src/compiler/reflector.es6 @@ -0,0 +1,26 @@ +import {Type, isPresent, BaseException} from 'facade/lang'; +import {Directive} from '../annotations/directive'; +import {AnnotatedType} from './annotated_type'; + +/** + * Interface representing a way of extracting [Directive] annotations from + * [Type]. This interface has three native implementations: + * + * 1) JavaScript native implementation + * 2) Dart reflective implementation + * 3) Dart transformer generated implementation + */ +export class Reflector { + annotatedType(type:Type):AnnotatedType { + var annotations = type.annotations; + if (annotations) { + for (var i=0; i 0) { + res += '=' + attrValue; + } + res += ']'; + } + } + return res; + } +} + +/** + * Reads a list of CssSelectors and allows to calculate which ones + * are contained in a given CssSelector. + */ export class SelectorMatcher { - /* TODO: Add these fields when the transpiler supports fields - _elementMap:Map; - _elementPartialMap:Map; - - _classMap:Map; - _classPartialMap:Map; - - _attrValueMap:Map>; - _attrValuePartialMap:Map>; - */ constructor() { this._selectables = ListWrapper.create(); - this._elementMap = StringMapWrapper.create(); - this._elementPartialMap = StringMapWrapper.create(); + this._elementMap = MapWrapper.create(); + this._elementPartialMap = MapWrapper.create(); - this._classMap = StringMapWrapper.create(); - this._classPartialMap = StringMapWrapper.create(); + this._classMap = MapWrapper.create(); + this._classPartialMap = MapWrapper.create(); - this._attrValueMap = StringMapWrapper.create(); - this._attrValuePartialMap = StringMapWrapper.create(); + this._attrValueMap = MapWrapper.create(); + this._attrValuePartialMap = MapWrapper.create(); } /** @@ -60,16 +137,15 @@ export class SelectorMatcher { } if (isPresent(attrs)) { - for (var index = 0; index, name:string, selectable) { + var terminalList = MapWrapper.get(map, name) if (isBlank(terminalList)) { terminalList = ListWrapper.create(); - StringMapWrapper.set(map, name, terminalList); + MapWrapper.set(map, name, terminalList); } ListWrapper.push(terminalList, selectable); } - // TODO: map:StringMap when we have a StringMap type... - _addPartial(map, name:string) { - var matcher = StringMapWrapper.get(map, name) + + _addPartial(map:Map, name:string) { + var matcher = MapWrapper.get(map, name) if (isBlank(matcher)) { matcher = new SelectorMatcher(); - StringMapWrapper.set(map, name, matcher); + MapWrapper.set(map, name, matcher); } return matcher; } @@ -121,25 +197,27 @@ export class SelectorMatcher { } if (isPresent(attrs)) { - for (var index = 0; index = null, name, matchedCallback) { if (isBlank(map) || isBlank(name)) { return; } - var selectables = StringMapWrapper.get(map, name) + var selectables = MapWrapper.get(map, name) if (isBlank(selectables)) { return; } @@ -147,12 +225,12 @@ export class SelectorMatcher { matchedCallback(selectables[index]); } } - // TODO: map:StringMap when we have a StringMap type... - _matchPartial(map, name, cssSelector, matchedCallback) { + + _matchPartial(map:Map = null, name, cssSelector, matchedCallback) { if (isBlank(map) || isBlank(name)) { return; } - var nestedSelector = StringMapWrapper.get(map, name) + var nestedSelector = MapWrapper.get(map, name) if (isBlank(nestedSelector)) { return; } @@ -162,71 +240,3 @@ export class SelectorMatcher { nestedSelector.match(cssSelector, matchedCallback); } } - -export class Attr { - @CONST() - constructor(name:string, value:string = null) { - this.name = name; - this.value = value; - } -} - -// TODO: Can't use `const` here as -// in Dart this is not transpiled into `final` yet... -var _SELECTOR_REGEXP = - RegExpWrapper.create('^([-\\w]+)|' + // "tag" - '(?:\\.([-\\w]+))|' + // ".class" - '(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])'); // "[name]", "[name=value]" or "[name*=value]" - -export class CssSelector { - static parse(selector:string):CssSelector { - var element = null; - var classNames = ListWrapper.create(); - var attrs = ListWrapper.create(); - selector = selector.toLowerCase(); - var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector); - var match; - while (isPresent(match = RegExpMatcherWrapper.next(matcher))) { - if (isPresent(match[1])) { - element = match[1]; - } - if (isPresent(match[2])) { - ListWrapper.push(classNames, match[2]); - } - if (isPresent(match[3])) { - ListWrapper.push(attrs, new Attr(match[3], match[4])); - } - } - return new CssSelector(element, classNames, attrs); - } - // TODO: do a toLowerCase() for all arguments - @CONST() - constructor(element:string, classNames:List, attrs:List) { - this.element = element; - this.classNames = classNames; - this.attrs = attrs; - } - - toString():string { - var res = ''; - if (isPresent(this.element)) { - res += this.element; - } - if (isPresent(this.classNames)) { - for (var i=0; i*/ { + load(url:String):Promise { return null; } } \ No newline at end of file diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index d289bc24b4..4b58fb2087 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -2,22 +2,21 @@ import {DOM, Element, Node, Text, DocumentFragment, TemplateElement} from 'facad import {ListWrapper} from 'facade/collection'; import {ProtoWatchGroup, WatchGroup, WatchGroupDispatcher} from 'change_detection/watch_group'; import {Record} from 'change_detection/record'; +import {AST} from 'change_detection/parser/ast'; import {ProtoElementInjector, ElementInjector} from './element_injector'; -// Seems like we are stripping the generics part of List and dartanalyzer -// complains about ElementBinder being unused. Comment back in once it makes it -// into the generated code. -// import {ElementBinder} from './element_binder'; +import {ElementBinder} from './element_binder'; import {SetterFn} from 'change_detection/parser/closure_map'; import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang'; import {List} from 'facade/collection'; import {Injector} from 'di/di'; +const NG_BINDING_CLASS = 'ng-binding'; + /*** * Const of making objects: http://jsperf.com/instantiate-size-of-object */ @IMPLEMENTS(WatchGroupDispatcher) export class View { - @FIELD('final fragment:DocumentFragment') /// This list matches the _nodes list. It is sparse, since only Elements have ElementInjector @FIELD('final rootElementInjectors:List') @FIELD('final elementInjectors:List') @@ -28,12 +27,11 @@ export class View { /// to keep track of the nodes. @FIELD('final nodes:List') @FIELD('final onChangeDispatcher:OnChangeDispatcher') - constructor(fragment:DocumentFragment, elementInjector:List, + constructor(nodes:List, elementInjectors:List, rootElementInjectors:List, textNodes:List, bindElements:List, protoWatchGroup:ProtoWatchGroup, context) { - this.fragment = fragment; - this.nodes = ListWrapper.clone(fragment.childNodes); - this.elementInjectors = elementInjector; + this.nodes = nodes; + this.elementInjectors = elementInjectors; this.rootElementInjectors = rootElementInjectors; this.onChangeDispatcher = null; this.textNodes = textNodes; @@ -60,27 +58,26 @@ export class View { } export class ProtoView { - @FIELD('final _template:TemplateElement') - @FIELD('final _elementBinders:List') - @FIELD('final _protoWatchGroup:ProtoWatchGroup') - @FIELD('final _useRootElement:bool') + @FIELD('final element:Element') + @FIELD('final elementBinders:List') + @FIELD('final protoWatchGroup:ProtoWatchGroup') constructor( - template:TemplateElement, - elementBinders:List, - protoWatchGroup:ProtoWatchGroup, - useRootElement:boolean) { - this._template = template; - this._elementBinders = elementBinders; - this._protoWatchGroup = protoWatchGroup; - - // not implemented - this._useRootElement = useRootElement; + template:Element, + protoWatchGroup:ProtoWatchGroup) { + this.element = template; + this.elementBinders = []; + this.protoWatchGroup = protoWatchGroup; + this.textNodesWithBindingCount = 0; + this.elementsWithBindingCount = 0; } instantiate(context, appInjector:Injector):View { - var fragment = DOM.clone(this._template.content); - var elements = DOM.querySelectorAll(fragment, ".ng-binding"); - var binders = this._elementBinders; + var clone = DOM.clone(this.element); + var elements = ListWrapper.clone(DOM.getElementsByClassName(clone, NG_BINDING_CLASS)); + if (DOM.hasClass(clone, NG_BINDING_CLASS)) { + ListWrapper.insert(elements, 0, clone); + } + var binders = this.elementBinders; /** * TODO: vsavkin: benchmark @@ -92,16 +89,77 @@ export class ProtoView { var bindElements = ProtoView._bindElements(elements, binders); ProtoView._instantiateDirectives(elementInjectors, appInjector); - return new View(fragment, elementInjectors, rootElementInjectors, textNodes, - bindElements, this._protoWatchGroup, context); + var viewNodes; + if (clone instanceof TemplateElement) { + viewNodes = ListWrapper.clone(clone.content.childNodes); + } else { + viewNodes = [clone]; + } + return new View(viewNodes, elementInjectors, rootElementInjectors, textNodes, + bindElements, this.protoWatchGroup, context); + } + + bindElement(protoElementInjector:ProtoElementInjector):ElementBinder { + var elBinder = new ElementBinder(protoElementInjector); + ListWrapper.push(this.elementBinders, elBinder); + return elBinder; + } + + /** + * Adds a text node binding for the last created ElementBinder via bindElement + */ + bindTextNode(indexInParent:int, expression:AST) { + var elBinder = this.elementBinders[this.elementBinders.length-1]; + ListWrapper.push(elBinder.textNodeIndices, indexInParent); + this.protoWatchGroup.watch(expression, this.textNodesWithBindingCount++); + } + + /** + * Adds an element property binding for the last created ElementBinder via bindElement + */ + bindElementProperty(propertyName:string, expression:AST) { + var elBinder = this.elementBinders[this.elementBinders.length-1]; + if (!elBinder.hasElementPropertyBindings) { + elBinder.hasElementPropertyBindings = true; + this.elementsWithBindingCount++; + } + this.protoWatchGroup.watch(expression, + new ElementPropertyMemento( + this.elementsWithBindingCount-1, + propertyName + ) + ); + } + + /** + * Adds a directive property binding for the last created ElementBinder via bindElement + */ + bindDirectiveProperty( + directiveIndex:number, + expression:AST, + setterName:string, + setter:SetterFn) { + this.protoWatchGroup.watch( + expression, + new DirectivePropertyMemento( + this.elementBinders.length-1, + directiveIndex, + setterName, + setter + ) + ); } static _createElementInjectors(elements, binders) { var injectors = ListWrapper.createFixedSize(binders.length); for (var i = 0; i < binders.length; ++i) { var proto = binders[i].protoElementInjector; - var parentElementInjector = isPresent(proto.parent) ? injectors[proto.parent.index] : null; - injectors[i] = ProtoView._createElementInjector(elements[i], parentElementInjector, proto); + if (isPresent(proto)) { + var parentElementInjector = isPresent(proto.parent) ? injectors[proto.parent.index] : null; + injectors[i] = ProtoView._createElementInjector(elements[i], parentElementInjector, proto); + } else { + injectors[i] = null; + } } return injectors; } @@ -115,7 +173,7 @@ export class ProtoView { static _createElementInjector(element, parent:ElementInjector, proto:ProtoElementInjector) { //TODO: vsavkin: pass element to `proto.instantiate()` once https://github.com/angular/angular/pull/98 is merged - return proto.hasBindings ? proto.instantiate(parent, null) : null; + return proto.instantiate(parent, null); } static _rootElementInjectors(injectors) { @@ -150,7 +208,7 @@ export class ProtoView { export class ElementPropertyMemento { @FIELD('final _elementIndex:int') - @FIELD('final _propertyIndex:string') + @FIELD('final _propertyName:string') constructor(elementIndex:int, propertyName:string) { this._elementIndex = elementIndex; this._propertyName = propertyName; diff --git a/modules/core/test/compiler/compiler_spec.js b/modules/core/test/compiler/compiler_spec.js index 161feb5959..5043817c53 100644 --- a/modules/core/test/compiler/compiler_spec.js +++ b/modules/core/test/compiler/compiler_spec.js @@ -1,7 +1,121 @@ -import {describe, it} from 'test_lib/test_lib'; -//import {Compiler} from 'core/compiler/compiler'; +import {describe, beforeEach, it, expect, ddescribe, iit} from 'test_lib/test_lib'; +import {DOM} from 'facade/dom'; +import {List} from 'facade/collection'; + +import {Compiler} from 'core/compiler/compiler'; +import {ProtoView} from 'core/compiler/view'; +import {Reflector} from 'core/compiler/reflector'; +import {TemplateLoader} from 'core/compiler/template_loader'; +import {Component} from 'core/annotations/component'; +import {TemplateConfig} from 'core/annotations/template_config'; +import {CompileElement} from 'core/compiler/pipeline/compile_element'; +import {CompileStep} from 'core/compiler/pipeline/compile_step' +import {CompileControl} from 'core/compiler/pipeline/compile_control'; + +import {Parser} from 'change_detection/parser/parser'; +import {Lexer} from 'change_detection/parser/lexer'; +import {ClosureMap} from 'change_detection/parser/closure_map'; export function main() { describe('compiler', function() { + var compiler, reflector; + + beforeEach( () => { + reflector = new Reflector(); + }); + + function createCompiler(processClosure) { + var closureMap = new ClosureMap(); + var steps = [new MockStep(processClosure)]; + return new TestableCompiler(null, reflector, new Parser(new Lexer(), closureMap), closureMap, steps); + } + + it('should run the steps and return the ProtoView of the root element', (done) => { + var rootProtoView = new ProtoView(null, null); + var compiler = createCompiler( (parent, current, control) => { + current.inheritedProtoView = rootProtoView; + }); + compiler.compile(MainComponent, createElement('
')).then( (protoView) => { + expect(protoView).toBe(rootProtoView); + done(); + }); + }); + + it('should use the given element', (done) => { + var el = createElement('
'); + var compiler = createCompiler( (parent, current, control) => { + current.inheritedProtoView = new ProtoView(current.element, null); + }); + compiler.compile(MainComponent, el).then( (protoView) => { + expect(protoView.element).toBe(el); + done(); + }); + }); + + it('should use the inline template if no element is given explicitly', (done) => { + var compiler = createCompiler( (parent, current, control) => { + current.inheritedProtoView = new ProtoView(current.element, null); + }); + compiler.compile(MainComponent, null).then( (protoView) => { + expect(DOM.getInnerHTML(protoView.element)).toEqual('inline component'); + done(); + }); + }); + + it('should load nested components', (done) => { + var mainEl = createElement('
'); + var compiler = createCompiler( (parent, current, control) => { + current.inheritedProtoView = new ProtoView(current.element, null); + current.inheritedElementBinder = current.inheritedProtoView.bindElement(null); + if (current.element === mainEl) { + current.componentDirective = reflector.annotatedType(NestedComponent); + } + }); + compiler.compile(MainComponent, mainEl).then( (protoView) => { + var nestedView = protoView.elementBinders[0].nestedProtoView; + expect(DOM.getInnerHTML(nestedView.element)).toEqual('nested component'); + done(); + }); + + }); + }); + +} + +@Component({ + template: new TemplateConfig({ + inline: 'inline component' + }) +}) +class MainComponent {} + +@Component({ + template: new TemplateConfig({ + inline: 'nested component' + }) +}) +class NestedComponent {} + +class TestableCompiler extends Compiler { + constructor(templateLoader:TemplateLoader, reflector:Reflector, parser, closureMap, steps:List) { + super(templateLoader, reflector, parser, closureMap); + this.steps = steps; + } + createSteps(component):List { + return this.steps; + } +} + +class MockStep extends CompileStep { + constructor(process) { + this.processClosure = process; + } + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + this.processClosure(parent, current, control); + } +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; } diff --git a/modules/core/test/compiler/integration_spec.js b/modules/core/test/compiler/integration_spec.js new file mode 100644 index 0000000000..c43eefa1ce --- /dev/null +++ b/modules/core/test/compiler/integration_spec.js @@ -0,0 +1,97 @@ +import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/test_lib'; + +import {DOM} from 'facade/dom'; + +import {ChangeDetector} from 'change_detection/change_detector'; +import {Parser} from 'change_detection/parser/parser'; +import {ClosureMap} from 'change_detection/parser/closure_map'; +import {Lexer} from 'change_detection/parser/lexer'; + +import {Compiler} from 'core/compiler/compiler'; +import {Reflector} from 'core/compiler/reflector'; + +import {Component} from 'core/annotations/component'; +import {Decorator} from 'core/annotations/decorator'; +import {TemplateConfig} from 'core/annotations/template_config'; + +export function main() { + describe('integration tests', function() { + var compiler; + + beforeEach( () => { + var closureMap = new ClosureMap(); + compiler = new Compiler(null, new Reflector(), new Parser(new Lexer(), closureMap), closureMap); + }); + + describe('react to watch group changes', function() { + var view, ctx, cd; + function createView(pv) { + ctx = new MyComp(); + view = pv.instantiate(ctx, null); + cd = new ChangeDetector(view.watchGroup); + } + + it('should consume text node changes', (done) => { + compiler.compile(MyComp, createElement('
{{ctxProp}}
')).then((pv) => { + createView(pv); + ctx.ctxProp = 'Hello World!'; + + cd.detectChanges(); + expect(DOM.getInnerHTML(view.nodes[0])).toEqual('Hello World!'); + done(); + }); + }); + + it('should consume element binding changes', (done) => { + compiler.compile(MyComp, createElement('
')).then((pv) => { + createView(pv); + + ctx.ctxProp = 'Hello World!'; + cd.detectChanges(); + + expect(view.nodes[0].id).toEqual('Hello World!'); + done(); + }); + }); + + it('should consume directive watch expression change.', (done) => { + compiler.compile(MyComp, createElement('
')).then((pv) => { + createView(pv); + + ctx.ctxProp = 'Hello World!'; + cd.detectChanges(); + + var elInj = view.elementInjectors[0]; + expect(elInj.get(MyDir).dirProp).toEqual('Hello World!'); + done(); + }); + }); + }); + }); +} + +@Decorator({ + selector: '[my-dir]', + bind: {'elprop':'dirProp'} +}) +class MyDir { + constructor() { + this.dirProp = ''; + } +} + +@Component({ + template: new TemplateConfig({ + directives: [MyDir] + }) +}) +class MyComp { + constructor() { + this.ctxProp = 'initial value'; + } +} + + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} diff --git a/modules/core/test/compiler/pipeline/directive_parser_spec.js b/modules/core/test/compiler/pipeline/directive_parser_spec.js new file mode 100644 index 0000000000..05b78890b5 --- /dev/null +++ b/modules/core/test/compiler/pipeline/directive_parser_spec.js @@ -0,0 +1,129 @@ +import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; +import {isPresent} from 'facade/lang'; +import {ListWrapper, MapWrapper} from 'facade/collection'; +import {DirectiveParser} from 'core/compiler/pipeline/directive_parser'; +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {CompileStep} from 'core/compiler/pipeline/compile_step'; +import {CompileElement} from 'core/compiler/pipeline/compile_element'; +import {CompileControl} from 'core/compiler/pipeline/compile_control'; +import {DOM} from 'facade/dom'; +import {Component} from 'core/annotations/component'; +import {Decorator} from 'core/annotations/decorator'; +import {Template} from 'core/annotations/template'; +import {TemplateConfig} from 'core/annotations/template_config'; +import {Reflector} from 'core/compiler/reflector'; + +export function main() { + describe('DirectiveParser', () => { + var reflector, directives; + + beforeEach( () => { + reflector = new Reflector(); + directives = [SomeDecorator, SomeTemplate, SomeTemplate2, SomeComponent, SomeComponent2]; + }); + + function createPipeline(propertyBindings = null) { + var annotatedDirectives = ListWrapper.create(); + for (var i=0; i { + if (isPresent(propertyBindings)) { + current.propertyBindings = propertyBindings; + } + }), new DirectiveParser(annotatedDirectives)]); + } + + it('should not add directives if they are not used', () => { + var results = createPipeline().process(createElement('
')); + expect(results[0].decoratorDirectives).toBe(null); + expect(results[0].componentDirective).toBe(null); + expect(results[0].templateDirective).toBe(null); + }); + + it('should detect directives in attributes', () => { + var results = createPipeline().process(createElement('
')); + expect(results[0].decoratorDirectives).toEqual([reflector.annotatedType(SomeDecorator)]); + expect(results[0].templateDirective).toEqual(reflector.annotatedType(SomeTemplate)); + expect(results[0].componentDirective).toEqual(reflector.annotatedType(SomeComponent)); + }); + + it('should detect directives in property bindings', () => { + var pipeline = createPipeline(MapWrapper.createFromStringMap({ + 'some-decor': 'someExpr', + 'some-templ': 'someExpr', + 'some-comp': 'someExpr' + })); + var results = pipeline.process(createElement('
')); + expect(results[0].decoratorDirectives).toEqual([reflector.annotatedType(SomeDecorator)]); + expect(results[0].templateDirective).toEqual(reflector.annotatedType(SomeTemplate)); + expect(results[0].componentDirective).toEqual(reflector.annotatedType(SomeComponent)); + }); + + describe('errors', () => { + + it('should not allow multiple template directives on the same element', () => { + expect( () => { + createPipeline().process( + createElement('
') + ); + }).toThrowError('Only one template directive per element is allowed!'); + }); + + it('should not allow multiple component directives on the same element', () => { + expect( () => { + createPipeline().process( + createElement('
') + ); + }).toThrowError('Only one component directive per element is allowed!'); + }); + }); + + }); +} + +class MockStep extends CompileStep { + constructor(process) { + this.processClosure = process; + } + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + this.processClosure(parent, current, control); + } +} + +@Decorator({ + selector: '[some-decor]' +}) +class SomeDecorator {} + +@Template({ + selector: '[some-templ]' +}) +class SomeTemplate {} + +@Template({ + selector: '[some-templ2]' +}) +class SomeTemplate2 {} + +@Component({ + selector: '[some-comp]' +}) +class SomeComponent {} + +@Component({ + selector: '[some-comp2]' +}) +class SomeComponent2 {} + +@Component({ + template: new TemplateConfig({ + directives: [SomeDecorator, SomeTemplate, SomeTemplate2, SomeComponent, SomeComponent2] + }) +}) +class MyComp {} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} diff --git a/modules/core/test/compiler/pipeline/element_binder_builder_spec.js b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js new file mode 100644 index 0000000000..b393987497 --- /dev/null +++ b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js @@ -0,0 +1,274 @@ +import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; +import {isPresent} from 'facade/lang'; +import {DOM} from 'facade/dom'; +import {ListWrapper, MapWrapper} from 'facade/collection'; + +import {ElementBinderBuilder} from 'core/compiler/pipeline/element_binder_builder'; +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {CompileElement} from 'core/compiler/pipeline/compile_element'; +import {CompileStep} from 'core/compiler/pipeline/compile_step' +import {CompileControl} from 'core/compiler/pipeline/compile_control'; + +import {Decorator} from 'core/annotations/decorator'; +import {Template} from 'core/annotations/template'; +import {Component} from 'core/annotations/component'; +import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view'; +import {ProtoElementInjector} from 'core/compiler/element_injector'; +import {Reflector} from 'core/compiler/reflector'; + +import {ProtoWatchGroup} from 'change_detection/watch_group'; +import {Parser} from 'change_detection/parser/parser'; +import {Lexer} from 'change_detection/parser/lexer'; +import {ClosureMap} from 'change_detection/parser/closure_map'; +import {ChangeDetector} from 'change_detection/change_detector'; + +export function main() { + describe('ElementBinderBuilder', () => { + var evalContext, view, changeDetector; + + function createPipeline({textNodeBindings, propertyBindings, directives, protoElementInjector}={}) { + var reflector = new Reflector(); + var closureMap = new ClosureMap(); + return new CompilePipeline([ + new MockStep((parent, current, control) => { + if (isPresent(current.element.getAttribute('viewroot'))) { + current.isViewRoot = true; + current.inheritedProtoView = new ProtoView(current.element, new ProtoWatchGroup()); + } else if (isPresent(parent)) { + current.inheritedProtoView = parent.inheritedProtoView; + } else { + current.inheritedProtoView = null; + } + var hasBinding = false; + if (isPresent(current.element.getAttribute('text-binding'))) { + current.textNodeBindings = textNodeBindings; + hasBinding = true; + } + if (isPresent(current.element.getAttribute('prop-binding'))) { + current.propertyBindings = propertyBindings; + hasBinding = true; + } + if (isPresent(protoElementInjector)) { + current.inheritedProtoElementInjector = protoElementInjector; + } else { + current.inheritedProtoElementInjector = null; + } + if (isPresent(current.element.getAttribute('directives'))) { + hasBinding = true; + for (var i=0; i { + var pipeline = createPipeline(); + var results = pipeline.process(createElement('
')); + var pv = results[0].inheritedProtoView; + + expect(pv.elementBinders.length).toBe(0); + }); + + it('should create an ElementBinder for elements that have bindings', () => { + var pipeline = createPipeline(); + var results = pipeline.process(createElement('
')); + var pv = results[0].inheritedProtoView; + + expect(pv.elementBinders.length).toBe(2); + expect(pv.elementBinders[1]).not.toBe(pv.elementBinders[0]); + }); + + it('should inherit ElementBinders to children that have no bindings', () => { + var pipeline = createPipeline(); + var results = pipeline.process(createElement('
')); + var pv = results[0].inheritedProtoView; + + expect(pv.elementBinders.length).toBe(1); + expect(results[0].inheritedElementBinder).toBe(results[1].inheritedElementBinder); + }); + + it('should store the current protoElementInjector', () => { + var directives = [SomeSimpleDirective]; + var protoElementInjector = new ProtoElementInjector(null, 0, directives); + + var pipeline = createPipeline({protoElementInjector: protoElementInjector, directives: directives}); + var results = pipeline.process(createElement('
')); + var pv = results[0].inheritedProtoView; + + expect(pv.elementBinders[0].protoElementInjector).toBe(protoElementInjector); + }); + + it('should bind text nodes', () => { + var textNodeBindings = MapWrapper.create(); + MapWrapper.set(textNodeBindings, 0, 'prop1'); + MapWrapper.set(textNodeBindings, 2, 'prop2'); + var pipeline = createPipeline({textNodeBindings: textNodeBindings}); + var results = pipeline.process(createElement('
{{}}{{}}
')); + var pv = results[0].inheritedProtoView; + + expect(sortArr(pv.elementBinders[0].textNodeIndices)).toEqual([0, 2]); + + instantiateView(pv); + evalContext.prop1 = 'a'; + evalContext.prop2 = 'b'; + changeDetector.detectChanges(); + + expect(view.nodes[0].childNodes[0].nodeValue).toEqual('a'); + expect(view.nodes[0].childNodes[2].nodeValue).toEqual('b'); + }); + + it('should bind element properties', () => { + var propertyBindings = MapWrapper.createFromStringMap({ + 'elprop1': 'prop1', + 'elprop2': 'prop2' + }); + var pipeline = createPipeline({propertyBindings: propertyBindings}); + var results = pipeline.process(createElement('
')); + var pv = results[0].inheritedProtoView; + + expect(pv.elementBinders[0].hasElementPropertyBindings).toBe(true); + + instantiateView(pv); + evalContext.prop1 = 'a'; + evalContext.prop2 = 'b'; + changeDetector.detectChanges(); + + expect(DOM.getProperty(view.nodes[0], 'elprop1')).toEqual('a'); + expect(DOM.getProperty(view.nodes[0], 'elprop2')).toEqual('b'); + }); + + it('should bind directive properties', () => { + var propertyBindings = MapWrapper.createFromStringMap({ + 'boundprop1': 'prop1', + 'boundprop2': 'prop2', + 'boundprop3': 'prop3' + }); + var directives = [SomeDecoratorDirective, SomeTemplateDirective, SomeComponentDirective]; + var protoElementInjector = new ProtoElementInjector(null, 0, directives); + var pipeline = createPipeline({ + propertyBindings: propertyBindings, + directives: directives, + protoElementInjector: protoElementInjector + }); + var results = pipeline.process(createElement('
')); + var pv = results[0].inheritedProtoView; + + instantiateView(pv); + evalContext.prop1 = 'a'; + evalContext.prop2 = 'b'; + evalContext.prop3 = 'c'; + changeDetector.detectChanges(); + + expect(view.elementInjectors[0].get(SomeDecoratorDirective).decorProp).toBe('a'); + expect(view.elementInjectors[0].get(SomeTemplateDirective).templProp).toBe('b'); + expect(view.elementInjectors[0].get(SomeComponentDirective).compProp).toBe('c'); + }); + + it('should bind directive properties for sibling elements', () => { + var propertyBindings = MapWrapper.createFromStringMap({ + 'boundprop1': 'prop1' + }); + var directives = [SomeDecoratorDirective]; + var protoElementInjector = new ProtoElementInjector(null, 0, directives); + var pipeline = createPipeline({ + propertyBindings: propertyBindings, + directives: directives, + protoElementInjector: protoElementInjector + }); + var results = pipeline.process( + createElement('
'+ + '
')); + var pv = results[0].inheritedProtoView; + + instantiateView(pv); + evalContext.prop1 = 'a'; + changeDetector.detectChanges(); + + expect(view.elementInjectors[1].get(SomeDecoratorDirective).decorProp).toBe('a'); + }); + + describe('errors', () => { + + it('should throw if there is no element property bindings for a directive property binding', () => { + var pipeline = createPipeline({propertyBindings: MapWrapper.create(), directives: [SomeDecoratorDirective]}); + expect( () => { + pipeline.process(createElement('
')); + }).toThrowError('No element binding found for property boundprop1 which is required by directive SomeDecoratorDirective'); + }); + + }); + + }); + +} + +@Decorator() +class SomeSimpleDirective { +} + +@Decorator({ + bind: {'boundprop1': 'decorProp'} +}) +class SomeDecoratorDirective { + constructor() { + this.decorProp = null; + } +} + +@Template({ + bind: {'boundprop2': 'templProp'} +}) +class SomeTemplateDirective { + constructor() { + this.templProp = null; + } +} + +@Component({ + bind: {'boundprop3': 'compProp'} +}) +class SomeComponentDirective { + constructor() { + this.compProp = null; + } +} + +class Context { + constructor() { + this.prop1 = null; + this.prop2 = null; + this.prop3 = null; + } +} + +class MockStep extends CompileStep { + constructor(process) { + this.processClosure = process; + } + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + this.processClosure(parent, current, control); + } +} + +function sortArr(arr) { + var arr2 = ListWrapper.clone(arr); + arr2.sort(); + return arr2; +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} + diff --git a/modules/core/test/compiler/pipeline/element_binding_marker_spec.js b/modules/core/test/compiler/pipeline/element_binding_marker_spec.js new file mode 100644 index 0000000000..8f38697b85 --- /dev/null +++ b/modules/core/test/compiler/pipeline/element_binding_marker_spec.js @@ -0,0 +1,105 @@ +import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; +import {isPresent} from 'facade/lang'; +import {DOM} from 'facade/dom'; +import {MapWrapper} from 'facade/collection'; + +import {ElementBindingMarker} from 'core/compiler/pipeline/element_binding_marker'; +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {CompileElement} from 'core/compiler/pipeline/compile_element'; +import {CompileStep} from 'core/compiler/pipeline/compile_step' +import {CompileControl} from 'core/compiler/pipeline/compile_control'; +import {Reflector} from 'core/compiler/reflector'; +import {Template} from 'core/annotations/template'; +import {Decorator} from 'core/annotations/decorator'; +import {Component} from 'core/annotations/component'; + +export function main() { + describe('ElementBindingMarker', () => { + + function createPipeline({textNodeBindings, propertyBindings, directives}={}) { + var reflector = new Reflector(); + return new CompilePipeline([ + new MockStep((parent, current, control) => { + if (isPresent(textNodeBindings)) { + current.textNodeBindings = textNodeBindings; + } + if (isPresent(propertyBindings)) { + current.propertyBindings = propertyBindings; + } + if (isPresent(directives)) { + for (var i=0; i { + var results = createPipeline().process(createElement('
')); + assertBinding(results[0], false); + }); + + it('should mark elements with text node bindings', () => { + var textNodeBindings = MapWrapper.create(); + MapWrapper.set(textNodeBindings, 0, 'expr'); + var results = createPipeline({textNodeBindings: textNodeBindings}).process(createElement('
')); + assertBinding(results[0], true); + }); + + it('should mark elements with property bindings', () => { + var propertyBindings = MapWrapper.createFromStringMap({'a': 'expr'}); + var results = createPipeline({propertyBindings: propertyBindings}).process(createElement('
')); + assertBinding(results[0], true); + }); + + it('should mark elements with decorator directives', () => { + var results = createPipeline({ + directives: [SomeDecoratorDirective] + }).process(createElement('
')); + assertBinding(results[0], true); + }); + + it('should mark elements with template directives', () => { + var results = createPipeline({ + directives: [SomeTemplateDirective] + }).process(createElement('
')); + assertBinding(results[0], true); + }); + + it('should mark elements with component directives', () => { + var results = createPipeline({ + directives: [SomeComponentDirective] + }).process(createElement('
')); + assertBinding(results[0], true); + }); + + }); +} + +function assertBinding(pipelineElement, shouldBePresent) { + expect(pipelineElement.hasBindings).toBe(shouldBePresent); + expect(DOM.hasClass(pipelineElement.element, 'ng-binding')).toBe(shouldBePresent); +} + +class MockStep extends CompileStep { + constructor(process) { + this.processClosure = process; + } + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + this.processClosure(parent, current, control); + } +} + +@Template() +class SomeTemplateDirective {} + +@Component() +class SomeComponentDirective {} + +@Decorator() +class SomeDecoratorDirective {} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} \ No newline at end of file diff --git a/modules/core/test/compiler/pipeline/pipeline_spec.js b/modules/core/test/compiler/pipeline/pipeline_spec.js new file mode 100644 index 0000000000..15efa04b0c --- /dev/null +++ b/modules/core/test/compiler/pipeline/pipeline_spec.js @@ -0,0 +1,165 @@ +import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; +import {ListWrapper} from 'facade/collection'; +import {DOM} from 'facade/dom'; +import {isPresent, NumberWrapper} from 'facade/lang'; + +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {CompileElement} from 'core/compiler/pipeline/compile_element'; +import {CompileStep} from 'core/compiler/pipeline/compile_step' +import {CompileControl} from 'core/compiler/pipeline/compile_control'; + +export function main() { + describe('compile_pipeline', () => { + var logs, pipeline, loggingStep; + + beforeEach( () => { + logs = []; + loggingStep = new LoggingStep(logs); + }); + + it('should walk the tree in depth first order including template contents', () => { + var element = createElement('
'); + + 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']); + }); + + describe('control.addParent', () => { + it('should wrap the underlying DOM element', () => { + var element = createElement('
'); + var pipeline = new CompilePipeline([ + createWrapperStep('wrap0', []) + ]); + pipeline.process(element); + + expect(DOM.getOuterHTML(element)).toEqual('
'); + }); + + it('should report the new parent to the following processor and the result', () => { + var element = createElement('
'); + 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 { + var element = createElement('
'); + 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 { + var element = createElement('
'); + 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 { + var element = createElement('
'); + 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 { + logEntry(log, parent, current); + }); +} + +function createWrapperStep(wrapperId, log) { + var nextElementId = 0; + return new MockStep((parent, current, control) => { + var parentCountStr = current.element.getAttribute(wrapperId); + if (isPresent(parentCountStr)) { + var parentCount = NumberWrapper.parseInt(parentCountStr, 10); + while (parentCount > 0) { + control.addParent(new CompileElement(createElement(``))); + parentCount--; + } + } + logEntry(log, parent, current); + }); +} + +function resultIdLog(result) { + var idLog = []; + ListWrapper.forEach(result, (current) => { + logEntry(idLog, null, current); + }); + return idLog; +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} diff --git a/modules/core/test/compiler/pipeline/property_binding_parser_spec.js b/modules/core/test/compiler/pipeline/property_binding_parser_spec.js new file mode 100644 index 0000000000..f061be1472 --- /dev/null +++ b/modules/core/test/compiler/pipeline/property_binding_parser_spec.js @@ -0,0 +1,27 @@ +import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; +import {PropertyBindingParser} from 'core/compiler/pipeline/property_binding_parser'; +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {DOM} from 'facade/dom'; +import {MapWrapper} from 'facade/collection'; + +export function main() { + describe('PropertyBindingParser', () => { + function createPipeline() { + return new CompilePipeline([new PropertyBindingParser()]); + } + + it('should detect [] syntax', () => { + var results = createPipeline().process(createElement('
')); + expect(MapWrapper.get(results[0].propertyBindings, 'a')).toEqual('b'); + }); + + it('should detect bind- syntax', () => { + var results = createPipeline().process(createElement('
')); + expect(MapWrapper.get(results[0].propertyBindings, 'a')).toEqual('b'); + }); + }); +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} diff --git a/modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js b/modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js new file mode 100644 index 0000000000..d08656b046 --- /dev/null +++ b/modules/core/test/compiler/pipeline/proto_element_injector_builder_spec.js @@ -0,0 +1,142 @@ +import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; +import {isPresent, isBlank} from 'facade/lang'; +import {DOM} from 'facade/dom'; +import {ListWrapper} from 'facade/collection'; + +import {ProtoElementInjectorBuilder} from 'core/compiler/pipeline/proto_element_injector_builder'; +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {CompileElement} from 'core/compiler/pipeline/compile_element'; +import {CompileStep} from 'core/compiler/pipeline/compile_step' +import {CompileControl} from 'core/compiler/pipeline/compile_control'; +import {ProtoView} from 'core/compiler/view'; +import {Reflector} from 'core/compiler/reflector'; +import {Template} from 'core/annotations/template'; +import {Decorator} from 'core/annotations/decorator'; +import {Component} from 'core/annotations/component'; +import {ProtoElementInjector} from 'core/compiler/element_injector'; + +export function main() { + describe('ProtoElementInjectorBuilder', () => { + var protoElementInjectorBuilder, protoView; + beforeEach( () => { + protoElementInjectorBuilder = new TestableProtoElementInjectorBuilder(); + protoView = new ProtoView(null, null); + }); + + function createPipeline(directives = null) { + if (isBlank(directives)) { + directives = []; + } + var reflector = new Reflector(); + return new CompilePipeline([new MockStep((parent, current, control) => { + if (isPresent(current.element.getAttribute('viewroot'))) { + current.isViewRoot = true; + } + if (isPresent(current.element.getAttribute('directives'))) { + for (var i=0; i { + var results = createPipeline().process(createElement('
')); + expect(results[0].inheritedProtoElementInjector).toBe(null); + }); + + it('should create a ProtoElementInjector for elements with directives', () => { + var directives = [SomeDecoratorDirective, SomeTemplateDirective, SomeComponentDirective]; + var results = createPipeline(directives).process(createElement('
')); + assertProtoElementInjector(results[0].inheritedProtoElementInjector, null, 0, directives); + }); + + it('should use the next ElementBinder index as index of the ProtoElementInjector', () => { + // just adding some indices.. + ListWrapper.push(protoView.elementBinders, null); + ListWrapper.push(protoView.elementBinders, null); + var directives = [SomeDecoratorDirective]; + var results = createPipeline(directives).process(createElement('
')); + assertProtoElementInjector( + results[0].inheritedProtoElementInjector, null, protoView.elementBinders.length, directives); + }); + + it('should inherit the ProtoElementInjector down to children without directives', () => { + var directives = [SomeDecoratorDirective, SomeTemplateDirective, SomeComponentDirective]; + var results = createPipeline(directives).process(createElement('
')); + assertProtoElementInjector(results[0].inheritedProtoElementInjector, null, 0, directives); + assertProtoElementInjector(results[1].inheritedProtoElementInjector, null, 0, directives); + }); + + it('should use the ProtoElementInjector of the parent element as parent', () => { + var el = createElement('
'); + var directives = [SomeDecoratorDirective, SomeTemplateDirective, SomeComponentDirective]; + var results = createPipeline(directives).process(el); + assertProtoElementInjector(results[2].inheritedProtoElementInjector, + results[0].inheritedProtoElementInjector, 0, directives); + }); + + it('should use a null parent for viewRoots', () => { + var el = createElement('
'); + var directives = [SomeDecoratorDirective, SomeTemplateDirective, SomeComponentDirective]; + var results = createPipeline(directives).process(el); + assertProtoElementInjector(results[1].inheritedProtoElementInjector, null, 0, directives); + }); + + it('should use a null parent if there is an intermediate viewRoot', () => { + var el = createElement('
'); + var directives = [SomeDecoratorDirective, SomeTemplateDirective, SomeComponentDirective]; + var results = createPipeline(directives).process(el); + assertProtoElementInjector(results[2].inheritedProtoElementInjector, null, 0, directives); + }); + }); +} + + +class TestableProtoElementInjectorBuilder extends ProtoElementInjectorBuilder { + constructor() { + this.debugObjects = []; + } + findArgsFor(protoElementInjector:ProtoElementInjector) { + for (var i=0; i { + function createPipeline() { + return new CompilePipeline([new MockStep((parent, current, control) => { + if (isPresent(current.element.getAttribute('viewroot'))) { + current.isViewRoot = true; + } + current.inheritedElementBinder = new ElementBinder(null); + }), new ProtoViewBuilder()]); + } + + it('should not create a ProtoView when the isViewRoot flag is not set', () => { + var results = createPipeline().process(createElement('
')); + expect(results[0].inheritedProtoView).toBe(null); + }); + + it('should create a ProtoView when the isViewRoot flag is set', () => { + var viewRootElement = createElement('
'); + var results = createPipeline().process(viewRootElement); + expect(results[0].inheritedProtoView.element).toBe(viewRootElement); + }); + + it('should inherit the ProtoView down to children that have no isViewRoot set', () => { + var viewRootElement = createElement('
'); + var results = createPipeline().process(viewRootElement); + expect(results[0].inheritedProtoView.element).toBe(viewRootElement); + expect(results[1].inheritedProtoView.element).toBe(viewRootElement); + }); + + it('should save ProtoView into elementBinder of parent element', () => { + var el = createElement('
'); + var results = createPipeline().process(el); + expect(results[1].inheritedElementBinder.nestedProtoView).toBe(results[2].inheritedProtoView); + }); + + describe('errors', () => { + + it('should not allow multiple nested ProtoViews for the same parent element', () => { + var el = createElement('
'); + expect( () => { + createPipeline().process(el); + }).toThrowError('Only one nested view per element is allowed'); + }); + + }); + + }); +} + +class MockStep extends CompileStep { + constructor(process) { + this.processClosure = process; + } + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + this.processClosure(parent, current, control); + } +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} \ No newline at end of file diff --git a/modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js b/modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js new file mode 100644 index 0000000000..e73cc181f5 --- /dev/null +++ b/modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js @@ -0,0 +1,31 @@ +import {describe, beforeEach, expect, it, iit, ddescribe} from 'test_lib/test_lib'; +import {TextInterpolationParser} from 'core/compiler/pipeline/text_interpolation_parser'; +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {DOM} from 'facade/dom'; +import {MapWrapper} from 'facade/collection'; + +export function main() { + describe('TextInterpolationParser', () => { + function createPipeline() { + return new CompilePipeline([new TextInterpolationParser()]); + } + + it('should find text interpolation in normal elements', () => { + var results = createPipeline().process(createElement('
{{expr1}}{{expr2}}
')); + var bindings = results[0].textNodeBindings; + expect(MapWrapper.get(bindings, 0)).toEqual("''+expr1+''"); + expect(MapWrapper.get(bindings, 2)).toEqual("''+expr2+''"); + }); + + it('should find text interpolation in template elements', () => { + var results = createPipeline().process(createElement('')); + var bindings = results[0].textNodeBindings; + expect(MapWrapper.get(bindings, 0)).toEqual("''+expr1+''"); + expect(MapWrapper.get(bindings, 2)).toEqual("''+expr2+''"); + }); + }); +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} diff --git a/modules/core/test/compiler/pipeline/view_splitter_spec.js b/modules/core/test/compiler/pipeline/view_splitter_spec.js new file mode 100644 index 0000000000..dd5c9fe4b4 --- /dev/null +++ b/modules/core/test/compiler/pipeline/view_splitter_spec.js @@ -0,0 +1,135 @@ +import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; +import {isPresent} from 'facade/lang'; +import {MapWrapper} from 'facade/collection'; + +import {ViewSplitter} from 'core/compiler/pipeline/view_splitter'; +import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; +import {CompileElement} from 'core/compiler/pipeline/compile_element'; +import {CompileStep} from 'core/compiler/pipeline/compile_step' +import {CompileControl} from 'core/compiler/pipeline/compile_control'; +import {DOM, TemplateElement} from 'facade/dom'; +import {Reflector} from 'core/compiler/reflector'; +import {Template} from 'core/annotations/template'; +import {Decorator} from 'core/annotations/decorator'; +import {Component} from 'core/annotations/component'; + +export function main() { + describe('ViewSplitter', () => { + + function createPipeline({textNodeBindings, propertyBindings, directives}={}) { + var reflector = new Reflector(); + return new CompilePipeline([ + new MockStep((parent, current, control) => { + if (isPresent(current.element.getAttribute('tmpl'))) { + current.addDirective(reflector.annotatedType(SomeTemplateDirective)); + if (isPresent(textNodeBindings)) { + current.textNodeBindings = textNodeBindings; + } + if (isPresent(propertyBindings)) { + current.propertyBindings = propertyBindings; + } + if (isPresent(directives)) { + for (var i=0; i { + if (useTemplateElement) { + rootElement = createElement('
'); + } else { + rootElement = createElement('
'); + } + }); + + it('should insert an empty