diff --git a/goldens/public-api/core/global_utils.d.ts b/goldens/public-api/core/global_utils.d.ts index 11f3023690..5fbf67baf3 100644 --- a/goldens/public-api/core/global_utils.d.ts +++ b/goldens/public-api/core/global_utils.d.ts @@ -1,10 +1,22 @@ export declare function applyChanges(component: {}): void; +export interface ComponentDebugMetadata extends DirectiveDebugMetadata { + changeDetection: ChangeDetectionStrategy; + encapsulation: ViewEncapsulation; +} + +export interface DirectiveDebugMetadata { + inputs: Record; + outputs: Record; +} + export declare function getComponent(element: Element): T | null; export declare function getContext(element: Element): T | null; -export declare function getDirectives(element: Element): {}[]; +export declare function getDirectiveMetadata(directiveOrComponentInstance: any): ComponentDebugMetadata | DirectiveDebugMetadata | null; + +export declare function getDirectives(node: Node): {}[]; export declare function getHostElement(componentOrDirective: {}): Element; diff --git a/packages/core/src/render3/global_utils_api.ts b/packages/core/src/render3/global_utils_api.ts index a8e0776e2b..b74bedf4af 100644 --- a/packages/core/src/render3/global_utils_api.ts +++ b/packages/core/src/render3/global_utils_api.ts @@ -16,4 +16,4 @@ */ export {applyChanges} from './util/change_detection_utils'; -export {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents, Listener} from './util/discovery_utils'; +export {ComponentDebugMetadata, DirectiveDebugMetadata, getComponent, getContext, getDirectiveMetadata, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents, Listener} from './util/discovery_utils'; diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index aa8b010bce..9480c64569 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -13,7 +13,7 @@ import {ɵɵNgOnChangesFeature} from './features/ng_onchanges_feature'; import {ɵɵProvidersFeature} from './features/providers_feature'; import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, PipeDef} from './interfaces/definition'; import {ɵɵComponentDeclaration, ɵɵDirectiveDeclaration, ɵɵFactoryDeclaration, ɵɵInjectorDeclaration, ɵɵNgModuleDeclaration, ɵɵPipeDeclaration} from './interfaces/public_definitions'; -import {getComponent, getDirectives, getHostElement, getRenderedText} from './util/discovery_utils'; +import {ComponentDebugMetadata, DirectiveDebugMetadata, getComponent, getDirectiveMetadata, getDirectives, getHostElement, getRenderedText} from './util/discovery_utils'; export {NgModuleType} from '../metadata/ng_module_def'; export {ComponentFactory, ComponentFactoryResolver, ComponentRef, injectComponentFactoryResolver} from './component_ref'; @@ -176,12 +176,15 @@ export { ɵɵtemplateRefExtractor} from './view_engine_compatibility_prebound'; // clang-format on export { + ComponentDebugMetadata, ComponentDef, ComponentTemplate, ComponentType, + DirectiveDebugMetadata, DirectiveDef, DirectiveType, getComponent, + getDirectiveMetadata, getDirectives, getHostElement, getRenderedText, diff --git a/packages/core/src/render3/util/discovery_utils.ts b/packages/core/src/render3/util/discovery_utils.ts index 1966517cd4..db5d760ff1 100644 --- a/packages/core/src/render3/util/discovery_utils.ts +++ b/packages/core/src/render3/util/discovery_utils.ts @@ -6,10 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {ChangeDetectionStrategy} from '../../change_detection/constants'; import {Injector} from '../../di/injector'; +import {ViewEncapsulation} from '../../metadata/view'; import {assertEqual} from '../../util/assert'; import {assertLView} from '../assert'; import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from '../context_discovery'; +import {getComponentDef, getDirectiveDef} from '../definition'; import {NodeInjector} from '../di'; import {buildDebugNode} from '../instructions/lview_debug'; import {LContext} from '../interfaces/context'; @@ -203,6 +206,70 @@ export function getDirectives(element: Element): {}[] { return context.directives === null ? [] : [...context.directives]; } +/** + * Partial metadata for a given directive instance. + * This information might be useful for debugging purposes or tooling. + * Currently only `inputs` and `outputs` metadata is available. + * + * @publicApi + */ +export interface DirectiveDebugMetadata { + inputs: Record; + outputs: Record; +} + +/** + * Partial metadata for a given component instance. + * This information might be useful for debugging purposes or tooling. + * Currently the following fields are available: + * - inputs + * - outputs + * - encapsulation + * - changeDetection + * + * @publicApi + */ +export interface ComponentDebugMetadata extends DirectiveDebugMetadata { + encapsulation: ViewEncapsulation; + changeDetection: ChangeDetectionStrategy; +} + +/** + * Returns the debug (partial) metadata for a particular directive or component instance. + * The function accepts an instance of a directive or component and returns the corresponding + * metadata. + * + * @param directiveOrComponentInstance Instance of a directive or component + * @returns metadata of the passed directive or component + * + * @publicApi + * @globalApi ng + */ +export function getDirectiveMetadata(directiveOrComponentInstance: any): ComponentDebugMetadata| + DirectiveDebugMetadata|null { + const {constructor} = directiveOrComponentInstance; + if (!constructor) { + throw new Error('Unable to find the instance constructor'); + } + // In case a component inherits from a directive, we may have component and directive metadata + // To ensure we don't get the metadata of the directive, we want to call `getComponentDef` first. + const componentDef = getComponentDef(constructor); + if (componentDef) { + return { + inputs: componentDef.inputs, + outputs: componentDef.outputs, + encapsulation: componentDef.encapsulation, + changeDetection: componentDef.onPush ? ChangeDetectionStrategy.OnPush : + ChangeDetectionStrategy.Default + }; + } + const directiveDef = getDirectiveDef(constructor); + if (directiveDef) { + return {inputs: directiveDef.inputs, outputs: directiveDef.outputs}; + } + return null; +} + /** * Returns LContext associated with a target passed as an argument. * Throws if a given target doesn't have associated LContext. diff --git a/packages/core/src/render3/util/global_utils.ts b/packages/core/src/render3/util/global_utils.ts index 9e488d3f1a..ad88607791 100644 --- a/packages/core/src/render3/util/global_utils.ts +++ b/packages/core/src/render3/util/global_utils.ts @@ -9,7 +9,7 @@ import {assertDefined} from '../../util/assert'; import {global} from '../../util/global'; import {setProfiler} from '../profiler'; import {applyChanges} from './change_detection_utils'; -import {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from './discovery_utils'; +import {getComponent, getContext, getDirectiveMetadata, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from './discovery_utils'; @@ -48,6 +48,7 @@ export function publishDefaultGlobalUtils() { * removed completely. */ publishGlobalUtil('ɵsetProfiler', setProfiler); + publishGlobalUtil('getDirectiveMetadata', getDirectiveMetadata); publishGlobalUtil('getComponent', getComponent); publishGlobalUtil('getContext', getContext); publishGlobalUtil('getListeners', getListeners); diff --git a/packages/core/test/acceptance/discover_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts index 26e6b09d9c..5af464f790 100644 --- a/packages/core/test/acceptance/discover_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -7,6 +7,9 @@ */ import {CommonModule} from '@angular/common'; import {Component, Directive, HostBinding, InjectionToken, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy} from '@angular/core/src/change_detection'; +import {EventEmitter} from '@angular/core/src/event_emitter'; +import {Input, Output, ViewEncapsulation} from '@angular/core/src/metadata'; import {isLView} from '@angular/core/src/render3/interfaces/type_checks'; import {CONTEXT} from '@angular/core/src/render3/interfaces/view'; import {ComponentFixture, TestBed} from '@angular/core/testing'; @@ -15,13 +18,13 @@ import {expect} from '@angular/core/testing/src/testing_internal'; import {onlyInIvy} from '@angular/private/testing'; import {getHostElement, markDirty} from '../../src/render3/index'; -import {getComponent, getComponentLView, getContext, getDebugNode, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getOwningComponent, getRootComponents, loadLContext} from '../../src/render3/util/discovery_utils'; +import {ComponentDebugMetadata, getComponent, getComponentLView, getContext, getDebugNode, getDirectiveMetadata, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getOwningComponent, getRootComponents, loadLContext} from '../../src/render3/util/discovery_utils'; onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { let fixture: ComponentFixture; let myApp: MyApp; let dirA: DirectiveA[]; - let childComponent: DirectiveA[]; + let childComponent: (DirectiveA|Child)[]; let child: NodeListOf; let span: NodeListOf; let div: NodeListOf; @@ -55,6 +58,8 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { @Directive({selector: '[dirA]', exportAs: 'dirA'}) class DirectiveA { + @Input('a') b = 2; + @Output('c') d = new EventEmitter(); constructor() { dirA.push(this); } @@ -73,6 +78,8 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { }) class MyApp { text: string = 'INIT'; + @Input('a') b = 2; + @Output('c') d = new EventEmitter(); constructor() { myApp = this; } @@ -288,6 +295,23 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { expect(lContext.native as any).toBe(ngContainerComment); }); }); + + describe('getDirectiveMetadata', () => { + it('should work with components', () => { + const metadata = getDirectiveMetadata(myApp); + expect(metadata!.inputs).toEqual({a: 'b'}); + expect(metadata!.outputs).toEqual({c: 'd'}); + expect((metadata as ComponentDebugMetadata).changeDetection) + .toBe(ChangeDetectionStrategy.Default); + expect((metadata as ComponentDebugMetadata).encapsulation).toBe(ViewEncapsulation.None); + }); + + it('should work with directives', () => { + const metadata = getDirectiveMetadata(getDirectives(div[0])[0]); + expect(metadata!.inputs).toEqual({a: 'b'}); + expect(metadata!.outputs).toEqual({c: 'd'}); + }); + }); }); onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () => { @@ -359,6 +383,14 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () => const elm2Dirs = getDirectives(elm2); expect(elm2Dirs).toContain(fixture.componentInstance.myDir3Instance!); }); + + it('should not throw if it cannot find LContext', () => { + let result: any; + expect(() => { + result = getDirectives(document.createElement('div')); + }).not.toThrow(); + expect(result).toEqual([]); + }); }); describe('getInjector', () => { diff --git a/packages/core/test/render3/global_utils_spec.ts b/packages/core/test/render3/global_utils_spec.ts index ea19965a1d..dfb1aca5a4 100644 --- a/packages/core/test/render3/global_utils_spec.ts +++ b/packages/core/test/render3/global_utils_spec.ts @@ -8,7 +8,7 @@ import {setProfiler} from '@angular/core/src/render3/profiler'; import {applyChanges} from '../../src/render3/util/change_detection_utils'; -import {getComponent, getContext, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from '../../src/render3/util/discovery_utils'; +import {getComponent, getContext, getDirectiveMetadata, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from '../../src/render3/util/discovery_utils'; import {GLOBAL_PUBLISH_EXPANDO_KEY, GlobalDevModeContainer, publishDefaultGlobalUtils, publishGlobalUtil} from '../../src/render3/util/global_utils'; import {global} from '../../src/util/global'; @@ -62,6 +62,10 @@ describe('global utils', () => { assertPublished('applyChanges', applyChanges); }); + it('should publish getDirectiveMetadata', () => { + assertPublished('getDirectiveMetadata', getDirectiveMetadata); + }); + it('should publish ɵsetProfiler', () => { assertPublished('ɵsetProfiler', setProfiler); });