diff --git a/modules/core/src/application.js b/modules/core/src/application.js new file mode 100644 index 0000000000..9a0eacbaeb --- /dev/null +++ b/modules/core/src/application.js @@ -0,0 +1,85 @@ +import {Injector, bind} from 'di/di'; +import {Type, FIELD, isBlank, isPresent, BaseException} from 'facade/lang'; +import {DOM, Element} from 'facade/dom'; +import {Compiler} from './compiler/compiler'; +import {ProtoView} from './compiler/view'; +import {ClosureMap} from 'change_detection/parser/closure_map'; +import {Parser} from 'change_detection/parser/parser'; +import {Lexer} from 'change_detection/parser/lexer'; +import {ChangeDetector} from 'change_detection/change_detector'; +import {WatchGroup} from 'change_detection/watch_group'; +import {TemplateLoader} from './compiler/template_loader'; +import {Reflector} from './compiler/reflector'; +import {AnnotatedType} from './compiler/annotated_type'; +import {ListWrapper} from 'facade/collection'; + +var _rootInjector: Injector; + +// Contains everything that is safe to share between applications. +var _rootBindings = [Compiler, TemplateLoader, Reflector, Parser, Lexer, ClosureMap]; + +export var appViewToken = new Object(); +export var appWatchGroupToken = new Object(); +export var appElementToken = new Object(); +export var appComponentAnnotatedTypeToken = new Object(); +export var appDocumentToken = new Object(); + +// Exported only for tests that need to overwrite default document binding. +export function documentDependentBindings(appComponentType) { + return [ + bind(appComponentAnnotatedTypeToken).toFactory((reflector) => { + // TODO(rado): inspect annotation here and warn if there are bindings, + // lightDomServices, and other component annotations that are skipped + // for bootstrapping components. + return reflector.annotatedType(appComponentType); + }, [Reflector]), + + bind(appElementToken).toFactory((appComponentAnnotatedType, appDocument) => { + var selector = appComponentAnnotatedType.annotation.selector; + var element = DOM.querySelector(appDocument, selector); + if (isBlank(element)) { + throw new BaseException(`The app selector "${selector}" did not match any elements`); + } + return element; + }, [appComponentAnnotatedTypeToken, appDocumentToken]), + + bind(appViewToken).toAsyncFactory((compiler, injector, appElement, + appComponentAnnotatedType) => { + return compiler.compile(appComponentAnnotatedType.type, null).then( + (protoView) => { + var appProtoView = ProtoView.createRootProtoView(protoView, + appElement, appComponentAnnotatedType); + // The light Dom of the app element is not considered part of + // the angular application. Thus the context and lightDomInjector are + // empty. + return appProtoView.instantiate(new Object(), injector, null, true); + }); + }, [Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken]), + + bind(appWatchGroupToken).toFactory((rootView) => rootView.watchGroup, + [appViewToken]), + bind(ChangeDetector).toFactory((appWatchGroup) => + new ChangeDetector(appWatchGroup), [appWatchGroupToken]) + ]; +} + +function _injectorBindings(appComponentType) { + return ListWrapper.concat([bind(appDocumentToken).toValue(DOM.defaultDoc())], + documentDependentBindings(appComponentType)); +} + +// Multiple calls to this method are allowed. Each application would only share +// _rootInjector, which is not user-configurable by design, thus safe to share. +export function bootstrap(appComponentType: Type, bindings=null) { + // TODO(rado): prepopulate template cache, so applications with only + // index.html and main.js are possible. + if (isBlank(_rootInjector)) _rootInjector = new Injector(_rootBindings); + var appInjector = _rootInjector.createChild(_injectorBindings( + appComponentType)); + if (isPresent(bindings)) appInjector = appInjector.createChild(bindings); + return appInjector.asyncGet(ChangeDetector).then((cd) => { + // TODO(rado): replace with zone. + cd.detectChanges(); + return appInjector; + }); +} diff --git a/modules/core/src/compiler/compiler.js b/modules/core/src/compiler/compiler.js index 722ca6a4cf..b655a59bed 100644 --- a/modules/core/src/compiler/compiler.js +++ b/modules/core/src/compiler/compiler.js @@ -13,6 +13,7 @@ import {CompileElement} from './pipeline/compile_element'; import {createDefaultSteps} from './pipeline/default_steps'; import {TemplateLoader} from './template_loader'; import {AnnotatedType} from './annotated_type'; +import {Component} from '../annotations/component'; /** * The compiler loads and translates the html templates of components into @@ -28,7 +29,8 @@ export class Compiler { } createSteps(component:AnnotatedType):List { - var directives = component.annotation.template.directives; + var annotation: Component = component.annotation; + var directives = annotation.template.directives; var annotatedDirectives = ListWrapper.create(); for (var i=0; i, + // and the component template is already compiled into protoView. + // Used for bootstrapping. + static createRootProtoView(protoView: ProtoView, + insertionElement, rootComponentAnnotatedType: AnnotatedType): ProtoView { + var rootProtoView = new ProtoView(insertionElement, new ProtoWatchGroup()); + var binder = rootProtoView.bindElement( + new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true)); + binder.componentDirective = rootComponentAnnotatedType; + binder.nestedProtoView = protoView; + DOM.addClass(insertionElement, 'ng-binding'); + return rootProtoView; + } } export class ElementPropertyMemento { diff --git a/modules/core/src/core.js b/modules/core/src/core.js index d4cea0732c..4005cae8e9 100644 --- a/modules/core/src/core.js +++ b/modules/core/src/core.js @@ -5,6 +5,8 @@ export * from './annotations/directive'; export * from './annotations/component'; export * from './annotations/template_config'; +export * from './application'; + export * from 'change_detection/change_detector'; export * from 'change_detection/watch_group'; export * from 'change_detection/record'; diff --git a/modules/core/test/application_spec.js b/modules/core/test/application_spec.js new file mode 100644 index 0000000000..dcb78e0e66 --- /dev/null +++ b/modules/core/test/application_spec.js @@ -0,0 +1,97 @@ +import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach} from 'test_lib/test_lib'; +import {bootstrap, appDocumentToken, appElementToken, documentDependentBindings} + from 'core/application'; +import {Component} from 'core/annotations/component'; +import {DOM} from 'facade/dom'; +import {ListWrapper} from 'facade/collection'; +import {PromiseWrapper} from 'facade/async'; +import {bind} from 'di/di'; +import {TemplateConfig} from 'core/annotations/template_config'; + +@Component({ + selector: 'hello-app', + template: new TemplateConfig({ + inline: '{{greeting}} world!', + directives: [] + }) +}) +class HelloRootCmp { + constructor() { + this.greeting = 'hello'; + } +} + +@Component({ + selector: 'hello-app-2', + template: new TemplateConfig({ + inline: '{{greeting}} world, again!', + directives: [] + }) +}) +class HelloRootCmp2 { + constructor() { + this.greeting = 'hello'; + } +} + +export function main() { + var fakeDoc, el, el2; + + beforeEach(() => { + fakeDoc = DOM.createHtmlDocument(); + el = DOM.createElement('hello-app', fakeDoc); + el2 = DOM.createElement('hello-app-2', fakeDoc); + DOM.appendChild(fakeDoc.body, el); + DOM.appendChild(fakeDoc.body, el2); + }); + + function testBindings(appComponentType) { + return ListWrapper.concat([bind(appDocumentToken).toValue(fakeDoc), + ], documentDependentBindings(appComponentType)); + } + + describe('bootstrap factory method', () => { + it('should throw if no element is found', (done) => { + var injectorPromise = bootstrap(HelloRootCmp); + PromiseWrapper.then(injectorPromise, null, (reason) => { + expect(reason.message).toContain( + 'The app selector "hello-app" did not match any elements'); + done(); + }); + }); + + it('should create an injector promise', () => { + var injectorPromise = bootstrap(HelloRootCmp, testBindings(HelloRootCmp)); + expect(injectorPromise).not.toBe(null); + }); + + it('should resolve an injector promise and contain bindings', (done) => { + var injectorPromise = bootstrap(HelloRootCmp, testBindings(HelloRootCmp)); + injectorPromise.then((injector) => { + expect(injector.get(appElementToken)).toBe(el); + done(); + }); + }); + + it('should display hello world', (done) => { + var injectorPromise = bootstrap(HelloRootCmp, testBindings(HelloRootCmp)); + injectorPromise.then((injector) => { + expect(injector.get(appElementToken) + .shadowRoot.childNodes[0].nodeValue).toEqual('hello world!'); + done(); + }); + }); + + it('should support multiple calls to bootstrap', (done) => { + var injectorPromise1 = bootstrap(HelloRootCmp, testBindings(HelloRootCmp)); + var injectorPromise2 = bootstrap(HelloRootCmp2, testBindings(HelloRootCmp2)); + PromiseWrapper.all([injectorPromise1, injectorPromise2]).then((injectors) => { + expect(injectors[0].get(appElementToken) + .shadowRoot.childNodes[0].nodeValue).toEqual('hello world!'); + expect(injectors[1].get(appElementToken) + .shadowRoot.childNodes[0].nodeValue).toEqual('hello world, again!'); + done(); + }); + }); + }); +} diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index 61c0ba3d05..496481607f 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -17,13 +17,15 @@ import {View} from 'core/compiler/view'; export function main() { describe('view', function() { - var parser, closureMap; + var parser, closureMap, someComponentDirective; - beforeEach( () => { + beforeEach(() => { closureMap = new ClosureMap(); parser = new Parser(new Lexer(), closureMap); + someComponentDirective = new Reflector().annotatedType(SomeComponent); }); + describe('ProtoView.instantiate', function() { function createCollectDomNodesTestCases(useTemplateElement:boolean) { @@ -92,6 +94,22 @@ export function main() { }); } + describe('inplace instantiation', () => { + it('should be supported.', () => { + var template = createElement('
') + var view = new ProtoView(template, new ProtoWatchGroup()) + .instantiate(null, null, null, true); + expect(view.nodes[0]).toBe(template); + }); + + it('should be off by default.', () => { + var template = createElement('
') + var view = new ProtoView(template, new ProtoWatchGroup()) + .instantiate(null, null, null); + expect(view.nodes[0]).not.toBe(template); + }); + }); + describe('collect dom nodes with a regular element as root', () => { createCollectDomNodesTestCases(false); }); @@ -158,7 +176,7 @@ export function main() { function createComponentWithSubPV(subProtoView) { var pv = new ProtoView(createElement(''), new ProtoWatchGroup()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponent], true)); - binder.componentDirective = new Reflector().annotatedType(SomeComponent); + binder.componentDirective = someComponentDirective; binder.nestedProtoView = subProtoView; return pv; } @@ -253,7 +271,26 @@ export function main() { expect(view.elementInjectors[0].get(SomeDirective).prop).toEqual('buz'); }); }); + }); + describe('protoView createRootProtoView', () => { + var el, pv; + beforeEach(() => { + el = DOM.createElement('div'); + pv = new ProtoView(createElement('
hi
'), new ProtoWatchGroup()); + }); + + it('should create the root component when instantiated', () => { + var rootProtoView = ProtoView.createRootProtoView(pv, el, someComponentDirective); + var view = rootProtoView.instantiate(null, new Injector([]), null, true); + expect(view.rootElementInjectors[0].get(SomeComponent)).not.toBe(null); + }); + + it('should inject the protoView into the shadowDom', () => { + var rootProtoView = ProtoView.createRootProtoView(pv, el, someComponentDirective); + var view = rootProtoView.instantiate(null, new Injector([]), null, true); + expect(el.shadowRoot.childNodes[0].childNodes[0].nodeValue).toEqual('hi'); + }); }); }); } diff --git a/modules/facade/src/dom.dart b/modules/facade/src/dom.dart index 4685340521..6c316db578 100644 --- a/modules/facade/src/dom.dart +++ b/modules/facade/src/dom.dart @@ -17,6 +17,9 @@ class DOM { static query(selector) { return document.querySelector(selector); } + static Element querySelector(el, String selector) { + return el.querySelector(selector); + } static ElementList querySelectorAll(el, String selector) { return el.querySelectorAll(selector); } @@ -52,6 +55,10 @@ class DOM { t.setInnerHtml(html, treeSanitizer:identitySanitizer); return t; } + static createElement(tagName, [doc=null]) { + if (doc == null) doc = document; + return doc.createElement(tagName); + } static clone(Node node) { return node.clone(true); } @@ -82,4 +89,10 @@ class DOM { static Node templateAwareRoot(Element el) { return el is TemplateElement ? el.content : el; } + static HtmlDocument createHtmlDocument() { + return document.implementation.createHtmlDocument('fakeTitle'); + } + static HtmlDocument defaultDoc() { + return document; + } } diff --git a/modules/facade/src/dom.es6 b/modules/facade/src/dom.es6 index e3be55146c..89731ebefc 100644 --- a/modules/facade/src/dom.es6 +++ b/modules/facade/src/dom.es6 @@ -13,6 +13,9 @@ export class DOM { static query(selector) { return document.querySelector(selector); } + static querySelector(el, selector:string):Node { + return el.querySelector(selector); + } static querySelectorAll(el, selector:string):NodeList { return el.querySelectorAll(selector); } @@ -48,6 +51,9 @@ export class DOM { t.innerHTML = html; return t; } + static createElement(tagName, doc=document) { + return doc.createElement(tagName); + } static clone(node:Node) { return node.cloneNode(true); } @@ -84,4 +90,10 @@ export class DOM { static templateAwareRoot(el:Element):Node { return el instanceof TemplateElement ? el.content : el; } + static createHtmlDocument() { + return document.implementation.createHTMLDocument(); + } + static defaultDoc() { + return document; + } }