From 746f85a621a49852efb07210cb3857b8865d5e53 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Fri, 30 Jan 2015 09:43:21 +0100 Subject: [PATCH] feat(compiler, ShadowDom): adds TemplateLoader using XHR. Also adds css shimming for emulated shadow dom and makes the shadowDom strategy global to the application. --- karma-dart.conf.js | 3 +- .../src/core/annotations/annotations.js | 6 - modules/angular2/src/core/application.js | 17 +- .../angular2/src/core/compiler/compiler.js | 123 +++-- .../src/core/compiler/directive_metadata.js | 9 +- .../compiler/directive_metadata_reader.js | 23 +- .../core/compiler/pipeline/default_steps.js | 6 +- .../compiler/pipeline/directive_parser.js | 1 + .../compiler/pipeline/proto_view_builder.js | 8 +- .../src/core/compiler/shadow_dom.dart | 9 - .../angular2/src/core/compiler/shadow_dom.es6 | 5 - .../compiler/shadow_dom_emulation/shim_css.js | 431 ++++++++++++++++++ .../src/core/compiler/shadow_dom_strategy.js | 81 +++- .../src/core/compiler/template_loader.js | 50 +- modules/angular2/src/core/compiler/view.js | 19 +- modules/angular2/src/core/compiler/xhr/xhr.js | 7 + .../src/core/compiler/xhr/xhr_impl.dart | 12 + .../src/core/compiler/xhr/xhr_impl.es6 | 27 ++ modules/angular2/src/facade/collection.dart | 4 +- modules/angular2/src/facade/dom.dart | 24 +- modules/angular2/src/facade/dom.es6 | 19 +- modules/angular2/src/facade/lang.dart | 14 +- modules/angular2/src/facade/lang.es6 | 17 +- modules/angular2/src/mock/xhr_mock.js | 108 +++++ .../test/core/compiler/compiler_spec.js | 137 +++++- .../directive_metadata_reader_spec.js | 45 +- .../test/core/compiler/integration_spec.js | 31 +- .../pipeline/directive_parser_spec.js | 2 +- .../pipeline/element_binder_builder_spec.js | 27 +- .../pipeline/element_binding_marker_spec.js | 2 +- .../proto_element_injector_builder_spec.js | 2 +- .../pipeline/proto_view_builder_spec.js | 5 +- .../shadow_dom_emulation_integration_spec.js | 27 +- .../core/compiler/shadow_dom/shim_css_spec.js | 102 +++++ .../core/compiler/shadow_dom_strategy_spec.js | 62 +++ .../core/compiler/template_loader_spec.js | 90 ++++ .../angular2/test/core/compiler/view_spec.js | 89 ++-- .../test/core/compiler/viewport_spec.js | 5 +- .../angular2/test/directives/ng_if_spec.js | 10 +- .../test/directives/ng_non_bindable_spec.js | 11 +- .../test/directives/ng_repeat_spec.js | 11 +- .../test/directives/ng_switch_spec.js | 11 +- .../angular2/test/forms/integration_spec.js | 5 +- modules/angular2/test/mock/xhr_mock_spec.js | 111 +++++ modules/benchmarks/e2e_test/compiler_perf.es6 | 18 +- .../src/compiler/compiler_benchmark.js | 13 +- modules/benchmarks/src/tree/tree_benchmark.js | 24 +- .../examples/src/hello_world/index_static.js | 23 +- 48 files changed, 1583 insertions(+), 303 deletions(-) delete mode 100644 modules/angular2/src/core/compiler/shadow_dom.dart delete mode 100644 modules/angular2/src/core/compiler/shadow_dom.es6 create mode 100644 modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js create mode 100644 modules/angular2/src/core/compiler/xhr/xhr.js create mode 100644 modules/angular2/src/core/compiler/xhr/xhr_impl.dart create mode 100644 modules/angular2/src/core/compiler/xhr/xhr_impl.es6 create mode 100644 modules/angular2/src/mock/xhr_mock.js create mode 100644 modules/angular2/test/core/compiler/shadow_dom/shim_css_spec.js create mode 100644 modules/angular2/test/core/compiler/shadow_dom_strategy_spec.js create mode 100644 modules/angular2/test/core/compiler/template_loader_spec.js create mode 100644 modules/angular2/test/mock/xhr_mock_spec.js diff --git a/karma-dart.conf.js b/karma-dart.conf.js index ecb758cb36..2e66057a6a 100644 --- a/karma-dart.conf.js +++ b/karma-dart.conf.js @@ -50,7 +50,8 @@ module.exports = function(config) { '/packages/directives': 'http://localhost:9877/base/modules/directives', '/packages/facade': 'http://localhost:9877/base/modules/facade', '/packages/forms': 'http://localhost:9877/base/modules/forms', - '/packages/test_lib': 'http://localhost:9877/base/modules/test_lib' + '/packages/test_lib': 'http://localhost:9877/base/modules/test_lib', + '/packages/mock': 'http://localhost:9877/base/modules/mock', }, preprocessors: { diff --git a/modules/angular2/src/core/annotations/annotations.js b/modules/angular2/src/core/annotations/annotations.js index 1ebe834064..285b74ce4e 100644 --- a/modules/angular2/src/core/annotations/annotations.js +++ b/modules/angular2/src/core/annotations/annotations.js @@ -1,8 +1,6 @@ import {ABSTRACT, CONST, normalizeBlank} from 'angular2/src/facade/lang'; import {List} from 'angular2/src/facade/collection'; import {TemplateConfig} from './template_config'; -import {ShadowDomStrategy} from '../compiler/shadow_dom'; - @ABSTRACT() export class Directive { @@ -40,7 +38,6 @@ export class Component extends Directive { lightDomServices:any; //List; shadowDomServices:any; //List; componentServices:any; //List; - shadowDom:any; //ShadowDomStrategy; lifecycle:any; //List @CONST() @@ -52,7 +49,6 @@ export class Component extends Directive { shadowDomServices, componentServices, implementsTypes, - shadowDom, lifecycle }:{ selector:String, @@ -62,7 +58,6 @@ export class Component extends Directive { shadowDomServices:List, componentServices:List, implementsTypes:List, - shadowDom:ShadowDomStrategy, lifecycle:List }={}) { @@ -78,7 +73,6 @@ export class Component extends Directive { this.lightDomServices = lightDomServices; this.shadowDomServices = shadowDomServices; this.componentServices = componentServices; - this.shadowDom = shadowDom; this.lifecycle = lifecycle; } } diff --git a/modules/angular2/src/core/application.js b/modules/angular2/src/core/application.js index b6e7a641c7..7ff2cb3b66 100644 --- a/modules/angular2/src/core/application.js +++ b/modules/angular2/src/core/application.js @@ -12,6 +12,9 @@ import {List, ListWrapper} from 'angular2/src/facade/collection'; import {PromiseWrapper} from 'angular2/src/facade/async'; import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone'; import {LifeCycle} from 'angular2/src/core/life_cycle/life_cycle'; +import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; +import {XHR} from 'angular2/src/core/compiler/xhr/xhr'; +import {XHRImpl} from 'angular2/src/core/compiler/xhr/xhr_impl'; var _rootInjector: Injector; @@ -24,7 +27,9 @@ var _rootBindings = [ TemplateLoader, DirectiveMetadataReader, Parser, - Lexer + Lexer, + bind(ShadowDomStrategy).toValue(new NativeShadowDomStrategy()), + bind(XHR).toValue(new XHRImpl()), ]; export var appViewToken = new OpaqueToken('AppView'); @@ -53,11 +58,12 @@ function _injectorBindings(appComponentType) { }, [appComponentAnnotatedTypeToken, appDocumentToken]), bind(appViewToken).toAsyncFactory((changeDetection, compiler, injector, appElement, - appComponentAnnotatedType) => { + appComponentAnnotatedType, strategy) => { return compiler.compile(appComponentAnnotatedType.type, null).then( (protoView) => { - var appProtoView = ProtoView.createRootProtoView(protoView, - appElement, appComponentAnnotatedType, changeDetection.createProtoChangeDetector('root')); + var appProtoView = ProtoView.createRootProtoView(protoView, appElement, + appComponentAnnotatedType, changeDetection.createProtoChangeDetector('root'), + strategy); // The light Dom of the app element is not considered part of // the angular application. Thus the context and lightDomInjector are // empty. @@ -65,7 +71,8 @@ function _injectorBindings(appComponentType) { view.hydrate(injector, null, new Object()); return view; }); - }, [ChangeDetection, Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken]), + }, [ChangeDetection, Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken, + ShadowDomStrategy]), bind(appChangeDetectorToken).toFactory((rootView) => rootView.changeDetector, [appViewToken]), diff --git a/modules/angular2/src/core/compiler/compiler.js b/modules/angular2/src/core/compiler/compiler.js index 64ec556235..00a32c9ab6 100644 --- a/modules/angular2/src/core/compiler/compiler.js +++ b/modules/angular2/src/core/compiler/compiler.js @@ -1,6 +1,6 @@ -import {Type, FIELD, isBlank, isPresent, BaseException, stringify} from 'angular2/src/facade/lang'; +import {Type, isBlank, isPresent, BaseException, normalizeBlank, stringify} from 'angular2/src/facade/lang'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; -import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; +import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection'; import {DOM, Element} from 'angular2/src/facade/dom'; import {ChangeDetection, Parser} from 'angular2/change_detection'; @@ -14,6 +14,7 @@ import {TemplateLoader} from './template_loader'; import {DirectiveMetadata} from './directive_metadata'; import {Component} from '../annotations/annotations'; import {Content} from './shadow_dom_emulation/content_tag'; +import {ShadowDomStrategy} from './shadow_dom_strategy'; /** * Cache that stores the ProtoView of the template of a component. @@ -31,15 +32,11 @@ export class CompilerCache { get(component:Type):ProtoView { var result = MapWrapper.get(this._cache, component); - if (isBlank(result)) { - // need to normalize undefined to null so that type checking passes :-( - return null; - } - return result; + return normalizeBlank(result); } clear() { - this._cache = MapWrapper.create(); + MapWrapper.clear(this._cache); } } @@ -53,59 +50,107 @@ export class Compiler { _parser:Parser; _compilerCache:CompilerCache; _changeDetection:ChangeDetection; + _templateLoader:TemplateLoader; + _compiling:Map; + _shadowDomStrategy: ShadowDomStrategy; + _shadowDomDirectives: List; - constructor(changeDetection:ChangeDetection, templateLoader:TemplateLoader, reader: DirectiveMetadataReader, parser:Parser, cache:CompilerCache) { + constructor(changeDetection:ChangeDetection, + templateLoader:TemplateLoader, + reader: DirectiveMetadataReader, + parser:Parser, + cache:CompilerCache, + shadowDomStrategy: ShadowDomStrategy) { this._changeDetection = changeDetection; this._reader = reader; this._parser = parser; this._compilerCache = cache; + this._templateLoader = templateLoader; + this._compiling = MapWrapper.create(); + this._shadowDomStrategy = shadowDomStrategy; + this._shadowDomDirectives = []; + var types = shadowDomStrategy.polyfillDirectives(); + for (var i = 0; i < types.length; i++) { + ListWrapper.push(this._shadowDomDirectives, reader.read(types[i])); + } } createSteps(component:DirectiveMetadata):List { - var dirs = ListWrapper.map(component.componentDirectives, (d) => this._reader.read(d)); - return createDefaultSteps(this._changeDetection, this._parser, component, dirs); + var directives = [] + var cmpDirectives = ListWrapper.map(component.componentDirectives, (d) => this._reader.read(d)); + directives = ListWrapper.concat(directives, cmpDirectives); + directives = ListWrapper.concat(directives, this._shadowDomDirectives); + return createDefaultSteps(this._changeDetection, this._parser, component, directives, + this._shadowDomStrategy); } compile(component:Type, templateRoot:Element = null):Promise { - var templateCache = null; - // TODO load all components that have urls - // transitively via the _templateLoader and store them in templateCache - - return PromiseWrapper.resolve(this.compileAllLoaded( - templateCache, this._reader.read(component), templateRoot) - ); + return this._compile(this._reader.read(component), templateRoot); } - // public so that we can compile in sync in performance tests. - compileAllLoaded(templateCache, component:DirectiveMetadata, templateRoot:Element = null):ProtoView { - var rootProtoView = this._compilerCache.get(component.type); - if (isPresent(rootProtoView)) { - return rootProtoView; + _compile(cmpMetadata: DirectiveMetadata, templateRoot:Element = null) { + var pvCached = this._compilerCache.get(cmpMetadata.type); + if (isPresent(pvCached)) { + // The component has already been compiled into a ProtoView, + // returns a resolved Promise. + return PromiseWrapper.resolve(pvCached); } - if (isBlank(templateRoot)) { - // TODO: read out the cache if templateRoot = null. Could contain: - // - templateRoot string - // - precompiled template - // - ProtoView - var annotation:any = component.annotation; - templateRoot = DOM.createTemplate(annotation.template.inline); + var pvPromise = MapWrapper.get(this._compiling, cmpMetadata.type); + if (isPresent(pvPromise)) { + // The component is already being compiled, attach to the existing Promise + // instead of re-compiling the component. + // It happens when a template references a component multiple times. + return pvPromise; } - var pipeline = new CompilePipeline(this.createSteps(component)); - var compileElements = pipeline.process(templateRoot); - rootProtoView = compileElements[0].inheritedProtoView; - // Save the rootProtoView before we recurse so that we are able - // to compile components that use themselves in their template. - this._compilerCache.set(component.type, rootProtoView); + var tplPromise = isBlank(templateRoot) ? + this._templateLoader.load(cmpMetadata) : + PromiseWrapper.resolve(templateRoot); - for (var i=0; i this._compileTemplate(el, cmpMetadata), + (_) => { throw new BaseException(`Failed to load the template for ${stringify(cmpMetadata.type)}`) } + ); + + MapWrapper.set(this._compiling, cmpMetadata.type, pvPromise); + + return pvPromise; + } + + _compileTemplate(template: Element, cmpMetadata): Promise { + this._shadowDomStrategy.processTemplate(template, cmpMetadata); + var pipeline = new CompilePipeline(this.createSteps(cmpMetadata)); + var compileElements = pipeline.process(template); + var protoView = compileElements[0].inheritedProtoView; + + // Populate the cache before compiling the nested components, + // so that components can reference themselves in their template. + this._compilerCache.set(cmpMetadata.type, protoView); + MapWrapper.delete(this._compiling, cmpMetadata.type); + + // Compile all the components from the template + var componentPromises = []; + for (var i = 0; i < compileElements.length; i++) { var ce = compileElements[i]; if (isPresent(ce.componentDirective)) { - ce.inheritedElementBinder.nestedProtoView = this.compileAllLoaded(templateCache, ce.componentDirective, null); + var componentPromise = this._compileNestedProtoView(ce); + ListWrapper.push(componentPromises, componentPromise); } } - return rootProtoView; + // The protoView is resolved after all the components in the template have been compiled. + return PromiseWrapper.then(PromiseWrapper.all(componentPromises), + (_) => protoView, + (e) => { throw new BaseException(`${e} -> Failed to compile ${stringify(cmpMetadata.type)}`) } + ); + } + + _compileNestedProtoView(ce: CompileElement):Promise { + var pvPromise = this._compile(ce.componentDirective); + pvPromise.then(function(protoView) { + ce.inheritedElementBinder.nestedProtoView = protoView; + }); + return pvPromise; } } diff --git a/modules/angular2/src/core/compiler/directive_metadata.js b/modules/angular2/src/core/compiler/directive_metadata.js index ca46f7927d..b300266c64 100644 --- a/modules/angular2/src/core/compiler/directive_metadata.js +++ b/modules/angular2/src/core/compiler/directive_metadata.js @@ -1,7 +1,7 @@ import {Type} from 'angular2/src/facade/lang'; -import {Directive} from '../annotations/annotations' +import {Directive} from 'angular2/src/core/annotations/annotations' import {List} from 'angular2/src/facade/collection' -import {ShadowDomStrategy} from './shadow_dom'; +import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; /** * Combination of a type with the Directive annotation @@ -9,14 +9,13 @@ import {ShadowDomStrategy} from './shadow_dom'; export class DirectiveMetadata { type:Type; annotation:Directive; - shadowDomStrategy:ShadowDomStrategy; componentDirectives:List; - constructor(type:Type, annotation:Directive, shadowDomStrategy:ShadowDomStrategy, + constructor(type:Type, + annotation:Directive, componentDirectives:List) { this.annotation = annotation; this.type = type; - this.shadowDomStrategy = shadowDomStrategy; this.componentDirectives = componentDirectives; } } diff --git a/modules/angular2/src/core/compiler/directive_metadata_reader.js b/modules/angular2/src/core/compiler/directive_metadata_reader.js index 9fcf16feb8..8d599d627b 100644 --- a/modules/angular2/src/core/compiler/directive_metadata_reader.js +++ b/modules/angular2/src/core/compiler/directive_metadata_reader.js @@ -3,7 +3,7 @@ import {List, ListWrapper} from 'angular2/src/facade/collection'; import {Directive, Component} from '../annotations/annotations'; import {DirectiveMetadata} from './directive_metadata'; import {reflector} from 'angular2/src/reflection/reflection'; -import {ShadowDom, ShadowDomStrategy, ShadowDomNative} from './shadow_dom'; +import {ShadowDom, ShadowDomStrategy, ShadowDomNative} from './shadow_dom_strategy'; export class DirectiveMetadataReader { read(type:Type):DirectiveMetadata { @@ -13,36 +13,23 @@ export class DirectiveMetadataReader { var annotation = annotations[i]; if (annotation instanceof Component) { - var shadowDomStrategy = this.parseShadowDomStrategy(annotation); return new DirectiveMetadata( type, annotation, - shadowDomStrategy, - this.componentDirectivesMetadata(annotation, shadowDomStrategy) + this.componentDirectivesMetadata(annotation) ); } if (annotation instanceof Directive) { - return new DirectiveMetadata(type, annotation, null, null); + return new DirectiveMetadata(type, annotation, null); } } } throw new BaseException(`No Directive annotation found on ${stringify(type)}`); } - parseShadowDomStrategy(annotation:Component):ShadowDomStrategy{ - return isPresent(annotation.shadowDom) ? annotation.shadowDom : ShadowDomNative; - } - - componentDirectivesMetadata(annotation:Component, shadowDomStrategy:ShadowDomStrategy):List { - var polyDirs = shadowDomStrategy.polyfillDirectives(); + componentDirectivesMetadata(annotation:Component):List { var template = annotation.template; - var templateDirs = isPresent(template) && isPresent(template.directives) ? template.directives : []; - - var res = []; - res = ListWrapper.concat(res, templateDirs) - res = ListWrapper.concat(res, polyDirs) - - return res; + return isPresent(template) && isPresent(template.directives) ? template.directives : []; } } diff --git a/modules/angular2/src/core/compiler/pipeline/default_steps.js b/modules/angular2/src/core/compiler/pipeline/default_steps.js index d34b194dd9..34594e7677 100644 --- a/modules/angular2/src/core/compiler/pipeline/default_steps.js +++ b/modules/angular2/src/core/compiler/pipeline/default_steps.js @@ -10,6 +10,7 @@ import {ProtoViewBuilder} from './proto_view_builder'; import {ProtoElementInjectorBuilder} from './proto_element_injector_builder'; import {ElementBinderBuilder} from './element_binder_builder'; import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; +import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {stringify} from 'angular2/src/facade/lang'; /** @@ -21,7 +22,8 @@ export function createDefaultSteps( changeDetection:ChangeDetection, parser:Parser, compiledComponent: DirectiveMetadata, - directives: List) { + directives: List, + shadowDomStrategy: ShadowDomStrategy) { var compilationUnit = stringify(compiledComponent.type); @@ -31,7 +33,7 @@ export function createDefaultSteps( new DirectiveParser(directives), new TextInterpolationParser(parser, compilationUnit), new ElementBindingMarker(), - new ProtoViewBuilder(changeDetection), + new ProtoViewBuilder(changeDetection, shadowDomStrategy), new ProtoElementInjectorBuilder(), new ElementBinderBuilder() ]; diff --git a/modules/angular2/src/core/compiler/pipeline/directive_parser.js b/modules/angular2/src/core/compiler/pipeline/directive_parser.js index fe4f86bbe3..b5491c5e47 100644 --- a/modules/angular2/src/core/compiler/pipeline/directive_parser.js +++ b/modules/angular2/src/core/compiler/pipeline/directive_parser.js @@ -10,6 +10,7 @@ import {Component} from '../../annotations/annotations'; import {CompileStep} from './compile_step'; import {CompileElement} from './compile_element'; import {CompileControl} from './compile_control'; +import {ShadowDomStrategy} from '../shadow_dom_strategy'; /** * Parses the directives on a single element. Assumes ViewSplitter has already created diff --git a/modules/angular2/src/core/compiler/pipeline/proto_view_builder.js b/modules/angular2/src/core/compiler/pipeline/proto_view_builder.js index 88fb0617df..b7e797b7e7 100644 --- a/modules/angular2/src/core/compiler/pipeline/proto_view_builder.js +++ b/modules/angular2/src/core/compiler/pipeline/proto_view_builder.js @@ -7,6 +7,7 @@ import {ChangeDetection} from 'angular2/change_detection'; import {CompileStep} from './compile_step'; import {CompileElement} from './compile_element'; import {CompileControl} from './compile_control'; +import {ShadowDomStrategy} from '../shadow_dom_strategy'; /** * Creates ProtoViews and forwards variable bindings from parent to children. @@ -22,7 +23,9 @@ import {CompileControl} from './compile_control'; */ export class ProtoViewBuilder extends CompileStep { changeDetection:ChangeDetection; - constructor(changeDetection:ChangeDetection) { + _shadowDomStrategy:ShadowDomStrategy; + constructor(changeDetection:ChangeDetection, shadowDomStrategy:ShadowDomStrategy) { + this._shadowDomStrategy = shadowDomStrategy; this.changeDetection = changeDetection; } @@ -30,7 +33,8 @@ export class ProtoViewBuilder extends CompileStep { var inheritedProtoView = null; if (current.isViewRoot) { var protoChangeDetector = this.changeDetection.createProtoChangeDetector('dummy'); - inheritedProtoView = new ProtoView(current.element, protoChangeDetector); + inheritedProtoView = new ProtoView(current.element, protoChangeDetector, + this._shadowDomStrategy); if (isPresent(parent)) { if (isPresent(parent.inheritedElementBinder.nestedProtoView)) { throw new BaseException('Only one nested view per element is allowed'); diff --git a/modules/angular2/src/core/compiler/shadow_dom.dart b/modules/angular2/src/core/compiler/shadow_dom.dart deleted file mode 100644 index 47df6809a5..0000000000 --- a/modules/angular2/src/core/compiler/shadow_dom.dart +++ /dev/null @@ -1,9 +0,0 @@ -library angular.core.compiler.shadow_dom; - -//TODO: merge this file with shadow_dom.es6 when the traspiler support creating const globals - -import './shadow_dom_strategy.dart'; -export './shadow_dom_strategy.dart'; - -const ShadowDomEmulated = const EmulatedShadowDomStrategy(); -const ShadowDomNative = const NativeShadowDomStrategy(); \ No newline at end of file diff --git a/modules/angular2/src/core/compiler/shadow_dom.es6 b/modules/angular2/src/core/compiler/shadow_dom.es6 deleted file mode 100644 index 485d799768..0000000000 --- a/modules/angular2/src/core/compiler/shadow_dom.es6 +++ /dev/null @@ -1,5 +0,0 @@ -import {EmulatedShadowDomStrategy, NativeShadowDomStrategy} from './shadow_dom_strategy'; -export * from './shadow_dom_strategy'; - -export var ShadowDomEmulated = new EmulatedShadowDomStrategy(); -export var ShadowDomNative = new NativeShadowDomStrategy(); \ No newline at end of file diff --git a/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js b/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js new file mode 100644 index 0000000000..7e4f0af870 --- /dev/null +++ b/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js @@ -0,0 +1,431 @@ +import {StringWrapper, RegExpWrapper, isPresent, BaseException, int} from 'angular2/src/facade/lang'; +import {List, ListWrapper} from 'angular2/src/facade/collection'; + +export function shimCssText(css: string, tag: string) { + return new CssShim(tag).shimCssText(css); +} + +var _HOST_RE = RegExpWrapper.create(':host', 'i'); +var _HOST_TOKEN = '-host-element'; +var _HOST_TOKEN_RE = RegExpWrapper.create('-host-element'); +var _PAREN_SUFFIX = ')(?:\\((' + + '(?:\\([^)(]*\\)|[^)(]*)+?' + + ')\\))?([^,{]*)'; +var _COLON_HOST_RE = RegExpWrapper.create(`(${_HOST_TOKEN}${_PAREN_SUFFIX}`, 'im'); + +var _POLYFILL_NON_STRICT = 'polyfill-non-strict'; +var _POLYFILL_UNSCOPED_NEXT_SELECTOR = 'polyfill-unscoped-next-selector'; +var _POLYFILL_NEXT_SELECTOR = 'polyfill-next-selector'; +var _CONTENT_RE = RegExpWrapper.create('[^}]*content:[\\s]*[\'"](.*?)[\'"][;\\s]*[^}]*}', 'im'); +var _COMBINATORS = [ + RegExpWrapper.create('/shadow/', 'i'), + RegExpWrapper.create('/shadow-deep/', 'i'), + RegExpWrapper.create('::shadow', 'i'), + RegExpWrapper.create('/deep/', 'i'), +]; +var _COLON_SELECTORS = RegExpWrapper.create('(' + _HOST_TOKEN + ')(\\(.*\\))?(.*)', 'i'); +var _SELECTOR_SPLITS = [' ', '>', '+', '~']; +var _SIMPLE_SELECTORS = RegExpWrapper.create('([^:]*)(:*)(.*)', 'i'); +var _IS_SELECTORS = RegExpWrapper.create('\\[is=[\'"]([^\\]]*)[\'"]\\]', 'i'); + +var _$EOF = 0; +var _$LBRACE = 123; +var _$RBRACE = 125; +var _$TAB = 9; +var _$SPACE = 32; +var _$NBSP = 160; + +export class CssShim { + _tag: string; + _attr: string; + + constructor(tag: string) { + this._tag = tag; + this._attr = `[${tag}]`; + } + + shimCssText(css: string): string { + var preprocessed = this.convertColonHost(css); + var rules = this.cssToRules(preprocessed); + return this.scopeRules(rules); + } + + convertColonHost(css: string):string { + css = StringWrapper.replaceAll(css, _HOST_RE, _HOST_TOKEN); + + var partReplacer = function(host, part, suffix) { + part = StringWrapper.replaceAll(part, _HOST_TOKEN_RE, ''); + return `${host}${part}${suffix}`; + } + + return StringWrapper.replaceAllMapped(css, _COLON_HOST_RE, function(m) { + var base = _HOST_TOKEN; + var inParens = m[2]; + var rest = m[3]; + + if (isPresent(inParens)) { + var srcParts = inParens.split(','); + var dstParts = []; + + for (var i = 0; i < srcParts.length; i++) { + var part = srcParts[i].trim(); + if (part.length > 0) { + ListWrapper.push(dstParts, partReplacer(base, part, rest)); + } + } + + return ListWrapper.join(dstParts, ','); + } else { + return `${base}${rest}`; + } + }); + } + + cssToRules(css: string): List<_Rule> { + return new _Parser(css).parse(); + } + + scopeRules(rules: List<_Rule>): string { + var scopedRules = []; + var prevRule = null; + + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + if (isPresent(prevRule) && + prevRule.selectorText == _POLYFILL_NON_STRICT) { + ListWrapper.push(scopedRules, this.scopeNonStrictMode(rule)); + + } else if (isPresent(prevRule) && + prevRule.selectorText == _POLYFILL_UNSCOPED_NEXT_SELECTOR) { + var content = this.extractContent(prevRule); + var r = new _Rule(content, rule.body, null); + ListWrapper.push(scopedRules, this.ruleToString(r)); + + } else if (isPresent(prevRule) && + prevRule.selectorText == _POLYFILL_NEXT_SELECTOR) { + + var content = this.extractContent(prevRule); + var r = new _Rule(content, rule.body, null); + ListWrapper.push(scopedRules, this.scopeStrictMode(r)) + + } else if (rule.selectorText != _POLYFILL_NON_STRICT && + rule.selectorText != _POLYFILL_UNSCOPED_NEXT_SELECTOR && + rule.selectorText != _POLYFILL_NEXT_SELECTOR) { + ListWrapper.push(scopedRules, this.scopeStrictMode(rule)); + } + prevRule = rule; + } + + return ListWrapper.join(scopedRules, '\n'); + } + + extractContent(rule: _Rule): string { + var match = RegExpWrapper.firstMatch(_CONTENT_RE, rule.body); + return isPresent(match) ? match[1] : ''; + } + + ruleToString(rule: _Rule): string { + return `${rule.selectorText} ${rule.body}`; + } + + scopeStrictMode(rule: _Rule): string { + if (rule.hasNestedRules()) { + var selector = rule.selectorText; + var rules = this.scopeRules(rule.rules); + return `${selector} {\n${rules}\n}`; + } + + var scopedSelector = this.scopeSelector(rule.selectorText, true); + var scopedBody = rule.body; + return `${scopedSelector} ${scopedBody}`; + } + + scopeNonStrictMode(rule: _Rule): string { + var scopedSelector = this.scopeSelector(rule.selectorText, false); + var scopedBody = rule.body; + return `${scopedSelector} ${scopedBody}`; + } + + scopeSelector(selector: string, strict: boolean) { + var parts = this.replaceCombinators(selector).split(','); + var scopedParts = []; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + var sel = this.scopeSimpleSelector(part.trim(), strict); + ListWrapper.push(scopedParts, sel) + } + return ListWrapper.join(scopedParts, ', '); + } + + replaceCombinators(selector: string): string { + for (var i = 0; i < _COMBINATORS.length; i++) { + var combinator = _COMBINATORS[i]; + selector = StringWrapper.replaceAll(selector, combinator, ''); + } + + return selector; + } + + scopeSimpleSelector(selector: string, strict: boolean) { + if (StringWrapper.contains(selector, _HOST_TOKEN)) { + return this.replaceColonSelectors(selector); + } else if (strict) { + return this.insertTagToEverySelectorPart(selector); + } else { + return `${this._tag} ${selector}`; + } + } + + replaceColonSelectors(css: string): string { + return StringWrapper.replaceAllMapped(css, _COLON_SELECTORS, (m) => { + var selectorInParens; + if (isPresent(m[2])) { + var len = selectorInParens.length; + selectorInParens = StringWrapper.substring(selectorInParens, 1, len - 1); + } else { + selectorInParens = ''; + } + var rest = m[3]; + return `${this._tag}${selectorInParens}${rest}`; + }); + } + + insertTagToEverySelectorPart(selector: string): string { + selector = this.handleIsSelector(selector); + + for (var i = 0; i < _SELECTOR_SPLITS.length; i++) { + var split = _SELECTOR_SPLITS[i]; + var parts = selector.split(split); + for (var j = 0; j < parts.length; j++) { + parts[j] = this.insertAttrSuffixIntoSelectorPart(parts[j].trim()); + } + selector = parts.join(split); + } + return selector; + } + + insertAttrSuffixIntoSelectorPart(p: string): string { + var shouldInsert = p.length > 0 && + !ListWrapper.contains(_SELECTOR_SPLITS, p) && + !StringWrapper.contains(p, this._attr); + return shouldInsert ? this.insertAttr(p) : p; + } + + insertAttr(selector: string): string { + return StringWrapper.replaceAllMapped(selector, _SIMPLE_SELECTORS, (m) => { + var basePart = m[1]; + var colonPart = m[2]; + var rest = m[3]; + return (m[0].length > 0) ? `${basePart}${this._attr}${colonPart}${rest}` : ''; + }); + } + + handleIsSelector(selector: string) { + return StringWrapper.replaceAllMapped(selector, _IS_SELECTORS, function(m) { + return m[1]; + }); + } +} + +class _Token { + string: string; + type: string; + + constructor(string: string, type: string) { + this.string = string; + this.type = type; + } +} + +var _EOF_TOKEN = new _Token(null, null); + +class _Lexer { + peek: int; + index: int; + input: string; + length: int; + + constructor(input: string) { + this.input = input; + this.length = input.length; + this.index = -1; + this.advance(); + } + + parse(): List<_Token> { + var tokens = []; + var token = this.scanToken(); + while (token !== _EOF_TOKEN) { + ListWrapper.push(tokens, token); + token = this.scanToken(); + } + return tokens; + } + + scanToken(): _Token { + this.skipWhitespace(); + if (this.peek === _$EOF) return _EOF_TOKEN; + if (this.isBodyEnd(this.peek)) { + this.advance(); + return new _Token('}', 'rparen'); + } + if (this.isMedia(this.peek)) return this.scanMedia(); + if (this.isSelector(this.peek)) return this.scanSelector(); + if (this.isBodyStart(this.peek)) return this.scanBody(); + + return _EOF_TOKEN; + } + + isSelector(v: int): boolean { + return !this.isBodyStart(v) && v !== _$EOF; + } + + isBodyStart(v: int): boolean { + return v === _$LBRACE; + } + + isBodyEnd(v: int): boolean { + return v === _$RBRACE; + } + + isMedia(v: int): boolean { + return v === 64; // @ -> 64 + } + + isWhitespace(v: int): boolean { + return (v >= _$TAB && v <= _$SPACE) || (v == _$NBSP) + } + + skipWhitespace() { + while (this.isWhitespace(this.peek)) { + if (++this.index >= this.length) { + this.peek = _$EOF; + return; + } else { + this.peek = StringWrapper.charCodeAt(this.input, this.index); + } + } + } + + scanSelector(): _Token { + var start = this.index; + this.advance(); + while (this.isSelector(this.peek)) { + this.advance(); + } + var selector = StringWrapper.substring(this.input, start, this.index); + return new _Token(selector.trim(), 'selector'); + } + + scanBody(): _Token { + var start = this.index; + this.advance(); + while (!this.isBodyEnd(this.peek)) { + this.advance(); + } + this.advance(); + var body = StringWrapper.substring(this.input, start, this.index); + return new _Token(body, 'body'); + } + + scanMedia(): _Token { + var start = this.index; + this.advance(); + while (!this.isBodyStart(this.peek)) { + this.advance(); + } + var media = StringWrapper.substring(this.input, start, this.index); + this.advance(); // skip "{" + return new _Token(media, 'media'); + } + + advance() { + this.index++; + if (this.index >= this.length) { + this.peek = _$EOF; + } else { + this.peek = StringWrapper.charCodeAt(this.input, this.index); + } + } +} + +class _Parser { + tokens: List<_Token>; + currentIndex: int; + + constructor(input: string) { + this.tokens = new _Lexer(input).parse(); + this.currentIndex = -1; + } + + parse(): List<_Rule> { + var rules = []; + var rule; + while (isPresent(rule = this.parseRule())) { + ListWrapper.push(rules, rule); + } + return rules; + } + + parseRule(): _Rule { + try { + if (this.getNext().type === 'media') { + return this.parseMedia(); + } else { + return this.parseCssRule(); + } + } catch (e) { + return null; + } + } + + parseMedia(): _Rule { + this.advance('media'); + var media = this.getCurrent().string; + var rules = []; + while (this.getNext().type !== 'rparen') { + ListWrapper.push(rules, this.parseCssRule()); + } + this.advance('rparen'); + return new _Rule(media.trim(), null, rules); + } + + parseCssRule() { + this.advance('selector'); + var selector = this.getCurrent().string; + this.advance('body'); + var body = this.getCurrent().string; + return new _Rule(selector, body, null); + } + + advance(expected: string) { + this.currentIndex++; + if (this.getCurrent().type !== expected) { + throw new BaseException(`Unexpected token "${this.getCurrent().type}". Expected "${expected}"`); + } + } + + getNext(): _Token { + return this.tokens[this.currentIndex + 1]; + } + + getCurrent(): _Token { + return this.tokens[this.currentIndex]; + } +} + +export class _Rule { + selectorText: string; + body: string; + rules: List<_Rule>; + + constructor(selectorText: string, body: string, rules: List<_Rule>) { + this.selectorText = selectorText; + this.body = body; + this.rules = rules; + } + + hasNestedRules() { + return isPresent(this.rules); + } +} diff --git a/modules/angular2/src/core/compiler/shadow_dom_strategy.js b/modules/angular2/src/core/compiler/shadow_dom_strategy.js index e086a99f95..f98f893018 100644 --- a/modules/angular2/src/core/compiler/shadow_dom_strategy.js +++ b/modules/angular2/src/core/compiler/shadow_dom_strategy.js @@ -1,19 +1,29 @@ -import {CONST} from 'angular2/src/facade/lang'; -import {DOM, Element} from 'angular2/src/facade/dom'; -import {List} from 'angular2/src/facade/collection'; +import {Type, isBlank, isPresent} from 'angular2/src/facade/lang'; +import {DOM, Element, StyleElement} from 'angular2/src/facade/dom'; +import {List, ListWrapper} from 'angular2/src/facade/collection'; import {View} from './view'; import {Content} from './shadow_dom_emulation/content_tag'; import {LightDom} from './shadow_dom_emulation/light_dom'; +import {DirectiveMetadata} from './directive_metadata'; +import {shimCssText} from './shadow_dom_emulation/shim_css'; export class ShadowDomStrategy { - @CONST() constructor() {} attachTemplate(el:Element, view:View){} constructLightDom(lightDomView:View, shadowDomView:View, el:Element){} - polyfillDirectives():List{ return null; }; + polyfillDirectives():List{ return null; } + processTemplate(template: Element, cmpMetadata: DirectiveMetadata) { return null; } } export class EmulatedShadowDomStrategy extends ShadowDomStrategy { - @CONST() constructor() {} + _styleHost: Element; + + constructor(styleHost: Element = null) { + if (isBlank(styleHost)) { + styleHost = DOM.defaultDoc().head; + } + this._styleHost = styleHost; + } + attachTemplate(el:Element, view:View){ DOM.clearNodes(el); moveViewNodesIntoParent(el, view); @@ -26,10 +36,26 @@ export class EmulatedShadowDomStrategy extends ShadowDomStrategy { polyfillDirectives():List { return [Content]; } + + processTemplate(template: Element, cmpMetadata: DirectiveMetadata) { + var templateRoot = DOM.templateAwareRoot(template); + var attrName = cmpMetadata.annotation.selector; + + // Shim CSS for emulated shadow DOM and attach the styles do the document head + var styles = _detachStyles(templateRoot); + for (var i = 0; i < styles.length; i++) { + var style = styles[i]; + var processedCss = shimCssText(DOM.getText(style), attrName); + DOM.setText(style, processedCss); + } + _attachStyles(this._styleHost, styles); + + // Update the DOM to trigger the CSS + _addAttributeToChildren(templateRoot, attrName); + } } export class NativeShadowDomStrategy extends ShadowDomStrategy { - @CONST() constructor() {} attachTemplate(el:Element, view:View){ moveViewNodesIntoParent(el.createShadowRoot(), view); } @@ -41,10 +67,49 @@ export class NativeShadowDomStrategy extends ShadowDomStrategy { polyfillDirectives():List { return []; } + + processTemplate(template: Element, cmpMetadata: DirectiveMetadata) { + return template; + } } function moveViewNodesIntoParent(parent, view) { for (var i = 0; i < view.nodes.length; ++i) { DOM.appendChild(parent, view.nodes[i]); } -} \ No newline at end of file +} + +// TODO(vicb): union types: el is an Element or a Document Fragment +function _detachStyles(el): List { + var nodeList = DOM.querySelectorAll(el, 'style'); + var styles = []; + for (var i = 0; i < nodeList.length; i++) { + var style = DOM.remove(nodeList[i]); + ListWrapper.push(styles, style); + } + return styles; +} + +// Move the styles as the first children of the template +function _attachStyles(el: Element, styles: List) { + var firstChild = DOM.firstChild(el); + for (var i = styles.length - 1; i >= 0; i--) { + var style = styles[i]; + if (isPresent(firstChild)) { + DOM.insertBefore(firstChild, style); + } else { + DOM.appendChild(el, style); + } + firstChild = style; + } +} + +// TODO(vicb): union types: el is an Element or a Document Fragment +function _addAttributeToChildren(el, attrName:string) { + // TODO(vicb): currently the code crashes when the attrName is not an el selector + var children = DOM.querySelectorAll(el, "*"); + for (var i = 0; i < children.length; i++) { + var child = children[i]; + DOM.setAttribute(child, attrName, ''); + } +} diff --git a/modules/angular2/src/core/compiler/template_loader.js b/modules/angular2/src/core/compiler/template_loader.js index b61c18778b..b68a9e0ae3 100644 --- a/modules/angular2/src/core/compiler/template_loader.js +++ b/modules/angular2/src/core/compiler/template_loader.js @@ -1,11 +1,51 @@ -import {Promise} from 'angular2/src/facade/async'; -//import {Document} from 'angular2/src/facade/dom'; +import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; +import {isBlank, isPresent, BaseException, stringify} from 'angular2/src/facade/lang'; +import {TemplateElement, DOM} from 'angular2/src/facade/dom'; +import {StringMapWrapper} from 'angular2/src/facade/collection'; + +import {TemplateConfig} from 'angular2/src/core/annotations/template_config'; +import {Component} from 'angular2/src/core/annotations/annotations'; + +import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; + +import {XHR} from './xhr/xhr'; /** * Strategy to load component templates. */ export class TemplateLoader { - load(url:string):Promise { - return null; + _xhr: XHR; + _cache; + + constructor(xhr: XHR) { + this._xhr = xhr; + this._cache = StringMapWrapper.create(); } -} \ No newline at end of file + + load(cmpMetadata: DirectiveMetadata):Promise { + var annotation:Component = cmpMetadata.annotation; + var tplConfig:TemplateConfig = annotation.template; + + if (isPresent(tplConfig.inline)) { + var template = DOM.createTemplate(tplConfig.inline); + return PromiseWrapper.resolve(template); + } + + if (isPresent(tplConfig.url)) { + var url = tplConfig.url; + var promise = StringMapWrapper.get(this._cache, url); + + if (isBlank(promise)) { + promise = this._xhr.get(url).then(function (html) { + var template = DOM.createTemplate(html); + return template; + }); + StringMapWrapper.set(this._cache, url, promise); + } + + return promise; + } + + throw new BaseException(`No template configured for component ${stringify(cmpMetadata.type)}`); + } +} diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index 317bb4df92..e7b62aee8d 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -15,6 +15,7 @@ import {ViewPort} from './viewport'; import {OnChange} from './interfaces'; import {Content} from './shadow_dom_emulation/content_tag'; import {LightDom, DestinationLightDom} from './shadow_dom_emulation/light_dom'; +import {ShadowDomStrategy} from './shadow_dom_strategy'; const NG_BINDING_CLASS = 'ng-binding'; const NG_BINDING_CLASS_SELECTOR = '.ng-binding'; @@ -256,9 +257,11 @@ export class ProtoView { instantiateInPlace:boolean; rootBindingOffset:int; isTemplateElement:boolean; + shadowDomStrategy: ShadowDomStrategy; constructor( template:Element, - protoChangeDetector:ProtoChangeDetector) { + protoChangeDetector:ProtoChangeDetector, + shadowDomStrategy: ShadowDomStrategy) { this.element = template; this.elementBinders = []; this.variableBindings = MapWrapper.create(); @@ -270,6 +273,7 @@ export class ProtoView { this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0; this.isTemplateElement = this.element instanceof TemplateElement; + this.shadowDomStrategy = shadowDomStrategy; } // TODO(rado): hostElementInjector should be moved to hydrate phase. @@ -353,11 +357,12 @@ export class ProtoView { var lightDom = null; var bindingPropagationConfig = null; if (isPresent(binder.componentDirective)) { + var strategy = this.shadowDomStrategy; var childView = binder.nestedProtoView.instantiate(elementInjector); view.changeDetector.addChild(childView.changeDetector); - lightDom = binder.componentDirective.shadowDomStrategy.constructLightDom(view, childView, element); - binder.componentDirective.shadowDomStrategy.attachTemplate(element, childView); + lightDom = strategy.constructLightDom(view, childView, element); + strategy.attachTemplate(element, childView); bindingPropagationConfig = new BindingPropagationConfig(view.changeDetector); @@ -497,12 +502,14 @@ export class ProtoView { // and the component template is already compiled into protoView. // Used for bootstrapping. static createRootProtoView(protoView: ProtoView, - insertionElement, rootComponentAnnotatedType: DirectiveMetadata, - protoChangeDetector:ProtoChangeDetector + insertionElement, + rootComponentAnnotatedType: DirectiveMetadata, + protoChangeDetector:ProtoChangeDetector, + shadowDomStrategy: ShadowDomStrategy ): ProtoView { DOM.addClass(insertionElement, 'ng-binding'); - var rootProtoView = new ProtoView(insertionElement, protoChangeDetector); + var rootProtoView = new ProtoView(insertionElement, protoChangeDetector, shadowDomStrategy); rootProtoView.instantiateInPlace = true; var binder = rootProtoView.bindElement( new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true)); diff --git a/modules/angular2/src/core/compiler/xhr/xhr.js b/modules/angular2/src/core/compiler/xhr/xhr.js new file mode 100644 index 0000000000..08884b578b --- /dev/null +++ b/modules/angular2/src/core/compiler/xhr/xhr.js @@ -0,0 +1,7 @@ +import {Promise} from 'angular2/src/facade/async'; + +export class XHR { + get(url: string): Promise { + return null; + } +} diff --git a/modules/angular2/src/core/compiler/xhr/xhr_impl.dart b/modules/angular2/src/core/compiler/xhr/xhr_impl.dart new file mode 100644 index 0000000000..4217a0877e --- /dev/null +++ b/modules/angular2/src/core/compiler/xhr/xhr_impl.dart @@ -0,0 +1,12 @@ +import 'dart:async'; +import 'dart:html'; +import './xhr.dart' show XHR; + +class XHRImpl extends XHR { + Future get(String url) { + return HttpRequest.request(url).then( + (HttpRequest request) => request.responseText, + onError: (Error e) => throw 'Failed to load $url' + ); + } +} diff --git a/modules/angular2/src/core/compiler/xhr/xhr_impl.es6 b/modules/angular2/src/core/compiler/xhr/xhr_impl.es6 new file mode 100644 index 0000000000..730f1917b2 --- /dev/null +++ b/modules/angular2/src/core/compiler/xhr/xhr_impl.es6 @@ -0,0 +1,27 @@ +import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; +import {XHR} from './xhr'; + +export class XHRImpl extends XHR { + get(url: string): Promise { + var completer = PromiseWrapper.completer(); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'text'; + + xhr.onload = function() { + var status = xhr.status; + if (200 <= status && status <= 300) { + completer.complete(xhr.responseText); + } else { + completer.reject(`Failed to load ${url}`); + } + }; + + xhr.onerror = function() { + completer.reject(`Failed to load ${url}`); + }; + + xhr.send(); + return completer.promise; + } +} diff --git a/modules/angular2/src/facade/collection.dart b/modules/angular2/src/facade/collection.dart index 3a4234f956..2d9e111101 100644 --- a/modules/angular2/src/facade/collection.dart +++ b/modules/angular2/src/facade/collection.dart @@ -104,9 +104,7 @@ class ListWrapper { static void insert(List l, int index, value) { l.insert(index, value); } - static void removeAt(List l, int index) { - l.removeAt(index); - } + static removeAt(List l, int index) => l.removeAt(index); static void removeAll(List list, List items) { for (var i = 0; i < items.length; ++i) { list.remove(items[i]); diff --git a/modules/angular2/src/facade/dom.dart b/modules/angular2/src/facade/dom.dart index 2eefdcbc76..7e86c4bd9a 100644 --- a/modules/angular2/src/facade/dom.dart +++ b/modules/angular2/src/facade/dom.dart @@ -3,7 +3,16 @@ library angular.core.facade.dom; import 'dart:html'; import 'dart:js' show JsObject, context; -export 'dart:html' show DocumentFragment, Node, Element, TemplateElement, Text, document, location, window; +export 'dart:html' show + document, + DocumentFragment, + Element, + location, + Node, + StyleElement, + TemplateElement, + Text, + window; // TODO(tbosch): Is there a builtin one? Why is Dart // removing unknown elements by default? @@ -62,7 +71,10 @@ class DOM { static void removeChild(Element el, Node node) { node.remove(); } - static void insertBefore(Node el, Node node) { + static Element remove(Element el) { + return el..remove(); + } + static insertBefore(Node el, node) { el.parentNode.insertBefore(node, el); } static void insertAllBefore(Node el, Iterable nodes) { @@ -72,8 +84,8 @@ class DOM { el.parentNode.insertBefore(node, el.nextNode); } static String getText(Node el) => el.text; - static void setText(Text text, String value) { - text.text = value; + static void setText(Node el, String value) { + el.text = value; } static TemplateElement createTemplate(String html) { var t = new TemplateElement(); @@ -116,6 +128,10 @@ class DOM { static String getAttribute(Element element, String attribute) => element.getAttribute(attribute); + static void setAttribute(Element element, String name, String value) { + element.setAttribute(name, value); + } + static Node templateAwareRoot(Element el) => el is TemplateElement ? el.content : el; diff --git a/modules/angular2/src/facade/dom.es6 b/modules/angular2/src/facade/dom.es6 index c6b8400645..faaa1ca3ec 100644 --- a/modules/angular2/src/facade/dom.es6 +++ b/modules/angular2/src/facade/dom.es6 @@ -5,6 +5,7 @@ export var NodeList = window.NodeList; export var Text = window.Text; export var Element = window.HTMLElement; export var TemplateElement = window.HTMLTemplateElement; +export var StyleElement = window.HTMLStyleElement; export var document = window.document; export var location = window.location; export var gc = window.gc ? () => window.gc() : () => null; @@ -70,6 +71,11 @@ export class DOM { static removeChild(el, node) { el.removeChild(node); } + static remove(el: Element): Element { + var parent = el.parentNode; + parent.removeChild(el); + return el; + } static insertBefore(el, node) { el.parentNode.insertBefore(node, el); } @@ -87,8 +93,9 @@ export class DOM { static getText(el: Element) { return el.textContent; } - static setText(text:Text, value:string) { - text.nodeValue = value; + // TODO(vicb): removed Element type because it does not support StyleElement + static setText(el, value:string) { + el.textContent = value; } static createTemplate(html) { var t = document.createElement('template'); @@ -103,6 +110,11 @@ export class DOM { el.setAttribute(attrName, attrValue); return el; } + static createStyleElement(css:string, doc=document):StyleElement { + var style = doc.createElement('STYLE'); + style.innerText = css; + return style; + } static clone(node:Node) { return node.cloneNode(true); } @@ -142,6 +154,9 @@ export class DOM { static getAttribute(element:Element, attribute:string) { return element.getAttribute(attribute); } + static setAttribute(element:Element, name:string, value:string) { + element.setAttribute(name, value); + } static templateAwareRoot(el:Element):Node { return el instanceof TemplateElement ? el.content : el; } diff --git a/modules/angular2/src/facade/lang.dart b/modules/angular2/src/facade/lang.dart index 24cf5cac1e..d740c147a3 100644 --- a/modules/angular2/src/facade/lang.dart +++ b/modules/angular2/src/facade/lang.dart @@ -69,6 +69,14 @@ class StringWrapper { static String substring(String s, int start, [int end]) { return s.substring(start, end); } + + static String replaceAllMapped(String s, RegExp from, Function cb) { + return s.replaceAllMapped(from, cb); + } + + static bool contains(String s, String substr) { + return s.contains(substr); + } } class StringJoiner { @@ -102,8 +110,10 @@ class NumberWrapper { } class RegExpWrapper { - static RegExp create(String regExpStr) { - return new RegExp(regExpStr); + static RegExp create(regExpStr, [String flags = '']) { + bool multiLine = flags.contains('m'); + bool caseSensitive = !flags.contains('i'); + return new RegExp(regExpStr, multiLine: multiLine, caseSensitive: caseSensitive); } static Match firstMatch(RegExp regExp, String input) { return regExp.firstMatch(input); diff --git a/modules/angular2/src/facade/lang.es6 b/modules/angular2/src/facade/lang.es6 index 108efbcda6..e42d7e8b06 100644 --- a/modules/angular2/src/facade/lang.es6 +++ b/modules/angular2/src/facade/lang.es6 @@ -75,6 +75,16 @@ export class StringWrapper { static substring(s:string, start:int, end:int = undefined) { return s.substring(start, end); } + + static replaceAllMapped(s:string, from:RegExp, cb:Function): string { + return s.replace(from.multiple, function(...matches) { + return cb(matches); + }); + } + + static contains(s:string, substr:string): boolean { + return s.indexOf(substr) != -1; + } } export class StringJoiner { @@ -160,10 +170,11 @@ export var RegExp = assert.define('RegExp', function(obj) { }); export class RegExpWrapper { - static create(regExpStr):RegExp { + static create(regExpStr, flags:string = ''):RegExp { + flags = flags.replace(/g/g, ''); return { - multiple: new window.RegExp(regExpStr, 'g'), - single: new window.RegExp(regExpStr) + multiple: new window.RegExp(regExpStr, flags + 'g'), + single: new window.RegExp(regExpStr, flags) }; } static firstMatch(regExp, input) { diff --git a/modules/angular2/src/mock/xhr_mock.js b/modules/angular2/src/mock/xhr_mock.js new file mode 100644 index 0000000000..7c56c29942 --- /dev/null +++ b/modules/angular2/src/mock/xhr_mock.js @@ -0,0 +1,108 @@ +import {XHR} from 'angular2/src/core/compiler/xhr/xhr'; +import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection'; +import {isBlank, isPresent, normalizeBlank, BaseException} from 'angular2/src/facade/lang'; +import {PromiseWrapper, Promise} from 'angular2/src/facade/async'; + +export class XHRMock extends XHR { + _expectations: List<_Expectation>; + _definitions: Map; + _requests: List; + + constructor() { + this._expectations = []; + this._definitions = MapWrapper.create(); + this._requests = []; + } + + get(url: string): Promise { + var request = new _PendingRequest(url); + ListWrapper.push(this._requests, request); + return request.getPromise(); + } + + expect(url: string, response: string) { + var expectation = new _Expectation(url, response); + ListWrapper.push(this._expectations, expectation); + } + + when(url: string, response: string) { + MapWrapper.set(this._definitions, url, response); + } + + flush() { + if (this._requests.length === 0) { + throw new BaseException('No pending requests to flush'); + } + + do { + var request = ListWrapper.removeAt(this._requests, 0); + this._processRequest(request); + } while (this._requests.length > 0); + + this.verifyNoOustandingExpectations(); + } + + verifyNoOustandingExpectations() { + if (this._expectations.length === 0) return; + + var urls = []; + for (var i = 0; i < this._expectations.length; i++) { + var expectation = this._expectations[i]; + ListWrapper.push(urls, expectation.url); + } + + throw new BaseException(`Unsatisfied requests: ${ListWrapper.join(urls, ', ')}`); + } + + _processRequest(request: _PendingRequest) { + var url = request.url; + + if (this._expectations.length > 0) { + var expectation = this._expectations[0]; + if (expectation.url === url) { + ListWrapper.remove(this._expectations, expectation); + request.complete(expectation.response); + return; + } + } + + if (MapWrapper.contains(this._definitions, url)) { + var response = MapWrapper.get(this._definitions, url); + request.complete(normalizeBlank(response)); + return; + } + + throw new BaseException(`Unexpected request ${url}`); + } +} + +class _PendingRequest { + url: string; + completer; + + constructor(url) { + this.url = url; + this.completer = PromiseWrapper.completer(); + } + + complete(response: string) { + if (isBlank(response)) { + this.completer.reject(`Failed to load ${this.url}`); + } else { + this.completer.complete(response); + } + } + + getPromise(): Promise { + return this.completer.promise; + } +} + +class _Expectation { + url: string; + response: string; + constructor(url: string, response: string) { + this.url = url; + this.response = response; + } +} diff --git a/modules/angular2/test/core/compiler/compiler_spec.js b/modules/angular2/test/core/compiler/compiler_spec.js index f483e97dd5..097a5ba6a3 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.js +++ b/modules/angular2/test/core/compiler/compiler_spec.js @@ -1,18 +1,23 @@ -import {describe, beforeEach, it, expect, ddescribe, iit, el} from 'angular2/test_lib'; -import {DOM} from 'angular2/src/facade/dom'; -import {List} from 'angular2/src/facade/collection'; +import {describe, beforeEach, it, expect, ddescribe, iit, el, IS_DARTIUM} from 'angular2/test_lib'; +import {DOM, Element, TemplateElement} from 'angular2/src/facade/dom'; +import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection'; +import {Type, isBlank} from 'angular2/src/facade/lang'; +import {PromiseWrapper} from 'angular2/src/facade/async'; import {Compiler, CompilerCache} from 'angular2/src/core/compiler/compiler'; import {ProtoView} from 'angular2/src/core/compiler/view'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; -import {TemplateLoader} from 'angular2/src/core/compiler/template_loader'; +import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; import {Component} from 'angular2/src/core/annotations/annotations'; import {TemplateConfig} from 'angular2/src/core/annotations/template_config'; import {CompileElement} from 'angular2/src/core/compiler/pipeline/compile_element'; import {CompileStep} from 'angular2/src/core/compiler/pipeline/compile_step' import {CompileControl} from 'angular2/src/core/compiler/pipeline/compile_control'; +import {TemplateLoader} from 'angular2/src/core/compiler/template_loader'; import {Lexer, Parser, dynamicChangeDetection} from 'angular2/change_detection'; +import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; +import {XHRMock} from 'angular2/src/mock/xhr_mock'; export function main() { describe('compiler', function() { @@ -22,13 +27,19 @@ export function main() { reader = new DirectiveMetadataReader(); }); - function createCompiler(processClosure) { + function createCompiler(processClosure, strategy:ShadowDomStrategy = null, xhr: XHRMock = null) { var steps = [new MockStep(processClosure)]; - return new TestableCompiler(reader, steps); + if (isBlank(strategy)) { + strategy = new NativeShadowDomStrategy(); + } + if (isBlank(xhr)) { + xhr = new XHRMock(); + } + return new TestableCompiler(reader, steps, strategy, xhr); } it('should run the steps and return the ProtoView of the root element', (done) => { - var rootProtoView = new ProtoView(null, null); + var rootProtoView = new ProtoView(null, null, null); var compiler = createCompiler( (parent, current, control) => { current.inheritedProtoView = rootProtoView; }); @@ -41,7 +52,7 @@ export function main() { it('should use the given element', (done) => { var element = el('
'); var compiler = createCompiler( (parent, current, control) => { - current.inheritedProtoView = new ProtoView(current.element, null); + current.inheritedProtoView = new ProtoView(current.element, null, null); }); compiler.compile(MainComponent, element).then( (protoView) => { expect(protoView.element).toBe(element); @@ -51,7 +62,7 @@ export function main() { 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); + current.inheritedProtoView = new ProtoView(current.element, null, null); }); compiler.compile(MainComponent, null).then( (protoView) => { expect(DOM.getInnerHTML(protoView.element)).toEqual('inline component'); @@ -59,10 +70,27 @@ export function main() { }); }); + it('should use the shadow dom strategy to process the template', (done) => { + // TODO(vicb) test in Dart when the bug is fixed + // https://code.google.com/p/dart/issues/detail?id=18249 + if (IS_DARTIUM) { + done(); + return; + } + var templateHtml = 'processed template'; + var compiler = createCompiler((parent, current, control) => { + current.inheritedProtoView = new ProtoView(current.element, null, null); + }, new FakeShadowDomStrategy(templateHtml)); + compiler.compile(MainComponent, null).then( (protoView) => { + expect(DOM.getInnerHTML(protoView.element)).toEqual('processed template'); + done(); + }); + }); + it('should load nested components', (done) => { var mainEl = el('
'); var compiler = createCompiler( (parent, current, control) => { - current.inheritedProtoView = new ProtoView(current.element, null); + current.inheritedProtoView = new ProtoView(current.element, null, null); current.inheritedElementBinder = current.inheritedProtoView.bindElement(null); if (current.element === mainEl) { current.componentDirective = reader.read(NestedComponent); @@ -73,13 +101,12 @@ export function main() { expect(DOM.getInnerHTML(nestedView.element)).toEqual('nested component'); done(); }); - }); - it('should cache components', (done) => { + it('should cache compiled components', (done) => { var element = el('
'); var compiler = createCompiler( (parent, current, control) => { - current.inheritedProtoView = new ProtoView(current.element, null); + current.inheritedProtoView = new ProtoView(current.element, null, null); }); var firstProtoView; compiler.compile(MainComponent, element).then( (protoView) => { @@ -89,12 +116,28 @@ export function main() { expect(firstProtoView).toBe(protoView); done(); }); + }); + it('should re-use components being compiled', (done) => { + var nestedElBinders = []; + var mainEl = el('
'); + var compiler = createCompiler( (parent, current, control) => { + if (DOM.hasClass(current.element, 'nested')) { + current.inheritedProtoView = new ProtoView(current.element, null, null); + current.inheritedElementBinder = current.inheritedProtoView.bindElement(null); + current.componentDirective = reader.read(NestedComponent); + ListWrapper.push(nestedElBinders, current.inheritedElementBinder); + } + }); + compiler.compile(MainComponent, mainEl).then( (protoView) => { + expect(nestedElBinders[0].nestedProtoView).toBe(nestedElBinders[1].nestedProtoView); + done(); + }); }); it('should allow recursive components', (done) => { var compiler = createCompiler( (parent, current, control) => { - current.inheritedProtoView = new ProtoView(current.element, null); + current.inheritedProtoView = new ProtoView(current.element, null, null); current.inheritedElementBinder = current.inheritedProtoView.bindElement(null); current.componentDirective = reader.read(RecursiveComponent); }); @@ -102,13 +145,54 @@ export function main() { expect(protoView.elementBinders[0].nestedProtoView).toBe(protoView); done(); }); - }); + describe('XHR', () => { + it('should load template via xhr', (done) => { + var xhr = new XHRMock(); + xhr.expect('/parent', 'xhr'); + + var compiler = createCompiler((parent, current, control) => { + current.inheritedProtoView = new ProtoView(current.element, null, null); + }, null, xhr); + + compiler.compile(XHRParentComponent).then( (protoView) => { + expect(DOM.getInnerHTML(protoView.element)).toEqual('xhr'); + done(); + }); + + xhr.flush(); + }); + + it('should return a rejected promise when loading a template fails', (done) => { + var xhr = new XHRMock(); + xhr.expect('/parent', null); + + var compiler = createCompiler((parent, current, control) => {}, null, xhr); + + PromiseWrapper.then(compiler.compile(XHRParentComponent), + function(_) { throw 'Failure expected'; }, + function(e) { + expect(e.message).toEqual('Failed to load the template for XHRParentComponent'); + done(); + } + ); + + xhr.flush(); + }); + }); }); } + +@Component({ + template: new TemplateConfig({ + url: '/parent' + }) +}) +class XHRParentComponent {} + @Component({ template: new TemplateConfig({ inline: 'inline component' @@ -133,10 +217,18 @@ class RecursiveComponent {} class TestableCompiler extends Compiler { steps:List; - constructor(reader:DirectiveMetadataReader, steps:List) { - super(dynamicChangeDetection, null, reader, new Parser(new Lexer()), new CompilerCache()); + + constructor(reader:DirectiveMetadataReader, steps:List, strategy:ShadowDomStrategy, + xhr: XHRMock) { + super(dynamicChangeDetection, + new TemplateLoader(xhr), + reader, + new Parser(new Lexer()), + new CompilerCache(), + strategy); this.steps = steps; } + createSteps(component):List { return this.steps; } @@ -151,3 +243,14 @@ class MockStep extends CompileStep { this.processClosure(parent, current, control); } } + +class FakeShadowDomStrategy extends NativeShadowDomStrategy { + templateHtml: string; + constructor(templateHtml: string) { + this.templateHtml = templateHtml; + } + + processTemplate(template: Element, cmpMetadata: DirectiveMetadata) { + DOM.setInnerHTML(template, this.templateHtml); + } +} diff --git a/modules/angular2/test/core/compiler/directive_metadata_reader_spec.js b/modules/angular2/test/core/compiler/directive_metadata_reader_spec.js index 6cbb12918e..714d5ea376 100644 --- a/modules/angular2/test/core/compiler/directive_metadata_reader_spec.js +++ b/modules/angular2/test/core/compiler/directive_metadata_reader_spec.js @@ -3,36 +3,16 @@ import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_meta import {Decorator, Component} from 'angular2/src/core/annotations/annotations'; import {TemplateConfig} from 'angular2/src/core/annotations/template_config'; import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; -import {ShadowDomStrategy, ShadowDomNative} from 'angular2/src/core/compiler/shadow_dom'; +import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {CONST} from 'angular2/src/facade/lang'; -class FakeShadowDomStrategy extends ShadowDomStrategy { - @CONST() - constructor() {} - - polyfillDirectives() { - return [SomeDirective]; - } -} - @Decorator({ selector: 'someSelector' }) class SomeDirective { } -@Component({ - selector: 'someSelector' -}) -class ComponentWithoutExplicitShadowDomStrategy {} - -@Component({ - selector: 'someSelector', - shadowDom: new FakeShadowDomStrategy() -}) -class ComponentWithExplicitShadowDomStrategy {} - class SomeDirectiveWithoutAnnotation { } @@ -55,14 +35,14 @@ export function main() { describe("DirectiveMetadataReader", () => { var reader; - beforeEach( () => { + beforeEach(() => { reader = new DirectiveMetadataReader(); }); it('should read out the annotation', () => { var directiveMetadata = reader.read(SomeDirective); expect(directiveMetadata).toEqual( - new DirectiveMetadata(SomeDirective, new Decorator({selector: 'someSelector'}), null, null)); + new DirectiveMetadata(SomeDirective, new Decorator({selector: 'someSelector'}), null)); }); it('should throw if not matching annotation is found', () => { @@ -71,18 +51,6 @@ export function main() { }).toThrowError('No Directive annotation found on SomeDirectiveWithoutAnnotation'); }); - describe("shadow dom strategy", () => { - it('should return the provided shadow dom strategy when it is present', () => { - var directiveMetadata = reader.read(ComponentWithExplicitShadowDomStrategy); - expect(directiveMetadata.shadowDomStrategy).toBeAnInstanceOf(FakeShadowDomStrategy); - }); - - it('should return Native otherwise', () => { - var directiveMetadata = reader.read(ComponentWithoutExplicitShadowDomStrategy); - expect(directiveMetadata.shadowDomStrategy).toEqual(ShadowDomNative); - }); - }); - describe("componentDirectives", () => { it("should return an empty list when no directives specified", () => { var cmp = reader.read(ComponentWithoutDirectives); @@ -93,11 +61,6 @@ export function main() { var cmp = reader.read(ComponentWithDirectives); expect(cmp.componentDirectives).toEqual([ComponentWithoutDirectives]); }); - - it("should include directives required by the shadow DOM strategy", () => { - var cmp = reader.read(ComponentWithExplicitShadowDomStrategy); - expect(cmp.componentDirectives).toEqual([SomeDirective]); - }); }); }); -} \ No newline at end of file +} diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index 93607cabc4..0b5c752f41 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -7,7 +7,8 @@ import {Lexer, Parser, ChangeDetector, dynamicChangeDetection} from 'angular2/ch import {Compiler, CompilerCache} from 'angular2/src/core/compiler/compiler'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; -import {ShadowDomEmulated} from 'angular2/src/core/compiler/shadow_dom'; +import {NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; +import {TemplateLoader} from 'angular2/src/core/compiler/template_loader'; import {Decorator, Component, Template} from 'angular2/src/core/annotations/annotations'; import {TemplateConfig} from 'angular2/src/core/annotations/template_config'; @@ -15,13 +16,20 @@ import {TemplateConfig} from 'angular2/src/core/annotations/template_config'; import {ViewPort} from 'angular2/src/core/compiler/viewport'; import {MapWrapper} from 'angular2/src/facade/collection'; +import {XHRMock} from 'angular2/src/mock/xhr_mock'; + export function main() { describe('integration tests', function() { var compiler; beforeEach( () => { - compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), - new Parser(new Lexer()), new CompilerCache()); + compiler = new Compiler(dynamicChangeDetection, + new TemplateLoader(new XHRMock()), + new DirectiveMetadataReader(), + new Parser(new Lexer()), + new CompilerCache(), + new NativeShadowDomStrategy() + ); }); describe('react to record changes', function() { @@ -69,22 +77,6 @@ export function main() { }); }); - it('should consume element binding for class attribute', (done) => { - compiler.compile(MyComp, el('
')).then((pv) => { - createView(pv); - - ctx.boolProp = true; - cd.detectChanges(); - expect(view.nodes[0].className).toEqual('foo ng-binding bar'); - - ctx.boolProp = false; - cd.detectChanges(); - expect(view.nodes[0].className).toEqual('foo ng-binding'); - - done(); - }); - }); - it('should support nested components.', (done) => { compiler.compile(MyComp, el('')).then((pv) => { createView(pv); @@ -163,7 +155,6 @@ class MyDir { }) class MyComp { ctxProp:string; - boolProp:boolean; constructor() { this.ctxProp = 'initial value'; } diff --git a/modules/angular2/test/core/compiler/pipeline/directive_parser_spec.js b/modules/angular2/test/core/compiler/pipeline/directive_parser_spec.js index 83758bfd53..2a1b6bd6f9 100644 --- a/modules/angular2/test/core/compiler/pipeline/directive_parser_spec.js +++ b/modules/angular2/test/core/compiler/pipeline/directive_parser_spec.js @@ -7,6 +7,7 @@ import {CompileStep} from 'angular2/src/core/compiler/pipeline/compile_step'; import {CompileElement} from 'angular2/src/core/compiler/pipeline/compile_element'; import {CompileControl} from 'angular2/src/core/compiler/pipeline/compile_control'; import {DOM} from 'angular2/src/facade/dom'; +import {NativeShadowDomStrategy, ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {Component} from 'angular2/src/core/annotations/annotations'; import {Decorator} from 'angular2/src/core/annotations/annotations'; import {Template} from 'angular2/src/core/annotations/annotations'; @@ -182,7 +183,6 @@ export function main() { }).toThrowError('Only template directives are allowed on