feat(view): generalized loading of dynamic components

This commit is contained in:
vsavkin
2015-04-06 13:19:30 -07:00
parent e9f70293ac
commit f45281a10a
19 changed files with 424 additions and 245 deletions

View File

@ -590,7 +590,7 @@ export class Component extends Directive {
* })
* class DynamicComp {
* helloCmp:HelloCmp;
* constructor(loader:PrivateComponentLoader, location:PrivateComponentLocation) {
* constructor(loader:DynamicComponentLoader, location:PrivateComponentLocation) {
* loader.load(HelloCmp, location).then((helloCmp) => {
* this.helloCmp = helloCmp;
* });

View File

@ -24,8 +24,7 @@ import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mappe
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {StyleUrlResolver} from 'angular2/src/render/dom/shadow_dom/style_url_resolver';
import {StyleInliner} from 'angular2/src/render/dom/shadow_dom/style_inliner';
import {Component} from 'angular2/src/core/annotations/annotations';
import {PrivateComponentLoader} from 'angular2/src/core/compiler/private_component_loader';
import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader';
import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/testability';
import {ViewFactory, VIEW_POOL_CAPACITY} from 'angular2/src/core/compiler/view_factory';
import {ProtoViewFactory} from 'angular2/src/core/compiler/proto_view_factory';
@ -35,7 +34,7 @@ import * as rc from 'angular2/src/render/dom/compiler/compiler';
import * as rvf from 'angular2/src/render/dom/view/view_factory';
import {
appViewToken,
appComponentRefToken,
appChangeDetectorToken,
appElementToken,
appComponentAnnotatedTypeToken,
@ -66,37 +65,20 @@ function _injectorBindings(appComponentType): List<Binding> {
}
return element;
}, [appComponentAnnotatedTypeToken, appDocumentToken]),
bind(appViewToken).toAsyncFactory((changeDetection, compiler, injector, appElement,
appComponentAnnotatedType, testability, registry, viewFactory) => {
bind(appComponentRefToken).toAsyncFactory((dynamicComponentLoader, injector, appElement,
appComponentAnnotatedType, testability, registry) => {
// We need to do this here to ensure that we create Testability and
// it's ready on the window for users.
registry.registerApplication(appElement, testability);
var annotation = appComponentAnnotatedType.annotation;
if(!isBlank(annotation) && !(annotation instanceof Component)) {
var type = appComponentAnnotatedType.type;
throw new BaseException(`Only Components can be bootstrapped; ` +
`Directive of ${stringify(type)} is not a Component`);
}
return compiler.compileRoot(
appElement,
appComponentAnnotatedType.type
).then(
(appProtoView) => {
// The light Dom of the app element is not considered part of
// the angular application. Thus the context and lightDomInjector are
// empty.
var view = viewFactory.getView(appProtoView);
view.hydrate(injector, null, new Object(), null);
return view;
});
}, [ChangeDetection, Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken,
Testability, TestabilityRegistry, ViewFactory]),
return dynamicComponentLoader.loadIntoNewLocation(appElement, appComponentAnnotatedType.type, null, injector);
}, [DynamicComponentLoader, Injector, appElementToken, appComponentAnnotatedTypeToken,
Testability, TestabilityRegistry]),
bind(appChangeDetectorToken).toFactory((rootView) => rootView.changeDetector,
[appViewToken]),
bind(appComponentType).toFactory((rootView) => rootView.elementInjectors[0].getComponent(),
[appViewToken]),
bind(appChangeDetectorToken).toFactory((ref) => ref.hostView.changeDetector,
[appComponentRefToken]),
bind(appComponentType).toFactory((ref) => ref.instance,
[appComponentRefToken]),
bind(LifeCycle).toFactory((exceptionHandler) => new LifeCycle(exceptionHandler, null, assertionsEnabled()),[ExceptionHandler]),
bind(EventManager).toFactory((zone) => {
var plugins = [new HammerGesturesPlugin(), new DomEventsPlugin()];
@ -136,8 +118,8 @@ function _injectorBindings(appComponentType): List<Binding> {
UrlResolver,
StyleUrlResolver,
StyleInliner,
PrivateComponentLoader,
Testability,
DynamicComponentLoader,
Testability
];
}
@ -260,8 +242,8 @@ function _createVmZone(givenReporter:Function): VmTurnZone {
* @publicModule angular2/angular2
*/
export function bootstrap(appComponentType: Type,
componentServiceBindings: List<Binding>=null,
errorReporter: Function=null): Promise<Injector> {
componentServiceBindings: List<Binding> = null,
errorReporter: Function = null): Promise<Injector> {
BrowserDomAdapter.makeCurrent();
var bootstrapProcess = PromiseWrapper.completer();
@ -272,11 +254,11 @@ export function bootstrap(appComponentType: Type,
var appInjector = _createAppInjector(appComponentType, componentServiceBindings, zone);
PromiseWrapper.then(appInjector.asyncGet(appViewToken),
(rootView) => {
PromiseWrapper.then(appInjector.asyncGet(appChangeDetectorToken),
(appChangeDetector) => {
// retrieve life cycle: may have already been created if injected in root component
var lc=appInjector.get(LifeCycle);
lc.registerWith(zone, rootView.changeDetector);
var lc = appInjector.get(LifeCycle);
lc.registerWith(zone, appChangeDetector);
lc.tick(); //the first tick that will bootstrap the app
bootstrapProcess.resolve(appInjector);

View File

@ -1,6 +1,6 @@
import {OpaqueToken} from 'angular2/di';
export var appViewToken = new OpaqueToken('AppView');
export var appComponentRefToken = new OpaqueToken('ComponentRef');
export var appChangeDetectorToken = new OpaqueToken('AppChangeDetector');
export var appElementToken = new OpaqueToken('AppElement');
export var appComponentAnnotatedTypeToken = new OpaqueToken('AppComponentAnnotatedType');

View File

@ -0,0 +1,99 @@
import {Key, Injector, Injectable} from 'angular2/di'
import {Compiler} from './compiler';
import {DirectiveMetadataReader} from './directive_metadata_reader';
import {Type, BaseException, stringify, isPresent} from 'angular2/src/facade/lang';
import {Promise} from 'angular2/src/facade/async';
import {Component} from 'angular2/src/core/annotations/annotations';
import {ViewFactory} from 'angular2/src/core/compiler/view_factory';
import {Renderer} from 'angular2/src/render/api';
import {ElementRef, DirectiveRef, ComponentRef} from './element_injector';
/**
* Service for dynamically loading a Component into an arbitrary position in the internal Angular
* application tree.
*/
@Injectable()
export class DynamicComponentLoader {
_compiler:Compiler;
_viewFactory:ViewFactory;
_renderer:Renderer;
_directiveMetadataReader:DirectiveMetadataReader;
constructor(compiler:Compiler, directiveMetadataReader:DirectiveMetadataReader,
renderer:Renderer, viewFactory:ViewFactory) {
this._compiler = compiler;
this._directiveMetadataReader = directiveMetadataReader;
this._renderer = renderer;
this._viewFactory = viewFactory
}
/**
* Loads a component into the location given by the provided ElementRef. The loaded component
* receives injection as if it in the place of the provided ElementRef.
*/
loadIntoExistingLocation(type:Type, location:ElementRef, injector:Injector = null):Promise<ComponentRef> {
this._assertTypeIsComponent(type);
var annotation = this._directiveMetadataReader.read(type).annotation;
var inj = this._componentAppInjector(location, injector, annotation.services);
var hostEi = location.elementInjector;
var hostView = location.hostView;
return this._compiler.compile(type).then(componentProtoView => {
var context = hostEi.dynamicallyCreateComponent(type, annotation, inj);
var componentView = this._instantiateAndHydrateView(componentProtoView, injector, hostEi, context);
//TODO(vsavkin): do not use component child views as we need to clear the dynamically created views
//same problem exists on the render side
hostView.addComponentChildView(componentView);
this._renderer.setDynamicComponentView(hostView.render, location.boundElementIndex, componentView.render);
// TODO(vsavkin): return a component ref that dehydrates the component view and removes it
// from the component child views
return new ComponentRef(Key.get(type), hostEi, componentView);
});
}
/**
* Loads a component as a child of the View given by the provided ElementRef. The loaded
* component receives injection normally as a hosted view.
*
* TODO(vsavkin, jelbourn): remove protoViewFactory after render layer exists.
*/
loadIntoNewLocation(elementOrSelector:any, type:Type, location:ElementRef,
injector:Injector = null):Promise<ComponentRef> {
this._assertTypeIsComponent(type);
var inj = this._componentAppInjector(location, injector, null);
//TODO(tbosch) this should always be a selector
return this._compiler.compileRoot(elementOrSelector, type).then(pv => {
var hostView = this._instantiateAndHydrateView(pv, inj, null, new Object());
// TODO(vsavkin): return a component ref that dehydrates the host view
return new ComponentRef(Key.get(type), hostView.elementInjectors[0], hostView.componentChildViews[0]);
});
}
_componentAppInjector(location, injector, services) {
var inj = isPresent(injector) ? injector : location.elementInjector.getLightDomAppInjector();
return isPresent(services) ? inj.createChild(services) : inj;
}
_instantiateAndHydrateView(protoView, injector, hostElementInjector, context) {
var componentView = this._viewFactory.getView(protoView);
componentView.hydrate(injector, hostElementInjector, context, null);
return componentView;
}
/** Asserts that the type being dynamically instantiated is a Component. */
_assertTypeIsComponent(type:Type) {
var annotation = this._directiveMetadataReader.read(type).annotation;
if (!(annotation instanceof Component)) {
throw new BaseException(`Could not load '${stringify(type)}' because it is not a component.`);
}
}
}

View File

@ -9,7 +9,6 @@ import {ViewContainer} from 'angular2/src/core/compiler/view_container';
import {NgElement} from 'angular2/src/core/compiler/ng_element';
import {Directive, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations';
import {BindingPropagationConfig} from 'angular2/change_detection';
import * as pclModule from 'angular2/src/core/compiler/private_component_location';
import {QueryList} from './query_list';
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
@ -20,12 +19,50 @@ var _undefined = new Object();
var _staticKeys;
export class ElementRef {
elementInjector:ElementInjector;
constructor(elementInjector:ElementInjector){
this.elementInjector = elementInjector;
}
get hostView() {
return this.elementInjector.getHostView();
}
get boundElementIndex() {
return this.elementInjector.getBoundElementIndex();
}
}
export class DirectiveRef extends ElementRef {
_key:Key;
constructor(key:Key, elementInjector:ElementInjector){
super(elementInjector);
this._key = key;
}
get instance() {
return this.elementInjector.get(this._key);
}
}
export class ComponentRef extends DirectiveRef {
componentView:viewModule.View;
constructor(key:Key, elementInjector:ElementInjector, componentView:viewModule.View){
super(key, elementInjector);
this.componentView = componentView;
}
}
class StaticKeys {
viewId:number;
ngElementId:number;
viewContainerId:number;
bindingPropagationConfigId:number;
privateComponentLocationId:number;
directiveRefId:number;
constructor() {
//TODO: vsavkin Key.annotate(Key.get(View), 'static')
@ -33,7 +70,7 @@ class StaticKeys {
this.ngElementId = Key.get(NgElement).id;
this.viewContainerId = Key.get(ViewContainer).id;
this.bindingPropagationConfigId = Key.get(BindingPropagationConfig).id;
this.privateComponentLocationId = Key.get(pclModule.PrivateComponentLocation).id;
this.directiveRefId = Key.get(DirectiveRef).id;
}
static instance() {
@ -200,32 +237,40 @@ export class DirectiveDependency extends Dependency {
}
static createFrom(d:Dependency):Dependency {
var depth = 0;
var eventName = null;
var propName = null;
var attributeName = null;
var properties = d.properties;
var queryDirective = null;
return new DirectiveDependency(d.key, d.asPromise, d.lazy, d.optional,
d.properties, DirectiveDependency._depth(d.properties),
DirectiveDependency._eventEmitterName(d.properties),
DirectiveDependency._propSetterName(d.properties),
DirectiveDependency._attributeName(d.properties),
DirectiveDependency._query(d.properties)
);
}
for (var i = 0; i < properties.length; i++) {
var property = properties[i];
if (property instanceof Parent) {
depth = 1;
} else if (property instanceof Ancestor) {
depth = MAX_DEPTH;
} else if (property instanceof EventEmitter) {
eventName = property.eventName;
} else if (property instanceof PropertySetter) {
propName = property.propName;
} else if (property instanceof Attribute) {
attributeName = property.attributeName;
} else if (property instanceof Query) {
queryDirective = property.directive;
}
}
static _depth(properties):int {
if (properties.length == 0) return 0;
if (ListWrapper.any(properties, p => p instanceof Parent)) return 1;
if (ListWrapper.any(properties, p => p instanceof Ancestor)) return MAX_DEPTH;
return 0;
}
return new DirectiveDependency(d.key, d.asPromise, d.lazy, d.optional, d.properties, depth,
eventName, propName, attributeName, queryDirective);
static _eventEmitterName(properties):string {
var p = ListWrapper.find(properties, (p) => p instanceof EventEmitter);
return isPresent(p) ? p.eventName : null;
}
static _propSetterName(properties):string {
var p = ListWrapper.find(properties, (p) => p instanceof PropertySetter);
return isPresent(p) ? p.propName : null;
}
static _attributeName(properties):string {
var p = ListWrapper.find(properties, (p) => p instanceof Attribute);
return isPresent(p) ? p.attributeName : null;
}
static _query(properties) {
var p = ListWrapper.find(properties, (p) => p instanceof Query);
return isPresent(p) ? p.directive : null;
}
}
@ -425,6 +470,8 @@ export class ElementInjector extends TreeNode {
_lightDomAppInjector:Injector;
_shadowDomAppInjector:Injector;
_host:ElementInjector;
// If this element injector has a component, the component instance will be stored in _obj0
_obj0:any;
_obj1:any;
_obj2:any;
@ -437,8 +484,9 @@ export class ElementInjector extends TreeNode {
_obj9:any;
_preBuiltObjects;
_constructionCounter;
_privateComponent;
_privateComponentBinding:DirectiveBinding;
_dynamicallyCreatedComponent:any;
_dynamicallyCreatedComponentBinding:DirectiveBinding;
// Queries are added during construction or linking with a new parent.
// They are never removed.
@ -450,7 +498,6 @@ export class ElementInjector extends TreeNode {
this._proto = proto;
//we cannot call clearDirectives because fields won't be detected
this._host = null;
this._preBuiltObjects = null;
this._lightDomAppInjector = null;
this._shadowDomAppInjector = null;
@ -488,8 +535,8 @@ export class ElementInjector extends TreeNode {
if (isPresent(p._binding7) && p._binding7.callOnDestroy) {this._obj7.onDestroy();}
if (isPresent(p._binding8) && p._binding8.callOnDestroy) {this._obj8.onDestroy();}
if (isPresent(p._binding9) && p._binding9.callOnDestroy) {this._obj9.onDestroy();}
if (isPresent(this._privateComponentBinding) && this._privateComponentBinding.callOnDestroy) {
this._privateComponent.onDestroy();
if (isPresent(this._dynamicallyCreatedComponentBinding) && this._dynamicallyCreatedComponentBinding.callOnDestroy) {
this._dynamicallyCreatedComponent.onDestroy();
}
this._obj0 = null;
@ -502,7 +549,8 @@ export class ElementInjector extends TreeNode {
this._obj7 = null;
this._obj8 = null;
this._obj9 = null;
this._privateComponent = null;
this._dynamicallyCreatedComponent = null;
this._dynamicallyCreatedComponentBinding = null;
this._constructionCounter = 0;
}
@ -526,15 +574,13 @@ export class ElementInjector extends TreeNode {
if (isPresent(p._keyId7)) this._getDirectiveByKeyId(p._keyId7);
if (isPresent(p._keyId8)) this._getDirectiveByKeyId(p._keyId8);
if (isPresent(p._keyId9)) this._getDirectiveByKeyId(p._keyId9);
if (isPresent(this._privateComponentBinding)) {
this._privateComponent = this._new(this._privateComponentBinding);
}
}
createPrivateComponent(componentType:Type, annotation:Directive) {
this._privateComponentBinding = DirectiveBinding.createFromType(componentType, annotation);
this._privateComponent = this._new(this._privateComponentBinding);
return this._privateComponent;
dynamicallyCreateComponent(componentType:Type, annotation:Directive, injector:Injector) {
this._shadowDomAppInjector = injector;
this._dynamicallyCreatedComponentBinding = DirectiveBinding.createFromType(componentType, annotation);
this._dynamicallyCreatedComponent = this._new(this._dynamicallyCreatedComponentBinding);
return this._dynamicallyCreatedComponent;
}
_checkShadowDomAppInjector(shadowDomAppInjector:Injector) {
@ -546,9 +592,18 @@ export class ElementInjector extends TreeNode {
}
get(token) {
if (this._isDynamicallyLoadedComponent(token)) {
return this._dynamicallyCreatedComponent;
}
return this._getByKey(Key.get(token), 0, false, null);
}
_isDynamicallyLoadedComponent(token) {
return isPresent(this._dynamicallyCreatedComponentBinding) &&
Key.get(token) === this._dynamicallyCreatedComponentBinding.key;
}
hasDirective(type:Type):boolean {
return this._getDirectiveByKeyId(Key.get(type).id) !== _undefined;
}
@ -563,20 +618,25 @@ export class ElementInjector extends TreeNode {
return this._preBuiltObjects.element;
}
/** Gets the View associated with this ElementInjector */
getHostView() {
return this._preBuiltObjects.view;
}
getComponent() {
if (this._proto._binding0IsComponent) {
return this._obj0;
} else {
throw new BaseException('There is not component stored in this ElementInjector');
throw new BaseException('There is no component stored in this ElementInjector');
}
}
getPrivateComponent() {
return this._privateComponent;
getDynamicallyLoadedComponent() {
return this._dynamicallyCreatedComponent;
}
getShadowDomAppInjector() {
return this._shadowDomAppInjector;
getLightDomAppInjector() {
return this._lightDomAppInjector;
}
directParent(): ElementInjector {
@ -587,8 +647,9 @@ export class ElementInjector extends TreeNode {
return this._proto._binding0IsComponent && key.id === this._proto._keyId0;
}
_isPrivateComponentKey(key:Key) {
return isPresent(this._privateComponentBinding) && key.id === this._privateComponentBinding.key.id;
_isDynamicallyLoadedComponentKey(key:Key) {
return isPresent(this._dynamicallyCreatedComponentBinding) && key.id ===
this._dynamicallyCreatedComponentBinding.key.id;
}
_new(binding:Binding) {
@ -647,6 +708,10 @@ export class ElementInjector extends TreeNode {
if (isPresent(dep.propSetterName)) return this._buildPropSetter(dep);
if (isPresent(dep.attributeName)) return this._buildAttribute(dep);
if (isPresent(dep.queryDirective)) return this._findQuery(dep.queryDirective).list;
if (dep.key.id === StaticKeys.instance().directiveRefId) {
// TODO: we need store component view here and pass it to directive ref
return new DirectiveRef(requestor, this);
}
return this._getByKey(dep.key, dep.depth, dep.optional, requestor);
}
@ -819,10 +884,11 @@ export class ElementInjector extends TreeNode {
ei = ei._parent;
}
if (isPresent(this._host) && this._host._isComponentKey(key)) {
return this._host.getComponent();
} else if (isPresent(this._host) && this._host._isPrivateComponentKey(key)) {
return this._host.getPrivateComponent();
} else if (isPresent(this._host) && this._host._isDynamicallyLoadedComponentKey(key)) {
return this._host.getDynamicallyLoadedComponent();
} else if (optional) {
return this._appInjector(requestor).getOptional(key);
} else {
@ -831,7 +897,7 @@ export class ElementInjector extends TreeNode {
}
_appInjector(requestor:Key) {
if (isPresent(requestor) && this._isComponentKey(requestor)) {
if (isPresent(requestor) && (this._isComponentKey(requestor) || this._isDynamicallyLoadedComponentKey(requestor))) {
return this._shadowDomAppInjector;
} else {
return this._lightDomAppInjector;
@ -850,10 +916,6 @@ export class ElementInjector extends TreeNode {
if (keyId === staticKeys.viewContainerId) return this._preBuiltObjects.viewContainer;
if (keyId === staticKeys.bindingPropagationConfigId) return this._preBuiltObjects.bindingPropagationConfig;
if (keyId === staticKeys.privateComponentLocationId) {
return new pclModule.PrivateComponentLocation(this, this._preBuiltObjects.view);
}
//TODO add other objects as needed
return _undefined;
}

View File

@ -1,38 +0,0 @@
import {Compiler} from './compiler';
import {ViewFactory} from './view_factory';
import {Injectable} from 'angular2/di';
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
import {Component} from 'angular2/src/core/annotations/annotations';
import {PrivateComponentLocation} from './private_component_location';
import {Type, stringify, BaseException} from 'angular2/src/facade/lang';
@Injectable()
export class PrivateComponentLoader {
compiler:Compiler;
directiveMetadataReader:DirectiveMetadataReader;
viewFactory:ViewFactory;
constructor(compiler:Compiler, directiveMetadataReader:DirectiveMetadataReader, viewFactory:ViewFactory) {
this.compiler = compiler;
this.directiveMetadataReader = directiveMetadataReader;
this.viewFactory = viewFactory;
}
load(type:Type, location:PrivateComponentLocation) {
var annotation = this.directiveMetadataReader.read(type).annotation;
if (!(annotation instanceof Component)) {
throw new BaseException(`Could not load '${stringify(type)}' because it is not a component.`);
}
return this.compiler.compile(type).then((componentProtoView) => {
location.createComponent(
this.viewFactory,
type, annotation,
componentProtoView
);
});
}
}

View File

@ -1,29 +0,0 @@
import {Directive} from 'angular2/src/core/annotations/annotations'
import * as viewModule from './view';
import * as eiModule from './element_injector';
import {ListWrapper} from 'angular2/src/facade/collection';
import {Type} from 'angular2/src/facade/lang';
import * as vfModule from './view_factory';
export class PrivateComponentLocation {
_elementInjector:eiModule.ElementInjector;
_view:viewModule.View;
constructor(elementInjector:eiModule.ElementInjector, view:viewModule.View){
this._elementInjector = elementInjector;
this._view = view;
}
createComponent(viewFactory: vfModule.ViewFactory, type:Type, annotation:Directive, componentProtoView:viewModule.ProtoView) {
var context = this._elementInjector.createPrivateComponent(type, annotation);
var view = viewFactory.getView(componentProtoView);
view.hydrate(this._elementInjector.getShadowDomAppInjector(), this._elementInjector, context, null);
this._view.proto.renderer.setDynamicComponentView(
this._view.render, this._elementInjector.getBoundElementIndex(), view.render
);
ListWrapper.push(this._view.componentChildViews, view);
this._view.changeDetector.addChild(view.changeDetector);
}
}

View File

@ -28,7 +28,18 @@ export class View {
viewContainers: List<ViewContainer>;
preBuiltObjects: List<PreBuiltObjects>;
proto: ProtoView;
/**
* The context against which data-binding expressions in this view are evaluated against.
* This is always a component instance.
*/
context: any;
/**
* Variables, local to this view, that can be used in binding expressions (in addition to the
* context). This is used for thing like `<video #player>` or
* `<li template="for #item of items">`, where "player" and "item" are locals, respectively.
*/
locals:Locals;
constructor(proto:ProtoView, protoLocals:Map) {
@ -244,6 +255,11 @@ export class View {
return elementInjector.getDirectiveAtIndex(directive.directiveIndex);
}
addComponentChildView(view:View) {
ListWrapper.push(this.componentChildViews, view);
this.changeDetector.addShadowDomChild(view.changeDetector);
}
// implementation of EventDispatcher#dispatchEvent
dispatchEvent(
elementIndex:number, eventName:string, locals:Map<string, any>