feat(ivy): support for the ngForOf directive, with tests (#21430)
Implement NgOnChangesFeature, ViewContainerRef, TemplateRef, and the renderEmbeddedTemplate instruction, and wire together the pieces required for the ngForOf directive to work. PR Close #21430
This commit is contained in:

committed by
Miško Hevery

parent
6472661ae8
commit
0ad02de47e
@ -6,13 +6,15 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {SimpleChange} from '../change_detection/change_detection_util';
|
||||
import {OnChanges, SimpleChanges} from '../metadata/lifecycle_hooks';
|
||||
import {RendererType2} from '../render/api';
|
||||
import {Type} from '../type';
|
||||
import {resolveRendererType2} from '../view/util';
|
||||
|
||||
import {diPublic} from './di';
|
||||
import {componentRefresh} from './instructions';
|
||||
import {ComponentDef, ComponentDefArgs, DirectiveDef, DirectiveDefArgs} from './interfaces/definition';
|
||||
import {ComponentDef, ComponentDefArgs, DirectiveDef, DirectiveDefArgs, TypedDirectiveDef} from './interfaces/definition';
|
||||
|
||||
|
||||
|
||||
@ -54,8 +56,57 @@ export function defineComponent<T>(componentDefinition: ComponentDefArgs<T>): Co
|
||||
return def;
|
||||
}
|
||||
|
||||
export function NgOnChangesFeature<T>(definition: DirectiveDef<T>) {
|
||||
// TODO: implement. See: https://app.asana.com/0/443577627818617/465170715764659
|
||||
|
||||
const PRIVATE_PREFIX = '__ngOnChanges_';
|
||||
|
||||
type OnChangesExpando = OnChanges & {
|
||||
__ngOnChanges_: SimpleChanges|null|undefined;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export function NgOnChangesFeature<T>(type: Type<T>): (definition: DirectiveDef<any>) => void {
|
||||
return function(definition: DirectiveDef<any>): void {
|
||||
const inputs = definition.inputs;
|
||||
const proto = type.prototype;
|
||||
// Place where we will store SimpleChanges if there is a change
|
||||
Object.defineProperty(proto, PRIVATE_PREFIX, {value: undefined, writable: true});
|
||||
for (let pubKey in inputs) {
|
||||
const minKey = inputs[pubKey];
|
||||
const privateMinKey = PRIVATE_PREFIX + minKey;
|
||||
// Create a place where the actual value will be stored and make it non-enumerable
|
||||
Object.defineProperty(proto, privateMinKey, {value: undefined, writable: true});
|
||||
|
||||
const existingDesc = Object.getOwnPropertyDescriptor(proto, minKey);
|
||||
|
||||
// create a getter and setter for property
|
||||
Object.defineProperty(proto, minKey, {
|
||||
get: function(this: OnChangesExpando) {
|
||||
return (existingDesc && existingDesc.get) ? existingDesc.get.call(this) :
|
||||
this[privateMinKey];
|
||||
},
|
||||
set: function(this: OnChangesExpando, value: any) {
|
||||
let simpleChanges = this[PRIVATE_PREFIX];
|
||||
let isFirstChange = simpleChanges === undefined;
|
||||
if (simpleChanges == null) {
|
||||
simpleChanges = this[PRIVATE_PREFIX] = {};
|
||||
}
|
||||
simpleChanges[pubKey] = new SimpleChange(this[privateMinKey], value, isFirstChange);
|
||||
(existingDesc && existingDesc.set) ? existingDesc.set.call(this, value) :
|
||||
this[privateMinKey] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
proto.ngDoCheck = (function(delegateDoCheck) {
|
||||
return function(this: OnChangesExpando) {
|
||||
let simpleChanges = this[PRIVATE_PREFIX];
|
||||
if (simpleChanges != null) {
|
||||
this.ngOnChanges(simpleChanges);
|
||||
this[PRIVATE_PREFIX] = null;
|
||||
}
|
||||
delegateDoCheck && delegateDoCheck.apply(this);
|
||||
};
|
||||
})(proto.ngDoCheck);
|
||||
};
|
||||
}
|
||||
|
||||
export function PublicFeature<T>(definition: DirectiveDef<T>) {
|
||||
|
@ -17,12 +17,16 @@ import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_co
|
||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_ViewRef} from '../linker/view_ref';
|
||||
import {Type} from '../type';
|
||||
|
||||
import {assertPreviousIsParent, getPreviousOrParentNode} from './instructions';
|
||||
import {assertLessThan} from './assert';
|
||||
import {assertPreviousIsParent, getPreviousOrParentNode, getRenderer, renderEmbeddedTemplate} from './instructions';
|
||||
import {ComponentTemplate, DirectiveDef, TypedDirectiveDef} from './interfaces/definition';
|
||||
import {LInjector} from './interfaces/injector';
|
||||
import {LContainerNode, LElementNode, LNode, LNodeFlags} from './interfaces/node';
|
||||
import {LContainerNode, LElementNode, LNode, LNodeFlags, LViewNode} from './interfaces/node';
|
||||
import {QueryReadType} from './interfaces/query';
|
||||
import {Renderer3} from './interfaces/renderer';
|
||||
import {LView} from './interfaces/view';
|
||||
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
||||
import {insertView} from './node_manipulation';
|
||||
import {notImplemented, stringify} from './util';
|
||||
|
||||
|
||||
@ -189,8 +193,8 @@ export function diPublic(def: TypedDirectiveDef<any>): void {
|
||||
* @param flags Injection flags (e.g. CheckParent)
|
||||
* @returns The instance found
|
||||
*/
|
||||
export function inject<T>(token: Type<T>, flags?: InjectFlags): T {
|
||||
return getOrCreateInjectable<T>(getOrCreateNodeInjector(), token, flags);
|
||||
export function inject<T>(token: Type<T>, flags?: InjectFlags, defaultValue?: T): T {
|
||||
return getOrCreateInjectable<T>(getOrCreateNodeInjector(), token, flags, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,7 +244,8 @@ export function injectViewContainerRef(): viewEngine_ViewContainerRef {
|
||||
* @param flags Injection flags (e.g. CheckParent)
|
||||
* @returns The instance found
|
||||
*/
|
||||
export function getOrCreateInjectable<T>(di: LInjector, token: Type<T>, flags?: InjectFlags): T {
|
||||
export function getOrCreateInjectable<T>(
|
||||
di: LInjector, token: Type<T>, flags?: InjectFlags, defaultValue?: T): T {
|
||||
const bloomHash = bloomHashBit(token);
|
||||
|
||||
// If the token has a bloom hash, then it is a directive that is public to the injection system
|
||||
@ -248,6 +253,9 @@ export function getOrCreateInjectable<T>(di: LInjector, token: Type<T>, flags?:
|
||||
if (bloomHash === null) {
|
||||
const moduleInjector = di.injector;
|
||||
if (!moduleInjector) {
|
||||
if (defaultValue != null) {
|
||||
return defaultValue;
|
||||
}
|
||||
throw createInjectionError('NotFound', token);
|
||||
}
|
||||
moduleInjector.get(token);
|
||||
@ -418,31 +426,6 @@ class ElementRef implements viewEngine_ElementRef {
|
||||
constructor(nativeElement: any) { this.nativeElement = nativeElement; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TemplateRef and stores it on the injector. Or, if the TemplateRef already
|
||||
* exists, retrieves the existing TemplateRef.
|
||||
*
|
||||
* @param di The node injector where we should store a created TemplateRef
|
||||
* @returns The TemplateRef instance to use
|
||||
*/
|
||||
export function getOrCreateTemplateRef<T>(di: LInjector): viewEngine_TemplateRef<T> {
|
||||
ngDevMode && assertNodeType(di.node, LNodeFlags.Container);
|
||||
const data = (di.node as LContainerNode).data;
|
||||
return di.templateRef ||
|
||||
(di.templateRef = new TemplateRef<any>(getOrCreateElementRef(di), data.template));
|
||||
}
|
||||
|
||||
/** A ref to a particular template. */
|
||||
class TemplateRef<T> implements viewEngine_TemplateRef<T> {
|
||||
readonly elementRef: viewEngine_ElementRef;
|
||||
|
||||
constructor(elementRef: viewEngine_ElementRef, template: ComponentTemplate<T>|null) {
|
||||
this.elementRef = elementRef;
|
||||
}
|
||||
|
||||
createEmbeddedView(context: T): viewEngine_EmbeddedViewRef<T> { throw notImplemented(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ViewContainerRef and stores it on the injector. Or, if the ViewContainerRef
|
||||
* already exists, retrieves the existing ViewContainerRef.
|
||||
@ -463,7 +446,7 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
||||
injector: Injector;
|
||||
parentInjector: Injector;
|
||||
|
||||
constructor(node: LContainerNode) {}
|
||||
constructor(private _node: LContainerNode) {}
|
||||
|
||||
clear(): void { throw notImplemented(); }
|
||||
get(index: number): viewEngine_ViewRef|null { throw notImplemented(); }
|
||||
@ -471,7 +454,9 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
||||
createEmbeddedView<C>(
|
||||
templateRef: viewEngine_TemplateRef<C>, context?: C|undefined,
|
||||
index?: number|undefined): viewEngine_EmbeddedViewRef<C> {
|
||||
throw notImplemented();
|
||||
const viewRef = templateRef.createEmbeddedView(context !);
|
||||
this.insert(viewRef, index);
|
||||
return viewRef;
|
||||
}
|
||||
createComponent<C>(
|
||||
componentFactory: viewEngine_ComponentFactory<C>, index?: number|undefined,
|
||||
@ -480,7 +465,29 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
||||
throw notImplemented();
|
||||
}
|
||||
insert(viewRef: viewEngine_ViewRef, index?: number|undefined): viewEngine_ViewRef {
|
||||
throw notImplemented();
|
||||
if (index == null) {
|
||||
index = this._node.data.views.length;
|
||||
} else {
|
||||
// +1 because it's legal to insert at the end.
|
||||
ngDevMode && assertLessThan(index, this._node.data.views.length + 1, 'index');
|
||||
}
|
||||
const lView = (viewRef as EmbeddedViewRef<any>)._lViewNode;
|
||||
insertView(this._node, lView, index);
|
||||
|
||||
// If the view is dynamic (has a template), it needs to be counted both at the container
|
||||
// level and at the node above the container.
|
||||
if (lView.data.template !== null) {
|
||||
// Increment the container view count.
|
||||
this._node.data.dynamicViewCount++;
|
||||
|
||||
// Look for the parent node and increment its dynamic view count.
|
||||
if (this._node.parent !== null && this._node.parent.data !== null) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(this._node.parent, LNodeFlags.View, LNodeFlags.Element);
|
||||
this._node.parent.data.dynamicViewCount++;
|
||||
}
|
||||
}
|
||||
return viewRef;
|
||||
}
|
||||
move(viewRef: viewEngine_ViewRef, currentIndex: number): viewEngine_ViewRef {
|
||||
throw notImplemented();
|
||||
@ -489,3 +496,57 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
||||
remove(index?: number|undefined): void { throw notImplemented(); }
|
||||
detach(index?: number|undefined): viewEngine_ViewRef|null { throw notImplemented(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TemplateRef and stores it on the injector. Or, if the TemplateRef already
|
||||
* exists, retrieves the existing TemplateRef.
|
||||
*
|
||||
* @param di The node injector where we should store a created TemplateRef
|
||||
* @returns The TemplateRef instance to use
|
||||
*/
|
||||
export function getOrCreateTemplateRef<T>(di: LInjector): viewEngine_TemplateRef<T> {
|
||||
ngDevMode && assertNodeType(di.node, LNodeFlags.Container);
|
||||
const data = (di.node as LContainerNode).data;
|
||||
return di.templateRef || (di.templateRef = new TemplateRef<any>(
|
||||
getOrCreateElementRef(di), data.template !, getRenderer()));
|
||||
}
|
||||
|
||||
class TemplateRef<T> implements viewEngine_TemplateRef<T> {
|
||||
readonly elementRef: viewEngine_ElementRef;
|
||||
private _template: ComponentTemplate<T>;
|
||||
|
||||
constructor(
|
||||
elementRef: viewEngine_ElementRef, template: ComponentTemplate<T>,
|
||||
private _renderer: Renderer3) {
|
||||
this.elementRef = elementRef;
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
createEmbeddedView(context: T): viewEngine_EmbeddedViewRef<T> {
|
||||
let viewNode: LViewNode = renderEmbeddedTemplate(null, this._template, context, this._renderer);
|
||||
return new EmbeddedViewRef(viewNode, this._template, context);
|
||||
}
|
||||
}
|
||||
|
||||
class EmbeddedViewRef<T> implements viewEngine_EmbeddedViewRef<T> {
|
||||
context: T;
|
||||
rootNodes: any[];
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_lViewNode: LViewNode;
|
||||
|
||||
constructor(viewNode: LViewNode, template: ComponentTemplate<T>, context: T) {
|
||||
this._lViewNode = viewNode;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
destroy(): void { notImplemented(); }
|
||||
destroyed: boolean;
|
||||
onDestroy(callback: Function) { notImplemented(); }
|
||||
markForCheck(): void { notImplemented(); }
|
||||
detach(): void { notImplemented(); }
|
||||
detectChanges(): void { notImplemented(); }
|
||||
checkNoChanges(): void { notImplemented(); }
|
||||
reattach(): void { notImplemented(); }
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import {LQuery, QueryReadType} from './interfaces/query';
|
||||
import {LView, TData, TView} from './interfaces/view';
|
||||
|
||||
import {LContainerNode, LElementNode, LNode, LNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue,} from './interfaces/node';
|
||||
import {assertNodeType} from './node_assert';
|
||||
import {assertNodeType, assertNodeOfPossibleTypes} from './node_assert';
|
||||
import {appendChild, insertChild, insertView, processProjectedNode, removeView} from './node_manipulation';
|
||||
import {isNodeMatchingSelector} from './node_selector_matcher';
|
||||
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, TypedDirectiveDef, TypedComponentDef} from './interfaces/definition';
|
||||
@ -173,7 +173,9 @@ export function leaveView(newView: LView): void {
|
||||
enterView(newView, null);
|
||||
}
|
||||
|
||||
export function createLView(viewId: number, renderer: Renderer3, tView: TView): LView {
|
||||
export function createLView(
|
||||
viewId: number, renderer: Renderer3, tView: TView,
|
||||
template: ComponentTemplate<any>| null = null, context: any | null = null): LView {
|
||||
const newView = {
|
||||
parent: currentView,
|
||||
id: viewId, // -1 for component views
|
||||
@ -187,7 +189,10 @@ export function createLView(viewId: number, renderer: Renderer3, tView: TView):
|
||||
next: null,
|
||||
bindingStartIndex: null,
|
||||
creationMode: true,
|
||||
viewHookStartIndex: null
|
||||
viewHookStartIndex: null,
|
||||
template: template,
|
||||
context: context,
|
||||
dynamicViewCount: 0,
|
||||
};
|
||||
|
||||
return newView;
|
||||
@ -305,6 +310,32 @@ export function renderTemplate<T>(
|
||||
return host;
|
||||
}
|
||||
|
||||
export function renderEmbeddedTemplate<T>(
|
||||
viewNode: LViewNode | null, template: ComponentTemplate<T>, context: T,
|
||||
renderer: Renderer3): LViewNode {
|
||||
const _isParent = isParent;
|
||||
const _previousOrParentNode = previousOrParentNode;
|
||||
try {
|
||||
isParent = true;
|
||||
previousOrParentNode = null !;
|
||||
let cm: boolean = false;
|
||||
if (viewNode == null) {
|
||||
const view = createLView(-1, renderer, {data: []}, template, context);
|
||||
viewNode = createLNode(null, LNodeFlags.View, null, view);
|
||||
cm = true;
|
||||
}
|
||||
enterView(viewNode.data, viewNode);
|
||||
|
||||
template(context, cm);
|
||||
} finally {
|
||||
refreshDynamicChildren();
|
||||
leaveView(currentView !.parent !);
|
||||
isParent = _isParent;
|
||||
previousOrParentNode = _previousOrParentNode;
|
||||
}
|
||||
return viewNode;
|
||||
}
|
||||
|
||||
export function renderComponentOrTemplate<T>(
|
||||
node: LElementNode, hostView: LView, componentOrContext: T, template?: ComponentTemplate<T>) {
|
||||
const oldView = enterView(hostView, node);
|
||||
@ -1045,7 +1076,8 @@ export function container(
|
||||
nextIndex: 0, renderParent,
|
||||
template: template == null ? null : template,
|
||||
next: null,
|
||||
parent: currentView
|
||||
parent: currentView,
|
||||
dynamicViewCount: 0,
|
||||
});
|
||||
|
||||
if (node.tNode == null) {
|
||||
@ -1101,6 +1133,18 @@ export function containerRefreshEnd(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDynamicChildren() {
|
||||
for (let current = currentView.child; current !== null; current = current.next) {
|
||||
if (current.dynamicViewCount !== 0 && (current as LContainer).views) {
|
||||
const container = current as LContainer;
|
||||
for (let i = 0; i < container.views.length; i++) {
|
||||
const view = container.views[i];
|
||||
renderEmbeddedTemplate(view, view.data.template !, view.data.context !, renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an LViewNode.
|
||||
*
|
||||
@ -1160,22 +1204,25 @@ export function viewEnd(): void {
|
||||
isParent = false;
|
||||
const viewNode = previousOrParentNode = currentView.node as LViewNode;
|
||||
const container = previousOrParentNode.parent as LContainerNode;
|
||||
ngDevMode && assertNodeType(viewNode, LNodeFlags.View);
|
||||
ngDevMode && assertNodeType(container, LNodeFlags.Container);
|
||||
const lContainer = container.data;
|
||||
const previousView = lContainer.nextIndex <= lContainer.views.length ?
|
||||
lContainer.views[lContainer.nextIndex - 1] as LViewNode :
|
||||
null;
|
||||
const viewIdChanged = previousView == null ? true : previousView.data.id !== viewNode.data.id;
|
||||
if (container) {
|
||||
ngDevMode && assertNodeType(viewNode, LNodeFlags.View);
|
||||
ngDevMode && assertNodeType(container, LNodeFlags.Container);
|
||||
const containerState = container.data;
|
||||
const previousView = containerState.nextIndex <= containerState.views.length ?
|
||||
containerState.views[containerState.nextIndex - 1] as LViewNode :
|
||||
null;
|
||||
const viewIdChanged = previousView == null ? true : previousView.data.id !== viewNode.data.id;
|
||||
|
||||
if (viewIdChanged) {
|
||||
insertView(container, viewNode, lContainer.nextIndex - 1);
|
||||
currentView.creationMode = false;
|
||||
if (viewIdChanged) {
|
||||
insertView(container, viewNode, containerState.nextIndex - 1);
|
||||
currentView.creationMode = false;
|
||||
}
|
||||
}
|
||||
leaveView(currentView !.parent !);
|
||||
ngDevMode && assertEqual(isParent, false, 'isParent');
|
||||
ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.View);
|
||||
}
|
||||
|
||||
/////////////
|
||||
|
||||
/**
|
||||
@ -1193,7 +1240,7 @@ export const componentRefresh:
|
||||
directiveIndex: number, elementIndex: number, template: ComponentTemplate<T>) {
|
||||
ngDevMode && assertDataInRange(elementIndex);
|
||||
const element = data ![elementIndex] as LElementNode;
|
||||
ngDevMode && assertNodeType(element, LNodeFlags.Element);
|
||||
ngDevMode && assertNodeOfPossibleTypes(element, LNodeFlags.Element, LNodeFlags.Container);
|
||||
ngDevMode && assertNotEqual(element.data, null, 'isComponent');
|
||||
ngDevMode && assertDataInRange(directiveIndex);
|
||||
const hostView = element.data !;
|
||||
@ -1204,6 +1251,7 @@ export const componentRefresh:
|
||||
template(directive, creationMode);
|
||||
} finally {
|
||||
hostView.creationMode = false;
|
||||
refreshDynamicChildren();
|
||||
leaveView(oldView);
|
||||
}
|
||||
};
|
||||
@ -1827,6 +1875,10 @@ export function getPreviousOrParentNode(): LNode {
|
||||
return previousOrParentNode;
|
||||
}
|
||||
|
||||
export function getRenderer(): Renderer3 {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
export function assertPreviousIsParent() {
|
||||
assertEqual(isParent, true, 'isParent');
|
||||
}
|
||||
|
@ -66,6 +66,13 @@ export interface LContainer {
|
||||
* The template extracted from the location of the Container.
|
||||
*/
|
||||
readonly template: ComponentTemplate<any>|null;
|
||||
|
||||
|
||||
/**
|
||||
* A count of dynamic views rendered into this container. If this is non-zero, the `views` array
|
||||
* will be traversed when refreshing dynamic views on this container.
|
||||
*/
|
||||
dynamicViewCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,6 @@ import {resolveRendererType2} from '../../view/util';
|
||||
export type ComponentTemplate<T> = {
|
||||
(ctx: T, creationMode: boolean): void; ngPrivateData?: never;
|
||||
};
|
||||
export type EmbeddedTemplate<T> = (ctx: T) => void;
|
||||
|
||||
export interface ComponentType<T> extends Type<T> { ngComponentDef: ComponentDef<T>; }
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {LContainer} from './container';
|
||||
import {DirectiveDef} from './definition';
|
||||
import {ComponentTemplate, DirectiveDef} from './definition';
|
||||
import {LElementNode, LViewNode, TNode} from './node';
|
||||
import {Renderer3} from './renderer';
|
||||
|
||||
@ -138,6 +138,24 @@ export interface LView {
|
||||
* directive defs are stored).
|
||||
*/
|
||||
tView: TView;
|
||||
|
||||
/**
|
||||
* For dynamically inserted views, the template function to refresh the view.
|
||||
*/
|
||||
template: ComponentTemplate<{}>|null;
|
||||
|
||||
/**
|
||||
* For embedded views, the context with which to render the template.
|
||||
*/
|
||||
context: {}|null;
|
||||
|
||||
/**
|
||||
* A count of dynamic views that are children of this view (indirectly via containers).
|
||||
*
|
||||
* This is used to decide whether to scan children of this view when refreshing dynamic views
|
||||
* after refreshing the view itself.
|
||||
*/
|
||||
dynamicViewCount: number;
|
||||
}
|
||||
|
||||
/** Interface necessary to work with view tree traversal */
|
||||
|
Reference in New Issue
Block a user