feat: add function to find Inputs & Outputs in an Angular component

The test allows to verify the function by using angular engine
This commit is contained in:
ThibaudAv 2020-11-26 14:21:17 +01:00
parent 3bfea0922f
commit 8bb7acec14
2 changed files with 229 additions and 0 deletions

View File

@ -0,0 +1,123 @@
import {
Component,
ComponentFactory,
ComponentFactoryResolver,
EventEmitter,
Input,
Output,
Type,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { getComponentInputsOutputs } from './NgComponentAnalyzer';
describe('getComponentInputsOutputs', () => {
it('should return empty if no I/O found', () => {
@Component({})
class FooComponent {}
expect(getComponentInputsOutputs(FooComponent)).toEqual({
inputs: [],
outputs: [],
});
class BarComponent {}
expect(getComponentInputsOutputs(BarComponent)).toEqual({
inputs: [],
outputs: [],
});
});
it('should return I/O', () => {
@Component({
template: '',
inputs: ['inputInComponentMetadata'],
outputs: ['outputInComponentMetadata'],
})
class FooComponent {
@Input()
public input: string;
@Input('inputPropertyName')
public inputWithBindingPropertyName: string;
@Output()
public output = new EventEmitter<Event>();
@Output('outputPropertyName')
public outputWithBindingPropertyName = new EventEmitter<Event>();
}
const fooComponentFactory = resolveComponentFactory(FooComponent);
const { inputs, outputs } = getComponentInputsOutputs(FooComponent);
expect({ inputs, outputs }).toEqual({
inputs: [
{ propName: 'inputInComponentMetadata', templateName: 'inputInComponentMetadata' },
{ propName: 'input', templateName: 'input' },
{ propName: 'inputWithBindingPropertyName', templateName: 'inputPropertyName' },
],
outputs: [
{ propName: 'outputInComponentMetadata', templateName: 'outputInComponentMetadata' },
{ propName: 'output', templateName: 'output' },
{ propName: 'outputWithBindingPropertyName', templateName: 'outputPropertyName' },
],
});
expect(sortByPropName(inputs)).toEqual(sortByPropName(fooComponentFactory.inputs));
expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs));
});
it("should return I/O when some of component metadata has the same name as one of component's properties", () => {
@Component({
template: '',
inputs: ['input', 'inputWithBindingPropertyName'],
outputs: ['outputWithBindingPropertyName'],
})
class FooComponent {
@Input()
public input: string;
@Input('inputPropertyName')
public inputWithBindingPropertyName: string;
@Output()
public output = new EventEmitter<Event>();
@Output('outputPropertyName')
public outputWithBindingPropertyName = new EventEmitter<Event>();
}
const fooComponentFactory = resolveComponentFactory(FooComponent);
const { inputs, outputs } = getComponentInputsOutputs(FooComponent);
expect(sortByPropName(inputs)).toEqual(sortByPropName(fooComponentFactory.inputs));
expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs));
});
});
function sortByPropName(
array: {
propName: string;
templateName: string;
}[]
) {
return array.sort((a, b) => a.propName.localeCompare(b.propName));
}
function resolveComponentFactory<T extends Type<any>>(component: T): ComponentFactory<T> {
TestBed.configureTestingModule({
declarations: [component],
}).overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [component],
},
});
const componentFactoryResolver = TestBed.inject(ComponentFactoryResolver);
return componentFactoryResolver.resolveComponentFactory(component);
}

View File

@ -0,0 +1,106 @@
import { Component, Input, Output } from '@angular/core';
export type ComponentInputsOutputs = {
inputs: {
propName: string;
templateName: string;
}[];
outputs: {
propName: string;
templateName: string;
}[];
};
/**
* Returns component Inputs / Outputs by browsing these properties and decorator
*/
export const getComponentInputsOutputs = (component: any): ComponentInputsOutputs => {
const componentMetadata = getComponentDecoratorMetadata(component);
const componentPropsMetadata = getComponentPropsDecoratorMetadata(component);
const initialValue: ComponentInputsOutputs = {
inputs: [],
outputs: [],
};
// Adds the I/O present in @Component metadata
if (componentMetadata && componentMetadata.inputs) {
initialValue.inputs.push(
...componentMetadata.inputs.map((i) => ({ propName: i, templateName: i }))
);
}
if (componentMetadata && componentMetadata.outputs) {
initialValue.outputs.push(
...componentMetadata.outputs.map((i) => ({ propName: i, templateName: i }))
);
}
if (!componentPropsMetadata) {
return initialValue;
}
// Browses component properties to extract I/O
// Filters properties that have the same name as the one present in the @Component property
return Object.entries(componentPropsMetadata).reduce((previousValue, [propertyName, [value]]) => {
if (value instanceof Input) {
const inputToAdd = {
propName: propertyName,
templateName: value.bindingPropertyName ?? propertyName,
};
const previousInputsFiltered = previousValue.inputs.filter(
(i) => i.templateName !== propertyName
);
return {
...previousValue,
inputs: [...previousInputsFiltered, inputToAdd],
};
}
if (value instanceof Output) {
const outputToAdd = {
propName: propertyName,
templateName: value.bindingPropertyName ?? propertyName,
};
const previousOutputsFiltered = previousValue.outputs.filter(
(i) => i.templateName !== propertyName
);
return {
...previousValue,
outputs: [...previousOutputsFiltered, outputToAdd],
};
}
return previousValue;
}, initialValue);
};
/**
* Returns all component decorator properties
* is used to get all `@Input` and `@Output` Decorator
*/
export const getComponentPropsDecoratorMetadata = (component: any) => {
const decoratorKey = '__prop__metadata__';
const propsDecorators: Record<string, (Input | Output)[]> =
Reflect &&
Reflect.getOwnPropertyDescriptor &&
Reflect.getOwnPropertyDescriptor(component, decoratorKey)
? Reflect.getOwnPropertyDescriptor(component, decoratorKey).value
: component[decoratorKey];
return propsDecorators;
};
/**
* Returns component decorator `@Component`
*/
export const getComponentDecoratorMetadata = (component: any): Component | undefined => {
const decoratorKey = '__annotations__';
const decorators: any[] =
Reflect &&
Reflect.getOwnPropertyDescriptor &&
Reflect.getOwnPropertyDescriptor(component, decoratorKey)
? Reflect.getOwnPropertyDescriptor(component, decoratorKey).value
: component[decoratorKey];
return (decorators || []).find((d) => d instanceof Component);
};