Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add function to find Inputs & Outputs in an Angular component
The test allows to verify the function by using angular engine
- Loading branch information
Showing
2 changed files
with
222 additions
and
0 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
app/angular/src/client/preview/angular/NgComponentAnalyzer.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
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: [], | ||
}); | ||
}); | ||
|
||
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); | ||
} |
106 changes: 106 additions & 0 deletions
106
app/angular/src/client/preview/angular/NgComponentAnalyzer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 || undefined; | ||
}; | ||
|
||
/** | ||
* 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); | ||
}; |