From e56c8bf8d1b537a7dbb4d83eb5bde29147e05a79 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 15 Nov 2018 08:43:56 -0800 Subject: [PATCH] fix(ivy): align discovery methods for consistency (#27117) PR Close #27117 --- .../differs/keyvalue_differs.ts | 9 +- packages/core/src/di/r3_injector.ts | 1 + packages/core/src/render3/component.ts | 36 +- packages/core/src/render3/component_ref.ts | 4 +- .../core/src/render3/context_discovery.ts | 18 +- packages/core/src/render3/debug.ts | 5 +- packages/core/src/render3/discovery_utils.ts | 149 +++++--- packages/core/src/render3/global_utils.ts | 11 +- packages/core/src/render3/global_utils_api.ts | 3 +- packages/core/src/render3/index.ts | 4 +- packages/core/src/render3/instructions.ts | 32 +- .../bundle.golden_symbols.json | 6 +- .../test/render3/change_detection_spec.ts | 5 +- packages/core/test/render3/component_spec.ts | 3 +- packages/core/test/render3/di_spec.ts | 3 +- .../core/test/render3/discovery_utils_spec.ts | 335 +++++++++++++----- .../core/test/render3/global_utils_spec.ts | 11 +- packages/core/test/render3/render_util.ts | 4 +- .../styling/class_and_style_bindings_spec.ts | 2 +- tools/public_api_guard/core/core.d.ts | 1 + tools/public_api_guard/global_utils.d.ts | 8 +- 21 files changed, 433 insertions(+), 217 deletions(-) diff --git a/packages/core/src/change_detection/differs/keyvalue_differs.ts b/packages/core/src/change_detection/differs/keyvalue_differs.ts index 5ce392b9ad..c2670339cd 100644 --- a/packages/core/src/change_detection/differs/keyvalue_differs.ts +++ b/packages/core/src/change_detection/differs/keyvalue_differs.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Optional, SkipSelf, StaticProvider} from '../../di'; +import {Optional, SkipSelf, StaticProvider, defineInjectable} from '../../di'; +import {DefaultKeyValueDifferFactory} from './default_keyvalue_differ'; /** @@ -116,6 +117,12 @@ export interface KeyValueDifferFactory { * @publicApi */ export class KeyValueDiffers { + /** @nocollapse */ + static ngInjectableDef = defineInjectable({ + providedIn: 'root', + factory: () => new KeyValueDiffers([new DefaultKeyValueDifferFactory()]) + }); + /** * @deprecated v4.0.0 - Should be private. */ diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index 6bd99964a6..b91b9b283c 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -200,6 +200,7 @@ export class R3Injector { defOrWrappedDef: InjectorType|InjectorTypeWithProviders, parents: InjectorType[], dedupStack: InjectorType[]) { defOrWrappedDef = resolveForwardRef(defOrWrappedDef); + if (!defOrWrappedDef) return; // Either the defOrWrappedDef is an InjectorType (with ngInjectorDef) or an // InjectorDefTypeWithProviders (aka ModuleWithProviders). Detecting either is a megamorphic diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index c900f17589..d420894e03 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -13,9 +13,10 @@ import {Injector} from '../di/injector'; import {Sanitizer} from '../sanitization/security'; import {assertComponentType, assertDefined} from './assert'; -import {getComponentViewByInstance} from './context_discovery'; +import {getContext} from './context_discovery'; import {getComponentDef} from './definition'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; +import {getHostElement} from './discovery_utils'; import {publishDefaultGlobalUtils} from './global_utils'; import {queueInitHooks, queueLifecycleHooks} from './hooks'; import {CLEAN_PROMISE, createLViewData, createNodeAtIndex, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, prefillHostVars, queueComponentIndexForCheck, refreshDescendantViews} from './instructions'; @@ -28,6 +29,7 @@ import {enterView, leaveView, resetComponentState} from './state'; import {defaultScheduler, getRootView, readElementValue, readPatchedLViewData, stringify} from './util'; + /** Options that control how the component should be bootstrapped. */ export interface CreateComponentOptions { /** Which renderer factory to use. */ @@ -121,8 +123,8 @@ export function renderComponent( const renderer = rendererFactory.createRenderer(hostRNode, componentDef); const rootView: LViewData = createLViewData( - renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined, - opts.injector || null); + null, renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, + undefined, opts.injector || null); const oldView = enterView(rootView, null); let component: T; @@ -159,7 +161,7 @@ export function createRootComponentView( resetComponentState(); const tView = rootView[TVIEW]; const componentView = createLViewData( - renderer, + rootView, renderer, getOrCreateTView( def.template, def.consts, def.vars, def.directiveDefs, def.pipeDefs, def.viewQuery), null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, sanitizer); @@ -243,32 +245,6 @@ function getRootContext(component: any): RootContext { return rootContext; } -/** - * Retrieve the host element of the component. - * - * Use this function to retrieve the host element of the component. The host - * element is the element which the component is associated with. - * - * @param component Component for which the host element should be retrieved. - */ -export function getHostElement(component: T): HTMLElement { - return readElementValue(getComponentViewByInstance(component)) as HTMLElement; -} - -/** - * Retrieves the rendered text for a given component. - * - * This function retrieves the host element of a component and - * and then returns the `textContent` for that element. This implies - * that the text returned will include re-projected content of - * the component as well. - * - * @param component The component to return the content text for. - */ -export function getRenderedText(component: any): string { - const hostElement = getHostElement(component); - return hostElement.textContent || ''; -} /** * Wait on component until it is rendered. diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 3bb4c25088..7fe055caf5 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -151,8 +151,8 @@ export class ComponentFactory extends viewEngine_ComponentFactory { // Create the root view. Uses empty TView and ContentTemplate. const rootView: LViewData = createLViewData( - renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined, - rootViewInjector); + null, renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, + undefined, rootViewInjector); // rootView is the parent when bootstrapping const oldView = enterView(rootView, null); diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index b3e76419e3..c11da7adb5 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -8,6 +8,7 @@ import './ng_dev_mode'; import {assertEqual} from './assert'; +import {EMPTY_ARRAY} from './definition'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {TNode, TNodeFlags} from './interfaces/node'; import {RElement} from './interfaces/renderer'; @@ -15,6 +16,7 @@ import {CONTEXT, HEADER_OFFSET, HOST, LViewData, TVIEW} from './interfaces/view' import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util'; + /** Returns the matching `LContext` data for a given DOM node, directive or component instance. * * This function will examine the provided DOM element, component, or directive instance\'s @@ -31,6 +33,8 @@ import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatched * will be updated with a new context (which is then returned). If the monkey-patch value is not * detected for a component/directive instance then it will throw an error (all components and * directives should be automatically monkey-patched by ivy). + * + * @param target Component, Directive or DOM Node. */ export function getContext(target: any): LContext|null { let mpValue = readPatchedData(target); @@ -54,7 +58,7 @@ export function getContext(target: any): LContext|null { if (nodeIndex == -1) { throw new Error('The provided directive was not found in the application'); } - directives = discoverDirectives(nodeIndex, lViewData, false); + directives = getDirectivesAtNodeIndex(nodeIndex, lViewData, false); } else { nodeIndex = findViaNativeElement(lViewData, target as RElement); if (nodeIndex == -1) { @@ -132,7 +136,8 @@ export function getContext(target: any): LContext|null { function createLContext(lViewData: LViewData, nodeIndex: number, native: RElement): LContext { return { lViewData, - nodeIndex: nodeIndex, native, + nodeIndex, + native, component: undefined, directives: undefined, localRefs: undefined, @@ -271,15 +276,22 @@ function assertDomElement(element: any) { * @param lViewData The target view data * @param includeComponents Whether or not to include components in returned directives */ -export function discoverDirectives( +export function getDirectivesAtNodeIndex( nodeIndex: number, lViewData: LViewData, includeComponents: boolean): any[]|null { const tNode = lViewData[TVIEW].data[nodeIndex] as TNode; let directiveStartIndex = getDirectiveStartIndex(tNode); + if (directiveStartIndex == 0) return EMPTY_ARRAY; const directiveEndIndex = getDirectiveEndIndex(tNode, directiveStartIndex); if (!includeComponents && tNode.flags & TNodeFlags.isComponent) directiveStartIndex++; return lViewData.slice(directiveStartIndex, directiveEndIndex); } +export function getComponentAtNodeIndex(nodeIndex: number, lViewData: LViewData): {}|null { + const tNode = lViewData[TVIEW].data[nodeIndex] as TNode; + let directiveStartIndex = getDirectiveStartIndex(tNode); + return tNode.flags & TNodeFlags.isComponent ? lViewData[directiveStartIndex] : null; +} + /** * Returns a map of local references (local reference name => element or directive instance) that * exist on a given element. diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/debug.ts index 968a46cdcf..4fadccd52c 100644 --- a/packages/core/src/render3/debug.ts +++ b/packages/core/src/render3/debug.ts @@ -11,11 +11,12 @@ import {Renderer2, RendererType2} from '../render/api'; import {DebugContext} from '../view'; import {DebugRenderer2, DebugRendererFactory2} from '../view/services'; -import {getHostComponent, getInjector, getLocalRefs, loadContext} from './discovery_utils'; +import {getComponent, getInjector, getLocalRefs, loadContext} from './discovery_utils'; import {DirectiveDef} from './interfaces/definition'; import {TNode, TNodeFlags} from './interfaces/node'; import {TVIEW} from './interfaces/view'; + /** * Adapts the DebugRendererFactory2 to create a DebugRenderer2 specific for IVY. * @@ -43,7 +44,7 @@ class Render3DebugContext implements DebugContext { get injector(): Injector { return getInjector(this._nativeNode); } - get component(): any { return getHostComponent(this._nativeNode); } + get component(): any { return getComponent(this._nativeNode); } get providerTokens(): any[] { const lDebugCtx = loadContext(this._nativeNode); diff --git a/packages/core/src/render3/discovery_utils.ts b/packages/core/src/render3/discovery_utils.ts index 10d1fcdfed..eb75cc4f8a 100644 --- a/packages/core/src/render3/discovery_utils.ts +++ b/packages/core/src/render3/discovery_utils.ts @@ -8,78 +8,83 @@ import {Injector} from '../di/injector'; import {assertDefined} from './assert'; -import {discoverDirectives, discoverLocalRefs, getContext, isComponentInstance} from './context_discovery'; +import {discoverLocalRefs, getComponentAtNodeIndex, getContext, getDirectivesAtNodeIndex} from './context_discovery'; import {LContext} from './interfaces/context'; -import {TElementNode, TNode, TNodeFlags} from './interfaces/node'; -import {CONTEXT, FLAGS, LViewData, LViewFlags, PARENT, RootContext, TVIEW} from './interfaces/view'; -import {getComponentViewByIndex, readPatchedLViewData} from './util'; +import {TElementNode} from './interfaces/node'; +import {CONTEXT, FLAGS, HOST, LViewData, LViewFlags, PARENT, RootContext, TVIEW} from './interfaces/view'; +import {readPatchedLViewData, stringify} from './util'; import {NodeInjector} from './view_engine_compatibility'; - /** - * NOTE: The following functions might not be ideal for core usage in Angular... + * Returns the component instance associated with a given DOM host element. + * Elements which don't represent components return `null`. * - * Each function below is designed - */ - -/** - * Returns the component instance associated with the target. + * @param element Host DOM element from which the component should be retrieved for. * - * If a DOM is used then it will return the component that - * owns the view where the element is situated. - * If a component instance is used then it will return the - * instance of the parent component depending on where - * the component instance is exists in a template. - * If a directive instance is used then it will return the - * component that contains that directive in it's template. + * ``` + * + * #VIEW + *
+ * + *
+ * + * + * expect(getComponent() instanceof ChildComponent).toBeTruthy(); + * expect(getComponent() instanceof MyApp).toBeTruthy(); + * ``` * * @publicApi */ -export function getComponent(target: {}): T|null { - const context = loadContext(target) !; +export function getComponent(element: Element): T|null { + if (!(element instanceof Node)) throw new Error('Expecting instance of DOM Node'); + + const context = loadContext(element) !; if (context.component === undefined) { - let lViewData: LViewData|null = context.lViewData; - while (lViewData) { - const ctx = lViewData ![CONTEXT] !as{}; - if (ctx && isComponentInstance(ctx)) { - context.component = ctx; - break; - } - lViewData = lViewData[FLAGS] & LViewFlags.IsRoot ? null : lViewData ![PARENT] !; - } - if (context.component === undefined) { - context.component = null; - } + context.component = getComponentAtNodeIndex(context.nodeIndex, context.lViewData); } return context.component as T; } /** - * Returns the host component instance associated with the target. + * Returns the component instance associated with view which owns the DOM element (`null` + * otherwise). * - * This will only return a component instance of the DOM node - * contains an instance of a component on it. + * @param element DOM element which is owned by an existing component's view. + * + * ``` + * + * #VIEW + *
+ * + *
+ * + * + * expect(getViewComponent() instanceof MyApp).toBeTruthy(); + * expect(getViewComponent()).toEqual(null); + * ``` * * @publicApi */ -export function getHostComponent(target: {}): T|null { - const context = loadContext(target); - const tNode = context.lViewData[TVIEW].data[context.nodeIndex] as TNode; - if (tNode.flags & TNodeFlags.isComponent) { - const componentView = getComponentViewByIndex(context.nodeIndex, context.lViewData); - return componentView[CONTEXT] as any as T; +export function getViewComponent(element: Element | {}): T|null { + const context = loadContext(element) !; + let lView: LViewData = context.lViewData; + while (lView[PARENT] && lView[HOST] === null) { + // As long as lView[HOST] is null we know we are part of sub-template such as `*ngIf` + lView = lView[PARENT] !; } - return null; + + return lView[FLAGS] & LViewFlags.IsRoot ? null : lView[CONTEXT] as T; } + + /** * Returns the `RootContext` instance that is associated with * the application where the target is situated. * - * @publicApi */ export function getRootContext(target: LViewData | {}): RootContext { const lViewData = Array.isArray(target) ? target : loadContext(target) !.lViewData; @@ -88,8 +93,11 @@ export function getRootContext(target: LViewData | {}): RootContext { } /** - * Returns a list of all the components in the application - * that are have been bootstrapped. + * Retrieve all root components. + * + * Root components are those which have been bootstrapped by Angular. + * + * @param target A DOM element, component or directive instance. * * @publicApi */ @@ -98,8 +106,9 @@ export function getRootComponents(target: {}): any[] { } /** - * Returns the injector instance that is associated with - * the element, component or directive. + * Retrieves an `Injector` associated with the element, component or directive. + * + * @param target A DOM element, component or directive instance. * * @publicApi */ @@ -111,8 +120,9 @@ export function getInjector(target: {}): Injector { } /** - * Returns a list of all the directives that are associated - * with the underlying target element. + * Retrieves directives associated with a given DOM host element. + * + * @param target A DOM element, component or directive instance. * * @publicApi */ @@ -120,7 +130,7 @@ export function getDirectives(target: {}): Array<{}> { const context = loadContext(target) !; if (context.directives === undefined) { - context.directives = discoverDirectives(context.nodeIndex, context.lViewData, false); + context.directives = getDirectivesAtNodeIndex(context.nodeIndex, context.lViewData, false); } return context.directives || []; @@ -130,13 +140,12 @@ export function getDirectives(target: {}): Array<{}> { * Returns LContext associated with a target passed as an argument. * Throws if a given target doesn't have associated LContext. * - * @publicApi */ export function loadContext(target: {}): LContext { const context = getContext(target); if (!context) { throw new Error( - ngDevMode ? 'Unable to find the given context data for the given target' : + ngDevMode ? `Unable to find context associated with ${stringify(target)}` : 'Invalid ng target'); } return context; @@ -148,7 +157,6 @@ export function loadContext(target: {}): LContext { * * @param componentOrView any component or view * - * @publicApi */ export function getRootView(componentOrView: LViewData | {}): LViewData { let lViewData: LViewData; @@ -166,7 +174,11 @@ export function getRootView(componentOrView: LViewData | {}): LViewData { } /** - * Retrieve map of local references (local reference name => element or directive instance). + * Retrieve map of local references. + * + * The references are retrieved as a map of local reference name to element or directive instance. + * + * @param target A DOM element, component or directive instance. * * @publicApi */ @@ -179,3 +191,32 @@ export function getLocalRefs(target: {}): {[key: string]: any} { return context.localRefs || {}; } + +/** + * Retrieve the host element of the component. + * + * Use this function to retrieve the host element of the component. The host + * element is the element which the component is associated with. + * + * @param directive Component or Directive for which the host element should be retrieved. + * + * @publicApi + */ +export function getHostElement(directive: T): Element { + return getContext(directive) !.native as never as Element; +} + +/** + * Retrieves the rendered text for a given component. + * + * This function retrieves the host element of a component and + * and then returns the `textContent` for that element. This implies + * that the text returned will include re-projected content of + * the component as well. + * + * @param component The component to return the content text for. + */ +export function getRenderedText(component: any): string { + const hostElement = getHostElement(component); + return hostElement.textContent || ''; +} \ No newline at end of file diff --git a/packages/core/src/render3/global_utils.ts b/packages/core/src/render3/global_utils.ts index 50d3ab726a..666c11f44d 100644 --- a/packages/core/src/render3/global_utils.ts +++ b/packages/core/src/render3/global_utils.ts @@ -6,7 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ import {global} from '../util'; -import {getComponent, getDirectives, getHostComponent, getInjector, getPlayers, getRootComponents} from './global_utils_api'; + +import {assertDefined} from './assert'; +import {getComponent, getDirectives, getHostElement, getInjector, getPlayers, getRootComponents, getViewComponent, markDirty} from './global_utils_api'; + + /** * This file introduces series of globally accessible debug tools @@ -37,11 +41,13 @@ export function publishDefaultGlobalUtils() { if (!_published) { _published = true; publishGlobalUtil('getComponent', getComponent); - publishGlobalUtil('getHostComponent', getHostComponent); + publishGlobalUtil('getViewComponent', getViewComponent); + publishGlobalUtil('getHostElement', getHostElement); publishGlobalUtil('getInjector', getInjector); publishGlobalUtil('getRootComponents', getRootComponents); publishGlobalUtil('getDirectives', getDirectives); publishGlobalUtil('getPlayers', getPlayers); + publishGlobalUtil('markDirty', markDirty); } } @@ -55,6 +61,7 @@ export declare type GlobalDevModeContainer = { */ export function publishGlobalUtil(name: string, fn: Function): void { const w = global as any as GlobalDevModeContainer; + ngDevMode && assertDefined(fn, 'function not defined'); if (w) { let container = w[GLOBAL_PUBLISH_EXPANDO_KEY]; if (!container) { diff --git a/packages/core/src/render3/global_utils_api.ts b/packages/core/src/render3/global_utils_api.ts index dadef7a664..b85207d2a4 100644 --- a/packages/core/src/render3/global_utils_api.ts +++ b/packages/core/src/render3/global_utils_api.ts @@ -15,5 +15,6 @@ * file in the public_api_guard test. */ -export {getComponent, getDirectives, getHostComponent, getInjector, getRootComponents} from './discovery_utils'; +export {getComponent, getDirectives, getHostElement, getInjector, getRootComponents, getViewComponent} from './discovery_utils'; +export {markDirty} from './instructions'; export {getPlayers} from './players'; diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 2f347f8ab2..b77fbf425f 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -5,8 +5,9 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component'; +import {LifecycleHooksFeature, renderComponent, whenRendered} from './component'; import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; +import {getHostElement, getRenderedText} from './discovery_utils'; import {InheritDefinitionFeature} from './features/inherit_definition_feature'; import {NgOnChangesFeature} from './features/ng_onchanges_feature'; import {ProvidersFeature} from './features/providers_feature'; @@ -17,6 +18,7 @@ export {getFactoryOf, getInheritedFactory} from './di'; export {RenderFlags} from './interfaces/definition'; export {CssSelectorList} from './interfaces/projection'; + // clang-format off export { bind, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 3f2b0a5add..b7d9aefd5b 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -163,15 +163,14 @@ function refreshChildComponents( } export function createLViewData( - renderer: Renderer3, tView: TView, context: T | null, flags: LViewFlags, - sanitizer?: Sanitizer | null, injector?: Injector | null): LViewData { - const viewData = getViewData(); + parentViewData: LViewData | null, renderer: Renderer3, tView: TView, context: T | null, + flags: LViewFlags, sanitizer?: Sanitizer | null, injector?: Injector | null): LViewData { const instance = tView.blueprint.slice() as LViewData; instance[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit; - instance[PARENT] = instance[DECLARATION_VIEW] = viewData; + instance[PARENT] = instance[DECLARATION_VIEW] = parentViewData; instance[CONTEXT] = context; instance[INJECTOR as any] = - injector === undefined ? (viewData ? viewData[INJECTOR] : null) : injector; + injector === undefined ? (parentViewData ? parentViewData[INJECTOR] : null) : injector; instance[RENDERER] = renderer; instance[SANITIZER] = sanitizer || null; return instance; @@ -299,16 +298,15 @@ export function renderTemplate( setRenderer(renderer); // We need to create a root view so it's possible to look up the host element through its index - enterView( - createLViewData( - renderer, createTView(-1, null, 1, 0, null, null, null), {}, - LViewFlags.CheckAlways | LViewFlags.IsRoot), - null); + const lView = createLViewData( + null, renderer, createTView(-1, null, 1, 0, null, null, null), {}, + LViewFlags.CheckAlways | LViewFlags.IsRoot); + enterView(lView, null); const componentTView = getOrCreateTView(templateFn, consts, vars, directives || null, pipes || null, null); - hostView = - createLViewData(renderer, componentTView, context, LViewFlags.CheckAlways, sanitizer); + hostView = createLViewData( + lView, renderer, componentTView, context, LViewFlags.CheckAlways, sanitizer); hostView[HOST_NODE] = createNodeAtIndex(0, TNodeType.Element, hostNode, null, null); } renderComponentOrTemplate(hostView, context, null, templateFn); @@ -329,8 +327,8 @@ export function createEmbeddedViewAndNode( setIsParent(true); setPreviousOrParentTNode(null !); - const lView = - createLViewData(renderer, tView, context, LViewFlags.CheckAlways, getCurrentSanitizer()); + const lView = createLViewData( + declarationView, renderer, tView, context, LViewFlags.CheckAlways, getCurrentSanitizer()); lView[DECLARATION_VIEW] = declarationView; if (queries) { @@ -1672,7 +1670,7 @@ function addComponentLogic( const componentView = addToViewTree( viewData, previousOrParentTNode.index as number, createLViewData( - getRendererFactory().createRenderer(native as RElement, def), tView, null, + getViewData(), getRendererFactory().createRenderer(native as RElement, def), tView, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, getCurrentSanitizer())); componentView[HOST_NODE] = previousOrParentTNode as TElementNode; @@ -2000,7 +1998,7 @@ export function embeddedViewStart(viewBlockId: number, consts: number, vars: num } else { // When we create a new LView, we always reset the state of the instructions. viewToRender = createLViewData( - getRenderer(), + getViewData(), getRenderer(), getOrCreateEmbeddedTView(viewBlockId, consts, vars, containerTNode as TContainerNode), null, LViewFlags.CheckAlways, getCurrentSanitizer()); @@ -2469,6 +2467,8 @@ function updateViewQuery( * can be provided. * * @param component Component to mark as dirty. + * + * @publicApi */ export function markDirty(component: T) { ngDevMode && assertDefined(component, 'component'); diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index 0fdfdf721e..8007265b47 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -485,9 +485,6 @@ { "name": "directiveInject" }, - { - "name": "discoverDirectives" - }, { "name": "domRendererFactory3" }, @@ -623,6 +620,9 @@ { "name": "getDirectiveStartIndex" }, + { + "name": "getDirectivesAtNodeIndex" + }, { "name": "getElementDepthCount" }, diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts index bb0c3bedbb..57730b61d7 100644 --- a/packages/core/test/render3/change_detection_spec.ts +++ b/packages/core/test/render3/change_detection_spec.ts @@ -10,8 +10,9 @@ import {TemplateRef, ViewContainerRef} from '@angular/core'; import {withBody} from '@angular/private/testing'; import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core'; -import {getRenderedText, whenRendered} from '../../src/render3/component'; -import {LifecycleHooksFeature, defineComponent, defineDirective, templateRefExtractor} from '../../src/render3/index'; +import {whenRendered} from '../../src/render3/component'; +import {LifecycleHooksFeature, defineComponent, defineDirective, getRenderedText, templateRefExtractor} from '../../src/render3/index'; + import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, listener, markDirty, reference, text, template, textBinding, tick} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer'; diff --git a/packages/core/test/render3/component_spec.ts b/packages/core/test/render3/component_spec.ts index 634053e1f4..6dd4596214 100644 --- a/packages/core/test/render3/component_spec.ts +++ b/packages/core/test/render3/component_spec.ts @@ -7,9 +7,8 @@ */ import {ViewEncapsulation, createInjector, defineInjectable, defineInjector} from '../../src/core'; -import {getRenderedText} from '../../src/render3/component'; -import {AttributeMarker, ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty, template} from '../../src/render3/index'; +import {AttributeMarker, ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty, template, getRenderedText} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, nextContext, text, textBinding, tick} from '../../src/render3/instructions'; import {ComponentDef, RenderFlags} from '../../src/render3/interfaces/definition'; diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 6d832a52ad..2b7648c017 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -2019,7 +2019,8 @@ describe('di', () => { describe('getOrCreateNodeInjector', () => { it('should handle initial undefined state', () => { const contentView = createLViewData( - null !, createTView(-1, null, 1, 0, null, null, null), null, LViewFlags.CheckAlways); + null, null !, createTView(-1, null, 1, 0, null, null, null), null, + LViewFlags.CheckAlways); const oldView = enterView(contentView, null); try { const parentTNode = createNodeAtIndex(0, TNodeType.Element, null, null, null); diff --git a/packages/core/test/render3/discovery_utils_spec.ts b/packages/core/test/render3/discovery_utils_spec.ts index 89a7fc1a96..f0c1229d35 100644 --- a/packages/core/test/render3/discovery_utils_spec.ts +++ b/packages/core/test/render3/discovery_utils_spec.ts @@ -5,59 +5,263 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {ReferenceFilter} from '@angular/compiler'; +import {createInjector} from '@angular/core'; + import {StaticInjector} from '../../src/di/injector'; -import {getComponent, getDirectives, getHostComponent, getInjector, getLocalRefs, getRootComponents} from '../../src/render3/discovery_utils'; -import {RenderFlags, defineComponent, defineDirective} from '../../src/render3/index'; -import {element, elementEnd, elementStart, elementStyling, elementStylingApply} from '../../src/render3/instructions'; +import {getComponent, getDirectives, getInjector, getLocalRefs, getRootComponents, getViewComponent} from '../../src/render3/discovery_utils'; +import {ProvidersFeature, RenderFlags, defineComponent, defineDirective, getHostElement} from '../../src/render3/index'; + +import {container, element, elementEnd, elementStart, elementStyling, elementStylingApply, template, bind, elementProperty, text, textBinding, markDirty} from '../../src/render3/instructions'; import {ComponentFixture} from './render_util'; +import {NgIf} from './common_with_def'; +import {getRootContext} from '@angular/core/src/render3/util'; describe('discovery utils', () => { - describe('getComponent()', () => { - it('should return the component instance for a DOM element', () => { - class InnerComp { - static ngComponentDef = defineComponent({ - type: InnerComp, - selectors: [['inner-comp']], - factory: () => new InnerComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: InnerComp) => { + let fixture: ComponentFixture; + let myApp: MyApp[]; + let dirA: DirectiveA[]; + let childComponent: DirectiveA[]; + let child: NodeListOf; + let span: NodeListOf; + let div: NodeListOf; + let p: NodeListOf; + + beforeEach(() => { + myApp = []; + dirA = []; + childComponent = []; + fixture = new ComponentFixture( + MyApp, {injector: createInjector(null, null, [{provide: String, useValue: 'Module'}])}); + child = fixture.hostElement.querySelectorAll('child'); + span = fixture.hostElement.querySelectorAll('span'); + div = fixture.hostElement.querySelectorAll('div'); + p = fixture.hostElement.querySelectorAll('p'); + }); + + /** + * For all tests assume this set up + * + * ``` + * + * <#VIEW> + * {{text}} + *
+ * + * <#VIEW> + *

+ * + *
+ * + * <#VIEW> + *

+ * + *
+ * + * <#VIEW> + *

+ * + *
+ * + *
+ * ``` + */ + class Child { + constructor() { childComponent.push(this); } + + static ngComponentDef = defineComponent({ + type: Child, + selectors: [['child']], + factory: () => new Child(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: Child) => { + if (rf & RenderFlags.Create) { + element(0, 'p'); + } + }, + features: [ProvidersFeature([{provide: String, useValue: 'Child'}])] + }); + } + + class DirectiveA { + constructor() { dirA.push(this); } + + static ngDirectiveDef = defineDirective({ + type: DirectiveA, + selectors: [['', 'dirA', '']], + exportAs: 'dirA', + factory: () => new DirectiveA(), + }); + } + + class MyApp { + text: string = 'INIT'; + constructor() { myApp.push(this); } + + static ngComponentDef = defineComponent({ + type: MyApp, + selectors: [['my-app']], + factory: () => new MyApp(), + consts: 9, + vars: 1, + directives: [Child, DirectiveA, NgIf], + template: (rf: RenderFlags, ctx: MyApp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'span'); + text(1); + elementEnd(); + element(2, 'div', ['dirA', ''], ['div', '', 'foo', 'dirA']); + element(5, 'child'); + element(6, 'child', ['dirA', ''], ['child', '']); + template(8, function(rf: RenderFlags, ctx: never) { if (rf & RenderFlags.Create) { - element(0, 'div'); + element(0, 'child'); } - } - }); + }, 1, 0, null, ['ngIf', '']); + } + if (rf & RenderFlags.Update) { + textBinding(1, bind(ctx.text)); + elementProperty(8, 'ngIf', bind(true)); + } } + }); + } - class Comp { - static ngComponentDef = defineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - element(0, 'inner-comp'); - } - }, - directives: [InnerComp] - }); - } - - const fixture = new ComponentFixture(Comp); - fixture.update(); - - const hostElm = fixture.hostElement; - const innerCompElm = hostElm.querySelector('inner-comp'); - const component = fixture.component; - - expect(getComponent(innerCompElm !) !).toBe(component); - expect(getComponent(hostElm) !).toBeFalsy(); + describe('getComponent', () => { + it('should return null if no component', () => { + expect(getComponent(span[0])).toEqual(null); + expect(getComponent(div[0])).toEqual(null); + expect(getComponent(p[0])).toEqual(null); + }); + it('should throw when called on non-element', () => { + expect(() => getComponent(dirA[0] as any)).toThrowError(/Expecting instance of DOM Node/); + expect(() => getComponent(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/); + }); + it('should return component from element', () => { + expect(getComponent(fixture.hostElement)).toEqual(myApp[0]); + expect(getComponent(child[0])).toEqual(childComponent[0]); + expect(getComponent(child[1])).toEqual(childComponent[1]); }); }); + describe('getHostElement', () => { + it('should return element on component', () => { + expect(getHostElement(myApp[0])).toEqual(fixture.hostElement); + expect(getHostElement(childComponent[0])).toEqual(child[0]); + expect(getHostElement(childComponent[1])).toEqual(child[1]); + }); + it('should return element on directive', () => { + expect(getHostElement(dirA[0])).toEqual(div[0]); + expect(getHostElement(dirA[1])).toEqual(child[1]); + }); + it('should throw on unknown target', () => { + expect(() => getHostElement({})).toThrowError(); // + }); + }); + + describe('getInjector', () => { + it('should return node-injector from element', () => { + expect(getInjector(fixture.hostElement).get(String)).toEqual('Module'); + expect(getInjector(child[0]).get(String)).toEqual('Child'); + expect(getInjector(p[0]).get(String)).toEqual('Child'); + }); + it('should return node-injector from component with providers', () => { + expect(getInjector(myApp[0]).get(String)).toEqual('Module'); + expect(getInjector(childComponent[0]).get(String)).toEqual('Child'); + expect(getInjector(childComponent[1]).get(String)).toEqual('Child'); + }); + it('should return node-injector from directive without providers', () => { + expect(getInjector(dirA[0]).get(String)).toEqual('Module'); + expect(getInjector(dirA[1]).get(String)).toEqual('Child'); + }); + }); + + describe('getDirectives', () => { + it('should return empty array if no directives', () => { + expect(getDirectives(fixture.hostElement)).toEqual([]); + expect(getDirectives(span[0])).toEqual([]); + expect(getDirectives(child[0])).toEqual([]); + }); + it('should return just directives', () => { + expect(getDirectives(div[0])).toEqual([dirA[0]]); + expect(getDirectives(child[1])).toEqual([dirA[1]]); + }); + }); + + describe('getViewComponent', () => { + it('should return null when called on root component', () => { + expect(getViewComponent(fixture.hostElement)).toEqual(null); + expect(getViewComponent(myApp[0])).toEqual(null); + }); + it('should return containing component of child component', () => { + expect(getViewComponent(child[0])).toEqual(myApp[0]); + expect(getViewComponent(child[1])).toEqual(myApp[0]); + expect(getViewComponent(child[2])).toEqual(myApp[0]); + + expect(getViewComponent(childComponent[0])).toEqual(myApp[0]); + expect(getViewComponent(childComponent[1])).toEqual(myApp[0]); + expect(getViewComponent(childComponent[2])).toEqual(myApp[0]); + }); + it('should return containing component of any view element', () => { + expect(getViewComponent(span[0])).toEqual(myApp[0]); + expect(getViewComponent(div[0])).toEqual(myApp[0]); + expect(getViewComponent(p[0])).toEqual(childComponent[0]); + expect(getViewComponent(p[1])).toEqual(childComponent[1]); + expect(getViewComponent(p[2])).toEqual(childComponent[2]); + }); + it('should return containing component of child directive', () => { + expect(getViewComponent(dirA[0])).toEqual(myApp[0]); + expect(getViewComponent(dirA[1])).toEqual(myApp[0]); + }); + }); + + describe('localRefs', () => { + it('should retrieve empty map', () => { + expect(getLocalRefs(fixture.hostElement)).toEqual({}); + expect(getLocalRefs(myApp[0])).toEqual({}); + expect(getLocalRefs(span[0])).toEqual({}); + expect(getLocalRefs(child[0])).toEqual({}); + }); + + it('should retrieve the local map', () => { + expect(getLocalRefs(div[0])).toEqual({div: div[0], foo: dirA[0]}); + expect(getLocalRefs(dirA[0])).toEqual({div: div[0], foo: dirA[0]}); + + expect(getLocalRefs(child[1])).toEqual({child: childComponent[1]}); + expect(getLocalRefs(dirA[1])).toEqual({child: childComponent[1]}); + }); + }); + + describe('getRootComponents', () => { + it('should return root components from component', () => { + const rootComponents = [myApp[0]]; + expect(getRootComponents(myApp[0])).toEqual(rootComponents); + expect(getRootComponents(childComponent[0])).toEqual(rootComponents); + expect(getRootComponents(childComponent[1])).toEqual(rootComponents); + expect(getRootComponents(dirA[0])).toEqual(rootComponents); + expect(getRootComponents(dirA[1])).toEqual(rootComponents); + expect(getRootComponents(child[0])).toEqual(rootComponents); + expect(getRootComponents(child[1])).toEqual(rootComponents); + expect(getRootComponents(div[0])).toEqual(rootComponents); + expect(getRootComponents(p[0])).toEqual(rootComponents); + }); + }); + + describe('markDirty', () => { + it('should re-render component', () => { + expect(span[0].textContent).toEqual('INIT'); + myApp[0].text = 'WORKS'; + markDirty(myApp[0]); + fixture.requestAnimationFrame.flush(); + expect(span[0].textContent).toEqual('WORKS'); + }); + }); +}); + +describe('discovery utils deprecated', () => { + describe('getRootComponents()', () => { it('should return a list of the root components of the application from an element', () => { let innerComp: InnerComp; @@ -171,55 +375,6 @@ describe('discovery utils', () => { }); }); - describe('getHostComponent()', () => { - it('should return the component instance for a DOM element', () => { - let innerComp: InnerComp; - - class InnerComp { - static ngComponentDef = defineComponent({ - type: InnerComp, - selectors: [['inner-comp']], - factory: () => innerComp = new InnerComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: InnerComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); - } - } - }); - } - - class Comp { - static ngComponentDef = defineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - element(0, 'inner-comp'); - } - }, - directives: [InnerComp] - }); - } - - const fixture = new ComponentFixture(Comp); - fixture.update(); - - const hostElm = fixture.hostElement; - const innerElm = hostElm.querySelector('inner-comp') !; - const divElm = hostElm.querySelector('div') !; - const component = fixture.component; - - expect(getHostComponent(hostElm) !).toBe(component); - expect(getHostComponent(innerElm) !).toBe(innerComp !); - expect(getHostComponent(divElm) !).toBeFalsy(); - }); - }); - describe('getInjector', () => { it('should return an injector that can return directive instances', () => { diff --git a/packages/core/test/render3/global_utils_spec.ts b/packages/core/test/render3/global_utils_spec.ts index 6a158e912a..bece3e429e 100644 --- a/packages/core/test/render3/global_utils_spec.ts +++ b/packages/core/test/render3/global_utils_spec.ts @@ -5,7 +5,9 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {getComponent, getDirectives, getHostComponent, getInjector, getRootComponents} from '../../src/render3/discovery_utils'; +import {ɵmarkDirty as markDirty} from '@angular/core'; + +import {getComponent, getDirectives, getHostElement, getInjector, getRootComponents, getViewComponent} from '../../src/render3/discovery_utils'; import {GLOBAL_PUBLISH_EXPANDO_KEY, GlobalDevModeContainer, publishDefaultGlobalUtils, publishGlobalUtil} from '../../src/render3/global_utils'; import {getPlayers} from '../../src/render3/players'; import {global} from '../../src/util'; @@ -26,16 +28,21 @@ describe('global utils', () => { it('should publish getComponent', () => { assertPublished('getComponent', getComponent); }); + it('should publish getViewComponent', + () => { assertPublished('getViewComponent', getViewComponent); }); + it('should publish getRootComponents', () => { assertPublished('getRootComponents', getRootComponents); }); it('should publish getDirectives', () => { assertPublished('getDirectives', getDirectives); }); it('should publish getHostComponent', - () => { assertPublished('getHostComponent', getHostComponent); }); + () => { assertPublished('getHostElement', getHostElement); }); it('should publish getInjector', () => { assertPublished('getInjector', getInjector); }); + it('should publish markDirty', () => { assertPublished('markDirty', markDirty); }); + it('should publish getPlayers', () => { assertPublished('getPlayers', getPlayers); }); }); }); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 01241b0f47..8070106852 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -20,7 +20,7 @@ import {SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ as R3_TEMPLATE_REF_FACTORY} from import {SWITCH_VIEW_CONTAINER_REF_FACTORY__POST_R3__ as R3_VIEW_CONTAINER_REF_FACTORY} from '../../src/linker/view_container_ref'; import {SWITCH_RENDERER2_FACTORY__POST_R3__ as R3_RENDERER2_FACTORY} from '../../src/render/api'; import {CreateComponentOptions} from '../../src/render3/component'; -import {discoverDirectives, getContext, isComponentInstance} from '../../src/render3/context_discovery'; +import {getContext, getDirectivesAtNodeIndex, isComponentInstance} from '../../src/render3/context_discovery'; import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'; import {NG_ELEMENT_ID} from '../../src/render3/fields'; import {ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; @@ -319,7 +319,7 @@ export function createDirective( /** Gets the directive on the given node at the given index */ export function getDirectiveOnNode(nodeIndex: number, dirIndex: number = 0) { - const directives = discoverDirectives(nodeIndex + HEADER_OFFSET, getViewData(), true); + const directives = getDirectivesAtNodeIndex(nodeIndex + HEADER_OFFSET, getViewData(), true); if (directives == null) { throw new Error(`No directives exist on node in slot ${nodeIndex}`); } diff --git a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts index 3af5d0b625..d945875e51 100644 --- a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts +++ b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts @@ -33,7 +33,7 @@ describe('style and class based bindings', () => { const rootContext = createRootContext(requestAnimationFrame.bind(window), playerHandler || null); const lViewData = createLViewData( - domRendererFactory3.createRenderer(element, null), + null, domRendererFactory3.createRenderer(element, null), createTView(-1, null, 1, 0, null, null, null), rootContext, LViewFlags.IsRoot); return lViewData; } diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 0692ddf3e1..2b0c1cb026 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -454,6 +454,7 @@ export declare class KeyValueDiffers { /** @deprecated */ factories: KeyValueDifferFactory[]; constructor(factories: KeyValueDifferFactory[]); find(kv: any): KeyValueDifferFactory; + static ngInjectableDef: never; static create(factories: KeyValueDifferFactory[], parent?: KeyValueDiffers): KeyValueDiffers; static extend(factories: KeyValueDifferFactory[]): StaticProvider; } diff --git a/tools/public_api_guard/global_utils.d.ts b/tools/public_api_guard/global_utils.d.ts index 62fba3a094..e8e9917945 100644 --- a/tools/public_api_guard/global_utils.d.ts +++ b/tools/public_api_guard/global_utils.d.ts @@ -1,11 +1,15 @@ -export declare function getComponent(target: {}): T | null; +export declare function getComponent(element: Element): T | null; export declare function getDirectives(target: {}): Array<{}>; -export declare function getHostComponent(target: {}): T | null; +export declare function getHostElement(directive: T): Element; export declare function getInjector(target: {}): Injector; export declare function getPlayers(ref: ComponentInstance | DirectiveInstance | HTMLElement): Player[]; export declare function getRootComponents(target: {}): any[]; + +export declare function getViewComponent(element: Element | {}): T | null; + +export declare function markDirty(component: T): void;