feat(core): introduce getDirectiveMetadata global debugging utility (#41525)

This commit introduces a global debugging method
`ng.getDirectiveMetadata` which returns the metadata for a directive or
component instance.

PR Close #41525
This commit is contained in:
mgechev 2021-04-05 20:01:42 -07:00 committed by Zach Arend
parent 1d12c50f63
commit a07f303708
7 changed files with 126 additions and 7 deletions

View File

@ -1,10 +1,22 @@
export declare function applyChanges(component: {}): void; export declare function applyChanges(component: {}): void;
export interface ComponentDebugMetadata extends DirectiveDebugMetadata {
changeDetection: ChangeDetectionStrategy;
encapsulation: ViewEncapsulation;
}
export interface DirectiveDebugMetadata {
inputs: Record<string, string>;
outputs: Record<string, string>;
}
export declare function getComponent<T>(element: Element): T | null; export declare function getComponent<T>(element: Element): T | null;
export declare function getContext<T>(element: Element): T | null; export declare function getContext<T>(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; export declare function getHostElement(componentOrDirective: {}): Element;

View File

@ -16,4 +16,4 @@
*/ */
export {applyChanges} from './util/change_detection_utils'; 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';

View File

@ -13,7 +13,7 @@ import {ɵɵNgOnChangesFeature} from './features/ng_onchanges_feature';
import {ɵɵProvidersFeature} from './features/providers_feature'; import {ɵɵProvidersFeature} from './features/providers_feature';
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, PipeDef} from './interfaces/definition'; import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, PipeDef} from './interfaces/definition';
import {ɵɵComponentDeclaration, ɵɵDirectiveDeclaration, ɵɵFactoryDeclaration, ɵɵInjectorDeclaration, ɵɵNgModuleDeclaration, ɵɵPipeDeclaration} from './interfaces/public_definitions'; 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 {NgModuleType} from '../metadata/ng_module_def';
export {ComponentFactory, ComponentFactoryResolver, ComponentRef, injectComponentFactoryResolver} from './component_ref'; export {ComponentFactory, ComponentFactoryResolver, ComponentRef, injectComponentFactoryResolver} from './component_ref';
@ -176,12 +176,15 @@ export { ɵɵtemplateRefExtractor} from './view_engine_compatibility_prebound';
// clang-format on // clang-format on
export { export {
ComponentDebugMetadata,
ComponentDef, ComponentDef,
ComponentTemplate, ComponentTemplate,
ComponentType, ComponentType,
DirectiveDebugMetadata,
DirectiveDef, DirectiveDef,
DirectiveType, DirectiveType,
getComponent, getComponent,
getDirectiveMetadata,
getDirectives, getDirectives,
getHostElement, getHostElement,
getRenderedText, getRenderedText,

View File

@ -6,10 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ChangeDetectionStrategy} from '../../change_detection/constants';
import {Injector} from '../../di/injector'; import {Injector} from '../../di/injector';
import {ViewEncapsulation} from '../../metadata/view';
import {assertEqual} from '../../util/assert'; import {assertEqual} from '../../util/assert';
import {assertLView} from '../assert'; import {assertLView} from '../assert';
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from '../context_discovery'; import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from '../context_discovery';
import {getComponentDef, getDirectiveDef} from '../definition';
import {NodeInjector} from '../di'; import {NodeInjector} from '../di';
import {buildDebugNode} from '../instructions/lview_debug'; import {buildDebugNode} from '../instructions/lview_debug';
import {LContext} from '../interfaces/context'; import {LContext} from '../interfaces/context';
@ -203,6 +206,70 @@ export function getDirectives(element: Element): {}[] {
return context.directives === null ? [] : [...context.directives]; 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<string, string>;
outputs: Record<string, string>;
}
/**
* 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. * Returns LContext associated with a target passed as an argument.
* Throws if a given target doesn't have associated LContext. * Throws if a given target doesn't have associated LContext.

View File

@ -9,7 +9,7 @@ import {assertDefined} from '../../util/assert';
import {global} from '../../util/global'; import {global} from '../../util/global';
import {setProfiler} from '../profiler'; import {setProfiler} from '../profiler';
import {applyChanges} from './change_detection_utils'; 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. * removed completely.
*/ */
publishGlobalUtil('ɵsetProfiler', setProfiler); publishGlobalUtil('ɵsetProfiler', setProfiler);
publishGlobalUtil('getDirectiveMetadata', getDirectiveMetadata);
publishGlobalUtil('getComponent', getComponent); publishGlobalUtil('getComponent', getComponent);
publishGlobalUtil('getContext', getContext); publishGlobalUtil('getContext', getContext);
publishGlobalUtil('getListeners', getListeners); publishGlobalUtil('getListeners', getListeners);

View File

@ -7,6 +7,9 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, Directive, HostBinding, InjectionToken, ViewChild} from '@angular/core'; 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 {isLView} from '@angular/core/src/render3/interfaces/type_checks';
import {CONTEXT} from '@angular/core/src/render3/interfaces/view'; import {CONTEXT} from '@angular/core/src/render3/interfaces/view';
import {ComponentFixture, TestBed} from '@angular/core/testing'; 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 {onlyInIvy} from '@angular/private/testing';
import {getHostElement, markDirty} from '../../src/render3/index'; 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', () => { onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
let fixture: ComponentFixture<MyApp>; let fixture: ComponentFixture<MyApp>;
let myApp: MyApp; let myApp: MyApp;
let dirA: DirectiveA[]; let dirA: DirectiveA[];
let childComponent: DirectiveA[]; let childComponent: (DirectiveA|Child)[];
let child: NodeListOf<Element>; let child: NodeListOf<Element>;
let span: NodeListOf<Element>; let span: NodeListOf<Element>;
let div: NodeListOf<Element>; let div: NodeListOf<Element>;
@ -55,6 +58,8 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
@Directive({selector: '[dirA]', exportAs: 'dirA'}) @Directive({selector: '[dirA]', exportAs: 'dirA'})
class DirectiveA { class DirectiveA {
@Input('a') b = 2;
@Output('c') d = new EventEmitter();
constructor() { constructor() {
dirA.push(this); dirA.push(this);
} }
@ -73,6 +78,8 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
}) })
class MyApp { class MyApp {
text: string = 'INIT'; text: string = 'INIT';
@Input('a') b = 2;
@Output('c') d = new EventEmitter();
constructor() { constructor() {
myApp = this; myApp = this;
} }
@ -288,6 +295,23 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => {
expect(lContext.native as any).toBe(ngContainerComment); 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', () => { onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () => {
@ -359,6 +383,14 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () =>
const elm2Dirs = getDirectives(elm2); const elm2Dirs = getDirectives(elm2);
expect(elm2Dirs).toContain(fixture.componentInstance.myDir3Instance!); 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', () => { describe('getInjector', () => {

View File

@ -8,7 +8,7 @@
import {setProfiler} from '@angular/core/src/render3/profiler'; import {setProfiler} from '@angular/core/src/render3/profiler';
import {applyChanges} from '../../src/render3/util/change_detection_utils'; 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_PUBLISH_EXPANDO_KEY, GlobalDevModeContainer, publishDefaultGlobalUtils, publishGlobalUtil} from '../../src/render3/util/global_utils';
import {global} from '../../src/util/global'; import {global} from '../../src/util/global';
@ -62,6 +62,10 @@ describe('global utils', () => {
assertPublished('applyChanges', applyChanges); assertPublished('applyChanges', applyChanges);
}); });
it('should publish getDirectiveMetadata', () => {
assertPublished('getDirectiveMetadata', getDirectiveMetadata);
});
it('should publish ɵsetProfiler', () => { it('should publish ɵsetProfiler', () => {
assertPublished('ɵsetProfiler', setProfiler); assertPublished('ɵsetProfiler', setProfiler);
}); });