diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index dd1c2012dab..1da15b580f7 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -233,6 +233,22 @@ export abstract class ComponentFactoryResolver { abstract resolveComponentFactory(component: Type): ComponentFactory; } +// @public +export interface ComponentMirror { + get inputs(): ReadonlyArray<{ + propName: string; + templateName: string; + }>; + get isStandalone(): boolean; + get ngContentSelectors(): ReadonlyArray; + get outputs(): ReadonlyArray<{ + propName: string; + templateName: string; + }>; + get selector(): string; + get type(): Type; +} + // @public export abstract class ComponentRef { abstract get changeDetectorRef(): ChangeDetectorRef; @@ -1114,6 +1130,9 @@ export class QueryList implements Iterable { toString(): string; } +// @public +export function reflectComponentType(component: Type): ComponentMirror | null; + // @public @deprecated export abstract class ReflectiveInjector implements Injector { abstract createChildFromResolved(providers: ResolvedReflectiveProvider[]): ReflectiveInjector; diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 0f88d26867f..09cb43c00bb 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -37,7 +37,7 @@ export * from './core_render3_private_export'; export {SecurityContext} from './sanitization/security'; export {Sanitizer} from './sanitization/sanitizer'; export {createNgModule, createNgModuleRef, createEnvironmentInjector} from './render3/ng_module_ref'; -export {createComponent} from './render3/component_ref'; +export {createComponent, reflectComponentType, ComponentMirror} from './render3/component_ref'; import {global} from './util/global'; if (typeof ngDevMode !== 'undefined' && ngDevMode) { diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 2fe254939b1..79e526b2dbc 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -28,7 +28,7 @@ import {getComponentDef} from './definition'; import {NodeInjector} from './di'; import {assertComponentDef} from './errors'; import {reportUnknownPropertyError} from './instructions/element_validation'; -import {createLView, createTView, initializeInputAndOutputAliases, locateHostElement, markDirtyIfOnPush, renderView, setInputsForProperty} from './instructions/shared'; +import {createLView, createTView, locateHostElement, markDirtyIfOnPush, renderView, setInputsForProperty} from './instructions/shared'; import {ComponentDef} from './interfaces/definition'; import {PropertyAliasValue, TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node'; import {RNode} from './interfaces/renderer_dom'; @@ -359,7 +359,9 @@ export class ComponentRef extends AbstractComponentRef { * https://angular.io/guide/hierarchical-dependency-injection#elementinjector. * * `projectableNodes` (optional): A list of DOM nodes that should be projected through * [``](api/core/ng-content) of the new component instance. - * @returns + * @returns ComponentRef instance that represents a given Component. + * + * @publicApi */ export function createComponent(component: Type, options: { hostElement?: Element, environmentInjector: EnvironmentInjector, @@ -373,3 +375,103 @@ export function createComponent(component: Type, options: { return factory.create( elementInjector, options.projectableNodes, options.hostElement, options.environmentInjector); } + +/** + * An interface that describes the subset of component metadata + * that can be retrieved using the `reflectComponentType` function. + * + * @publicApi + */ +export interface ComponentMirror { + /** + * The component's HTML selector. + */ + get selector(): string; + /** + * The type of component the factory will create. + */ + get type(): Type; + /** + * The inputs of the component. + */ + get inputs(): ReadonlyArray<{propName: string, templateName: string}>; + /** + * The outputs of the component. + */ + get outputs(): ReadonlyArray<{propName: string, templateName: string}>; + /** + * Selector for all elements in the component. + */ + get ngContentSelectors(): ReadonlyArray; + /** + * Whether this component is marked as standalone. + * Note: an extra flag, not present in `ComponentFactory`. + */ + get isStandalone(): boolean; +} + +/** + * Creates an object that allows to retrieve component metadata. + * + * @usageNotes + * + * The example below demonstrates how to use the function and how the fields + * of the returned object map to the component metadata. + * + * ```typescript + * @Component({ + * standalone: true, + * selector: 'foo-component', + * template: ` + * + * + * `, + * }) + * class FooComponent { + * @Input('inputName') inputPropName: string; + * @Output('outputName') outputPropName = new EventEmitter(); + * } + * + * const mirror = reflectComponentType(FooComponent)!; + * expect(mirror.type).toBe(FooComponent); + * expect(mirror.selector).toBe('foo-component'); + * expect(mirror.isStandalone).toBe(true); + * expect(mirror.inputs).toEqual([{propName: 'inputName', templateName: 'inputPropName'}]); + * expect(mirror.outputs).toEqual([{propName: 'outputName', templateName: 'outputPropName'}]); + * expect(mirror.ngContentSelectors).toEqual([ + * '*', // first `` in a template, the selector defaults to `*` + * 'content-selector-a' // second `` in a template + * ]); + * ``` + * + * @param component Component class reference. + * @returns An object that allows to retrieve component metadata. + * + * @publicApi + */ +export function reflectComponentType(component: Type): ComponentMirror|null { + const componentDef = getComponentDef(component); + if (!componentDef) return null; + + const factory = new ComponentFactory(componentDef); + return { + get selector(): string { + return factory.selector; + }, + get type(): Type { + return factory.componentType; + }, + get inputs(): ReadonlyArray<{propName: string, templateName: string}> { + return factory.inputs; + }, + get outputs(): ReadonlyArray<{propName: string, templateName: string}> { + return factory.outputs; + }, + get ngContentSelectors(): ReadonlyArray { + return factory.ngContentSelectors; + }, + get isStandalone(): boolean { + return componentDef.standalone; + }, + }; +} diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts index 2bb39b8892b..2589eface94 100644 --- a/packages/core/test/acceptance/component_spec.ts +++ b/packages/core/test/acceptance/component_spec.ts @@ -7,7 +7,7 @@ */ import {DOCUMENT} from '@angular/common'; -import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, createComponent, createEnvironmentInjector, Directive, ElementRef, EmbeddedViewRef, EnvironmentInjector, inject, Injectable, InjectionToken, Injector, Input, NgModule, OnDestroy, Renderer2, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument} from '@angular/core'; +import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, createComponent, createEnvironmentInjector, Directive, ElementRef, EmbeddedViewRef, EnvironmentInjector, inject, Injectable, InjectionToken, Injector, Input, NgModule, OnDestroy, reflectComponentType, Renderer2, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument} from '@angular/core'; import {stringifyForError} from '@angular/core/src/render3/util/stringify_utils'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -866,4 +866,91 @@ describe('component', () => { }); }); }); + + describe('reflectComponentType', () => { + it('should create an ComponentMirror for a standalone component', () => { + @Component({ + selector: 'standalone-component', + standalone: true, + template: ` + + + + + `, + inputs: ['input-a', 'input-b:input-alias-b'], + outputs: ['output-a', 'output-b:output-alias-b'], + }) + class StandaloneComponent { + } + + const mirror = reflectComponentType(StandaloneComponent)!; + + expect(mirror.selector).toBe('standalone-component'); + expect(mirror.type).toBe(StandaloneComponent); + expect(mirror.isStandalone).toEqual(true); + expect(mirror.inputs).toEqual([ + {propName: 'input-a', templateName: 'input-a'}, + {propName: 'input-b', templateName: 'input-alias-b'} + ]); + expect(mirror.outputs).toEqual([ + {propName: 'output-a', templateName: 'output-a'}, + {propName: 'output-b', templateName: 'output-alias-b'} + ]); + expect(mirror.ngContentSelectors).toEqual([ + '*', 'content-selector-a', 'content-selector-b', '*' + ]); + }); + + it('should create an ComponentMirror for a non-standalone component', () => { + @Component({ + selector: 'non-standalone-component', + template: ` + + + + + `, + inputs: ['input-a', 'input-b:input-alias-b'], + outputs: ['output-a', 'output-b:output-alias-b'], + }) + class NonStandaloneComponent { + } + + const mirror = reflectComponentType(NonStandaloneComponent)!; + + expect(mirror.selector).toBe('non-standalone-component'); + expect(mirror.type).toBe(NonStandaloneComponent); + expect(mirror.isStandalone).toEqual(false); + expect(mirror.inputs).toEqual([ + {propName: 'input-a', templateName: 'input-a'}, + {propName: 'input-b', templateName: 'input-alias-b'} + ]); + expect(mirror.outputs).toEqual([ + {propName: 'output-a', templateName: 'output-a'}, + {propName: 'output-b', templateName: 'output-alias-b'} + ]); + expect(mirror.ngContentSelectors).toEqual([ + '*', 'content-selector-a', 'content-selector-b', '*' + ]); + }); + + describe('error checking', () => { + it('should throw when provided class is not a component', () => { + class NotAnnotated {} + + @Directive() + class ADirective { + } + + @Injectable() + class AnInjectiable { + } + + expect(reflectComponentType(NotAnnotated)).toBe(null); + expect(reflectComponentType(ADirective)).toBe(null); + expect(reflectComponentType(AnInjectiable)).toBe(null); + }); + }); + }); });