Skip to content

Commit

Permalink
feat: support inputs and outputs from extended components
Browse files Browse the repository at this point in the history
  • Loading branch information
ike18t committed Mar 5, 2018
1 parent 29972c3 commit fc46838
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 22 deletions.
20 changes: 15 additions & 5 deletions lib/mock-component/mock-component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { SimpleComponent } from './test-components/simple-component.component';
template: `
<simple-component [someInput]="\'hi\'"
[someOtherInput]="\'bye\'"
(someOutput1)="emitted = $event">
(someOutput1)="emitted = $event"
(someOutput2)="emitted = $event">
</simple-component>
<simple-component [someInput]="\'hi again\'" #f='seeimple'></simple-component>
<empty-component></empty-component>
Expand Down Expand Up @@ -55,7 +56,7 @@ describe('MockComponent', () => {
fixture.detectChanges();
const mockedComponent = fixture.debugElement
.query(By.directive(MockComponent(SimpleComponent)))
.componentInstance as SimpleComponent;
.componentInstance;
expect(mockedComponent.someInput).toEqual('hi');
expect(mockedComponent.someInput2).toEqual('bye');
});
Expand All @@ -64,11 +65,20 @@ describe('MockComponent', () => {
fixture.detectChanges();
const mockedComponent = fixture.debugElement
.query(By.directive(MockComponent(SimpleComponent)))
.componentInstance as SimpleComponent;
.componentInstance;
mockedComponent.someOutput1.emit('hi');
expect(component.emitted).toEqual('hi');
});

it('should trigger output bound behavior for extended outputs', () => {
fixture.detectChanges();
const mockedComponent = fixture.debugElement
.query(By.directive(MockComponent(SimpleComponent)))
.componentInstance;
mockedComponent.someOutput2.emit('hi');
expect(component.emitted).toEqual('hi');
});

it('the mock should have an ng-content body', () => {
fixture.detectChanges();
const mockedComponent = fixture.debugElement.query(By.css('#ng-content-component'));
Expand All @@ -78,8 +88,8 @@ describe('MockComponent', () => {
it('should give each instance of a mocked component its own event emitter', () => {
const mockedComponents = fixture.debugElement
.queryAll(By.directive(MockComponent(SimpleComponent)));
const mockedComponent1 = mockedComponents[0].componentInstance as SimpleComponent;
const mockedComponent2 = mockedComponents[1].componentInstance as SimpleComponent;
const mockedComponent1 = mockedComponents[0].componentInstance;
const mockedComponent2 = mockedComponents[1].componentInstance;
expect(mockedComponent1.someOutput1).not.toEqual(mockedComponent2.someOutput1);
});

Expand Down
33 changes: 18 additions & 15 deletions lib/mock-component/mock-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,34 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

const cache = new Map<Type<Component>, Type<Component>>();

const metaReducer = (propertyMetaData: any) =>
(acc: string[], meta: any): string[] =>
acc.concat(propertyMetaData[meta].map((m: any): string =>
[meta, m.bindingPropertyName || meta].join(':')));

function getInputsOrOutputs<TComponent>(component: Type<TComponent>, type: 'Input' | 'Output'): string[] {
if (!component) {
return [];
}
const propertyMetadata = (component as any).__prop__metadata__ || {};
const outputs = Object.keys(propertyMetadata)
.filter((meta) => propertyMetadata[meta][0].ngMetadataName === type)
.reduce(metaReducer(propertyMetadata), []);
return outputs.concat(getInputsOrOutputs((component as any).__proto__, type));
}

export function MockComponent<TComponent>(component: Type<TComponent>): Type<TComponent> {
const cacheHit = cache.get(component);
if (cacheHit) {
return cacheHit as Type<TComponent>;
}

const annotations = (component as any).__annotations__[0] || {};
const propertyMetadata = (component as any).__prop__metadata__ || {};

const options: Component = {
exportAs: annotations.exportAs,
inputs: Object.keys(propertyMetadata)
.filter((meta) => isInput(propertyMetadata[meta]))
.map((meta) => [meta, propertyMetadata[meta][0].bindingPropertyName || meta].join(':')),
outputs: Object.keys(propertyMetadata)
.filter((meta) => isOutput(propertyMetadata[meta]))
.map((meta) => [meta, propertyMetadata[meta][0].bindingPropertyName || meta].join(':')),
inputs: getInputsOrOutputs(component, 'Input'),
outputs: getInputsOrOutputs(component, 'Output'),
providers: [{
multi: true,
provide: NG_VALUE_ACCESSOR,
Expand Down Expand Up @@ -51,11 +62,3 @@ export function MockComponent<TComponent>(component: Type<TComponent>): Type<TCo

return mockedComponent;
}

function isInput(propertyMetadata: any): boolean {
return propertyMetadata[0].ngMetadataName === 'Input';
}

function isOutput(propertyMetadata: any): boolean {
return propertyMetadata[0].ngMetadataName === 'Output';
}
13 changes: 11 additions & 2 deletions lib/mock-component/test-components/simple-component.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
selector: 'base-simple-component',
template: 'some template'
})
export class BaseSimpleComponent {
@Output() someOutput2: EventEmitter<string>;
}

/* tslint:disable:max-classes-per-file */
@Component({
exportAs: 'seeimple',
selector: 'simple-component',
template: 'some template'
})
export class SimpleComponent {
export class SimpleComponent extends BaseSimpleComponent {
@Input() someInput: string;
@Input('someOtherInput') someInput2: string;
@Output() someOutput1: EventEmitter<string>;
@Output() someOutput2: EventEmitter<string>;
}
/* tslint:enable:max-classes-per-file */

0 comments on commit fc46838

Please sign in to comment.