Skip to content

Commit

Permalink
feat(core): add reflectComponentType function
Browse files Browse the repository at this point in the history
This commit introduces a new function that allows creating a object which exposes a number of getters to retrieve information about a given component.

Closes angular#44926.
  • Loading branch information
AndrewKushnir committed Jul 10, 2022
1 parent 1cd1148 commit 0a20df1
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 3 deletions.
19 changes: 19 additions & 0 deletions goldens/public-api/core/index.md
Expand Up @@ -233,6 +233,22 @@ export abstract class ComponentFactoryResolver {
abstract resolveComponentFactory<T>(component: Type<T>): ComponentFactory<T>;
}

// @public
export interface ComponentMirror<C> {
get inputs(): {
propName: string;
templateName: string;
}[];
get isStandalone(): boolean;
get ngContentSelectors(): string[];
get outputs(): {
propName: string;
templateName: string;
}[];
get selector(): string;
get type(): Type<C>;
}

// @public
export abstract class ComponentRef<C> {
abstract get changeDetectorRef(): ChangeDetectorRef;
Expand Down Expand Up @@ -1111,6 +1127,9 @@ export class QueryList<T> implements Iterable<T> {
toString(): string;
}

// @public
export function reflectComponentType<C>(component: Type<C>): ComponentMirror<C>;

// @public @deprecated
export abstract class ReflectiveInjector implements Injector {
abstract createChildFromResolved(providers: ResolvedReflectiveProvider[]): ReflectiveInjector;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core.ts
Expand Up @@ -37,7 +37,7 @@ export * from './core_render3_private_export';
export {SecurityContext} from './sanitization/security';
export {Sanitizer} from './sanitization/sanitizer';
export {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) {
Expand Down
97 changes: 96 additions & 1 deletion packages/core/src/render3/component_ref.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -373,3 +373,98 @@ export function createComponent<C>(component: Type<C>, 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.
*/
export interface ComponentMirror<C> {
/**
* The component's HTML selector.
*/
get selector(): string;
/**
* The type of component the factory will create.
*/
get type(): Type<C>;
/**
* The inputs of the component.
*/
get inputs(): Array<{propName: string, templateName: string}>;
/**
* The outputs of the component.
*/
get outputs(): Array<{propName: string, templateName: string}>;
/**
* Selector for all <ng-content> elements in the component.
*/
get ngContentSelectors(): string[];
/**
* 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: `
* <ng-content></ng-content>
* <ng-content select="content-selector-a"></ng-content>
* `,
* })
* class FooComponent {
* @Input('inputName') inputPropName: string;
* @Output('outputName') outputPropName = new EventEmitter<void>();
* }
*
* 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 `<ng-content>` in a template, the selector defaults to `*`
* 'content-selector-a' // second `<ng-content>` in a template
* ]);
* ```
*
* @param component Component class reference.
* @returns An object that allows to retrieve component metadata.
*/
export function reflectComponentType<C>(component: Type<C>): ComponentMirror<C> {
ngDevMode && assertComponentDef(component);
const componentDef = getComponentDef(component)!;
const factory = new ComponentFactory<C>(componentDef);
return {
get selector(): string {
return factory.selector;
},
get type(): Type<C> {
return factory.componentType;
},
get inputs(): Array<{propName: string, templateName: string}> {
return factory.inputs;
},
get outputs(): Array<{propName: string, templateName: string}> {
return factory.outputs;
},
get ngContentSelectors(): string[] {
return factory.ngContentSelectors;
},
get isStandalone(): boolean {
return componentDef.standalone;
},
};
}
93 changes: 92 additions & 1 deletion packages/core/test/acceptance/component_spec.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -866,4 +866,95 @@ describe('component', () => {
});
});
});

describe('reflectComponentType', () => {
it('should create an ComponentMirror for a standalone component', () => {
@Component({
selector: 'standalone-component',
standalone: true,
template: `
<ng-content></ng-content>
<ng-content select="content-selector-a"></ng-content>
<ng-content select="content-selector-b"></ng-content>
<ng-content></ng-content>
`,
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: `
<ng-content></ng-content>
<ng-content select="content-selector-a"></ng-content>
<ng-content select="content-selector-b"></ng-content>
<ng-content></ng-content>
`,
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 NotAComponent {}

@Directive()
class ADirective {
}

@Injectable()
class AnInjectiable {
}

const errorFor = (type: Type<unknown>): string =>
`NG0906: The ${stringifyForError(type)} is not an Angular component, ` +
`make sure it has the \`@Component\` decorator.`;

expect(() => reflectComponentType(NotAComponent)).toThrowError(errorFor(NotAComponent));
expect(() => reflectComponentType(ADirective)).toThrowError(errorFor(ADirective));
expect(() => reflectComponentType(AnInjectiable)).toThrowError(errorFor(AnInjectiable));
});
});
});
});

0 comments on commit 0a20df1

Please sign in to comment.