feat(core): view engine - add missing DI features (#14225)

Part of #14013

PR Close #14225
This commit is contained in:
Tobias Bosch
2017-02-01 07:27:38 -08:00
committed by Miško Hevery
parent ae7f5f37d2
commit a05e50fda3
16 changed files with 396 additions and 158 deletions

View File

@ -8,7 +8,7 @@
export {anchorDef, elementDef} from './element';
export {ngContentDef} from './ng_content';
export {providerDef} from './provider';
export {directiveDef, providerDef} from './provider';
export {pureArrayDef, pureObjectDef, purePipeDef} from './pure_expression';
export {queryDef} from './query';
export {textDef} from './text';

View File

@ -7,17 +7,18 @@
*/
import {isDevMode} from '../application_ref';
import {SimpleChange, SimpleChanges} from '../change_detection/change_detection';
import {ChangeDetectorRef, SimpleChange, SimpleChanges} from '../change_detection/change_detection';
import {Injector} from '../di';
import {stringify} from '../facade/lang';
import {ElementRef} from '../linker/element_ref';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';
import {Renderer} from '../render/api';
import {Type} from '../type';
import {queryDef} from './query';
import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, ViewState, asElementData, asProviderData} from './types';
import {checkAndUpdateBinding, dispatchEvent, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util';
import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, ProviderType, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, ViewState, asElementData, asProviderData} from './types';
import {checkAndUpdateBinding, dispatchEvent, entryAction, findElementDef, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util';
const _tokenKeyCache = new Map<any, string>();
@ -25,11 +26,31 @@ const RendererTokenKey = tokenKey(Renderer);
const ElementRefTokenKey = tokenKey(ElementRef);
const ViewContainerRefTokenKey = tokenKey(ViewContainerRef);
const TemplateRefTokenKey = tokenKey(TemplateRef);
const ChangeDetectorRefTokenKey = tokenKey(ChangeDetectorRef);
const InjectorRefTokenKey = tokenKey(Injector);
export function providerDef(
const NOT_CREATED = new Object();
export function directiveDef(
flags: NodeFlags, matchedQueries: [string, QueryValueType][], childCount: number, ctor: any,
deps: ([DepFlags, any] | any)[], props?: {[name: string]: [number, string]},
outputs?: {[name: string]: string}, component?: () => ViewDefinition): NodeDef {
return _providerDef(
flags, matchedQueries, childCount, ProviderType.Class, ctor, ctor, deps, props, outputs,
component);
}
export function providerDef(
flags: NodeFlags, matchedQueries: [string, QueryValueType][], type: ProviderType, token: any,
value: any, deps: ([DepFlags, any] | any)[]): NodeDef {
return _providerDef(flags, matchedQueries, 0, type, token, value, deps);
}
export function _providerDef(
flags: NodeFlags, matchedQueries: [string, QueryValueType][], childCount: number,
type: ProviderType, token: any, value: any, deps: ([DepFlags, any] | any)[],
props?: {[name: string]: [number, string]}, outputs?: {[name: string]: string},
component?: () => ViewDefinition): NodeDef {
const matchedQueryDefs: {[queryId: string]: QueryValueType} = {};
if (matchedQueries) {
matchedQueries.forEach(([queryId, valueType]) => { matchedQueryDefs[queryId] = valueType; });
@ -85,8 +106,9 @@ export function providerDef(
disposableCount: outputDefs.length,
element: undefined,
provider: {
tokenKey: tokenKey(ctor),
token: ctor, ctor,
type,
token,
tokenKey: tokenKey(token), value,
deps: depDefs,
outputs: outputDefs, component
},
@ -106,19 +128,9 @@ export function tokenKey(token: any): string {
return key;
}
export function createProvider(
view: ViewData, def: NodeDef, componentView: ViewData): ProviderData {
export function createProviderInstance(view: ViewData, def: NodeDef): any {
const providerDef = def.provider;
const provider = createInstance(view, def.parent, providerDef.ctor, providerDef.deps);
if (providerDef.outputs.length) {
for (let i = 0; i < providerDef.outputs.length; i++) {
const output = providerDef.outputs[i];
const subscription = provider[output.propName].subscribe(
eventHandlerClosure(view, def.parent, output.eventName));
view.disposables[def.disposableIndex + i] = subscription.unsubscribe.bind(subscription);
}
}
return {instance: provider, componentView: componentView};
return def.flags & NodeFlags.LazyProvider ? NOT_CREATED : createInstance(view, def);
}
function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
@ -182,7 +194,38 @@ export function checkAndUpdateProviderDynamic(view: ViewData, def: NodeDef, valu
}
}
function createInstance(view: ViewData, elIndex: number, ctor: any, deps: DepDef[]): any {
function createInstance(view: ViewData, nodeDef: NodeDef): any {
const providerDef = nodeDef.provider;
let injectable: any;
switch (providerDef.type) {
case ProviderType.Class:
injectable =
createClass(view, nodeDef.index, nodeDef.parent, providerDef.value, providerDef.deps);
break;
case ProviderType.Factory:
injectable =
callFactory(view, nodeDef.index, nodeDef.parent, providerDef.value, providerDef.deps);
break;
case ProviderType.UseExisting:
injectable = resolveDep(view, nodeDef.index, nodeDef.parent, providerDef.deps[0]);
break;
case ProviderType.Value:
injectable = providerDef.value;
break;
}
if (providerDef.outputs.length) {
for (let i = 0; i < providerDef.outputs.length; i++) {
const output = providerDef.outputs[i];
const subscription = injectable[output.propName].subscribe(
eventHandlerClosure(view, nodeDef.parent, output.eventName));
view.disposables[nodeDef.disposableIndex + i] = subscription.unsubscribe.bind(subscription);
}
}
return injectable;
}
function createClass(
view: ViewData, requestorNodeIndex: number, elIndex: number, ctor: any, deps: DepDef[]): any {
const len = deps.length;
let injectable: any;
switch (len) {
@ -190,32 +233,69 @@ function createInstance(view: ViewData, elIndex: number, ctor: any, deps: DepDef
injectable = new ctor();
break;
case 1:
injectable = new ctor(resolveDep(view, elIndex, deps[0]));
injectable = new ctor(resolveDep(view, requestorNodeIndex, elIndex, deps[0]));
break;
case 2:
injectable = new ctor(resolveDep(view, elIndex, deps[0]), resolveDep(view, elIndex, deps[1]));
injectable = new ctor(
resolveDep(view, requestorNodeIndex, elIndex, deps[0]),
resolveDep(view, requestorNodeIndex, elIndex, deps[1]));
break;
case 3:
injectable = new ctor(
resolveDep(view, elIndex, deps[0]), resolveDep(view, elIndex, deps[1]),
resolveDep(view, elIndex, deps[2]));
resolveDep(view, requestorNodeIndex, elIndex, deps[0]),
resolveDep(view, requestorNodeIndex, elIndex, deps[1]),
resolveDep(view, requestorNodeIndex, elIndex, deps[2]));
break;
default:
const depValues = new Array(len);
for (let i = 0; i < len; i++) {
depValues[i] = resolveDep(view, elIndex, deps[i]);
depValues[i] = resolveDep(view, requestorNodeIndex, elIndex, deps[i]);
}
injectable = new ctor(...depValues);
}
return injectable;
}
function callFactory(
view: ViewData, requestorNodeIndex: number, elIndex: number, factory: any,
deps: DepDef[]): any {
const len = deps.length;
let injectable: any;
switch (len) {
case 0:
injectable = factory();
break;
case 1:
injectable = factory(resolveDep(view, requestorNodeIndex, elIndex, deps[0]));
break;
case 2:
injectable = factory(
resolveDep(view, requestorNodeIndex, elIndex, deps[0]),
resolveDep(view, requestorNodeIndex, elIndex, deps[1]));
break;
case 3:
injectable = factory(
resolveDep(view, requestorNodeIndex, elIndex, deps[0]),
resolveDep(view, requestorNodeIndex, elIndex, deps[1]),
resolveDep(view, requestorNodeIndex, elIndex, deps[2]));
break;
default:
const depValues = Array(len);
for (let i = 0; i < len; i++) {
depValues[i] = resolveDep(view, requestorNodeIndex, elIndex, deps[i]);
}
injectable = factory(...depValues);
}
return injectable;
}
export function resolveDep(
view: ViewData, elIndex: number, depDef: DepDef,
notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
view: ViewData, requestNodeIndex: number, elIndex: number, depDef: DepDef): any {
const notFoundValue = depDef.flags & DepFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND;
const tokenKey = depDef.tokenKey;
if (depDef.flags & DepFlags.SkipSelf) {
requestNodeIndex = null;
const elDef = view.def.nodes[elIndex];
if (elDef.parent != null) {
elIndex = elDef.parent;
@ -240,12 +320,30 @@ export function resolveDep(
return view.services.createViewContainerRef(asElementData(view, elIndex));
case TemplateRefTokenKey:
return view.services.createTemplateRef(view, elDef);
case ChangeDetectorRefTokenKey:
let cdView = view;
// If we are still checking dependencies on the initial element...
if (requestNodeIndex != null) {
const requestorNodeDef = view.def.nodes[requestNodeIndex];
if (requestorNodeDef.flags & NodeFlags.HasComponent) {
cdView = asProviderData(view, requestNodeIndex).componentView;
}
}
// A ViewRef is also a ChangeDetectorRef
return view.services.createViewRef(cdView);
case InjectorRefTokenKey:
return createInjector(view, elIndex);
default:
const providerIndex = elDef.element.providerIndices[tokenKey];
if (providerIndex != null) {
return asProviderData(view, providerIndex).instance;
const providerData = asProviderData(view, providerIndex);
if (providerData.instance === NOT_CREATED) {
providerData.instance = createInstance(view, view.def.nodes[providerIndex]);
}
return providerData.instance;
}
}
requestNodeIndex = null;
elIndex = parentDiIndex(view);
view = view.parent;
}
@ -274,7 +372,8 @@ class Injector_ implements Injector {
constructor(private view: ViewData, private elIndex: number) {}
get(token: any, notFoundValue?: any): any {
return resolveDep(
this.view, this.elIndex, {flags: DepFlags.None, token, tokenKey: tokenKey(token)});
this.view, undefined, this.elIndex,
{flags: DepFlags.None, token, tokenKey: tokenKey(token)});
}
}

View File

@ -64,7 +64,7 @@ function _pureExpressionDef(
export function createPureExpression(view: ViewData, def: NodeDef): PureExpressionData {
const pipe = def.pureExpression.pipeDep ?
resolveDep(view, def.parent, def.pureExpression.pipeDep) :
resolveDep(view, def.index, def.parent, def.pureExpression.pipeDep) :
undefined;
return {value: undefined, pipe};
}

View File

@ -19,7 +19,7 @@ import {Sanitizer, SecurityContext} from '../security';
import {createInjector} from './provider';
import {getQueryValue} from './query';
import {DebugContext, ElementData, NodeData, NodeDef, NodeType, Services, ViewData, ViewDefinition, ViewState, asElementData} from './types';
import {isComponentView, renderNode, rootRenderNodes} from './util';
import {findElementDef, isComponentView, renderNode, rootRenderNodes} from './util';
import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view';
import {attachEmbeddedView, detachEmbeddedView} from './view_attach';
@ -33,6 +33,7 @@ export class DefaultServices implements Services {
sanitize(context: SecurityContext, value: string): string {
return this._sanitizer.sanitize(context, value);
}
createViewRef(data: ViewData): ViewRef { return new ViewRef_(data); }
createViewContainerRef(data: ElementData): ViewContainerRef {
return new ViewContainerRef_(data);
}
@ -120,8 +121,16 @@ class ViewRef_ implements EmbeddedViewRef<any> {
this._view.state = ViewState.ChecksDisabled;
}
}
detectChanges(): void { checkAndUpdateView(this._view); }
checkNoChanges(): void { checkNoChangesView(this._view); }
detectChanges(): void {
if (this._view.state !== ViewState.FirstCheck) {
checkAndUpdateView(this._view);
}
}
checkNoChanges(): void {
if (this._view.state !== ViewState.FirstCheck) {
checkNoChangesView(this._view);
}
}
reattach(): void {
if (this._view.state === ViewState.ChecksDisabled) {
@ -217,18 +226,6 @@ function findHostElement(view: ViewData): ElementData {
return undefined;
}
function findElementDef(view: ViewData, nodeIndex: number): NodeDef {
const viewDef = view.def;
let nodeDef = viewDef.nodes[nodeIndex];
while (nodeDef) {
if (nodeDef.type === NodeType.Element) {
return nodeDef;
}
nodeDef = nodeDef.parent != null ? viewDef.nodes[nodeDef.parent] : undefined;
}
return undefined;
}
function collectReferences(view: ViewData, nodeDef: NodeDef, references: {[key: string]: any}) {
for (let queryId in nodeDef.matchedQueries) {
if (queryId.startsWith('#')) {

View File

@ -10,6 +10,7 @@ import {PipeTransform} from '../change_detection/change_detection';
import {QueryList} from '../linker/query_list';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';
import {ViewRef} from '../linker/view_ref';
import {RenderComponentType, RenderDebugInfo, Renderer, RootRenderer} from '../render/api';
import {Sanitizer, SecurityContext} from '../security';
@ -126,6 +127,7 @@ export enum NodeFlags {
HasComponent = 1 << 9,
HasContentQuery = 1 << 10,
HasViewQuery = 1 << 11,
LazyProvider = 1 << 12
}
export interface BindingDef {
@ -162,8 +164,6 @@ export interface ElementDef {
/**
* visible providers for DI in the view,
* as see from this element.
* Note: We use protoypical inheritance
* to indices in parent ElementDefs.
*/
providerIndices: {[tokenKey: string]: number};
source: string;
@ -175,15 +175,23 @@ export interface ElementOutputDef {
}
export interface ProviderDef {
type: ProviderType;
token: any;
tokenKey: string;
ctor: any;
value: any;
deps: DepDef[];
outputs: ProviderOutputDef[];
// closure to allow recursive components
component: ViewDefinitionFactory;
}
export enum ProviderType {
Value,
Class,
Factory,
UseExisting
}
export interface DepDef {
flags: DepFlags;
token: any;
@ -195,7 +203,8 @@ export interface DepDef {
*/
export enum DepFlags {
None = 0,
SkipSelf = 1 << 0
SkipSelf = 1 << 0,
Optional = 1 << 1
}
export interface ProviderOutputDef {
@ -377,6 +386,8 @@ export interface Services {
renderComponent(rcp: RenderComponentType): Renderer;
sanitize(context: SecurityContext, value: string): string;
// Note: This needs to be here to prevent a cycle in source files.
createViewRef(data: ViewData): ViewRef;
// Note: This needs to be here to prevent a cycle in source files.
createViewContainerRef(data: ElementData): ViewContainerRef;
// Note: This needs to be here to prevent a cycle in source files.
createTemplateRef(parentView: ViewData, def: NodeDef): TemplateRef<any>;

View File

@ -88,6 +88,18 @@ export function declaredViewContainer(view: ViewData): ElementData {
return undefined;
}
export function findElementDef(view: ViewData, nodeIndex: number): NodeDef {
const viewDef = view.def;
let nodeDef = viewDef.nodes[nodeIndex];
while (nodeDef) {
if (nodeDef.type === NodeType.Element) {
return nodeDef;
}
nodeDef = nodeDef.parent != null ? viewDef.nodes[nodeDef.parent] : undefined;
}
return undefined;
}
export function renderNode(view: ViewData, def: NodeDef): any {
switch (def.type) {
case NodeType.Element:

View File

@ -12,7 +12,7 @@ import {RenderComponentType, Renderer} from '../render/api';
import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element';
import {expressionChangedAfterItHasBeenCheckedError} from './errors';
import {appendNgContent} from './ng_content';
import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider';
import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProviderInstance} from './provider';
import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression';
import {checkAndUpdateQuery, createQuery, queryDef} from './query';
import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text';
@ -58,6 +58,7 @@ export function viewDef(
});
if (node.element) {
node.element = cloneAndModifyElement(node.element, {
// Use protoypical inheritance to not get O(n^2) complexity...
providerIndices:
Object.create(currentParent ? currentParent.element.providerIndices : null),
});
@ -284,43 +285,49 @@ function _createViewNodes(view: ViewData) {
const nodes = view.nodes;
for (let i = 0; i < def.nodes.length; i++) {
const nodeDef = def.nodes[i];
let nodeData: any;
// As the current node is being created, we have to use
// the parent node as the current node for error messages, ...
setCurrentNode(view, nodeDef.parent);
switch (nodeDef.type) {
case NodeType.Element:
nodeData = createElement(view, renderHost, nodeDef);
nodes[i] = createElement(view, renderHost, nodeDef) as any;
break;
case NodeType.Text:
nodeData = createText(view, renderHost, nodeDef);
nodes[i] = createText(view, renderHost, nodeDef) as any;
break;
case NodeType.Provider:
let componentView: ViewData;
if (nodeDef.provider.component) {
const hostElIndex = nodeDef.parent;
componentView = createView(
view.services, view, hostElIndex, resolveViewDefinition(nodeDef.provider.component));
}
const providerData = nodeData = createProvider(view, nodeDef, componentView);
if (componentView) {
initView(componentView, providerData.instance, providerData.instance);
// Components can inject a ChangeDetectorRef that needs a references to
// the component view. Therefore, we create the component view first
// and set the ProviderData in ViewData, and then instantiate the provider.
const componentView = createView(
view.services, view, nodeDef.parent,
resolveViewDefinition(nodeDef.provider.component));
const providerData = <ProviderData>{componentView, instance: undefined};
nodes[i] = providerData as any;
const instance = providerData.instance = createProviderInstance(view, nodeDef);
initView(componentView, instance, instance);
} else {
const instance = createProviderInstance(view, nodeDef);
const providerData = <ProviderData>{componentView: undefined, instance};
nodes[i] = providerData as any;
}
break;
case NodeType.PureExpression:
nodeData = createPureExpression(view, nodeDef);
nodes[i] = createPureExpression(view, nodeDef) as any;
break;
case NodeType.Query:
nodeData = createQuery();
nodes[i] = createQuery() as any;
break;
case NodeType.NgContent:
appendNgContent(view, renderHost, nodeDef);
// no runtime data needed for NgContent...
nodeData = undefined;
nodes[i] = undefined;
break;
}
nodes[i] = nodeData;
}
// Create the ViewData.nodes of component views after we created everything else,
// so that e.g. ng-content works
execComponentViewsAction(view, ViewAction.CreateViewNodes);
}