feat(angular): handle angular components without selector

This commit is contained in:
ThibaudAv 2021-02-17 12:26:05 +01:00
parent 3a9ef82903
commit 59c7bc8ed8
6 changed files with 249 additions and 1 deletions

View File

@ -1,3 +1,4 @@
import { Component } from '@angular/core';
import { ArgTypes } from '@storybook/api';
import { computesTemplateSourceFromComponent } from './ComputesTemplateFromComponent';
import { ButtonAccent, InputComponent, ISomeInterface } from './__testfixtures__/input.component';
@ -10,6 +11,24 @@ describe('angular source decorator', () => {
const source = computesTemplateSourceFromComponent(component, props, argTypes);
expect(source).toEqual('<doc-button></doc-button>');
});
describe('with component without selector', () => {
@Component({
template: `The content`,
})
class WithoutSelectorComponent {}
it('should add component ng-container', async () => {
const component = WithoutSelectorComponent;
const props = {};
const argTypes: ArgTypes = {};
const source = computesTemplateSourceFromComponent(component, props, argTypes);
expect(source).toEqual(
`<ng-container *ngComponentOutlet="WithoutSelectorComponent"></ng-container>`
);
});
});
describe('no argTypes', () => {
it('should generate tag-only template with no props', () => {
const component = InputComponent;

View File

@ -39,6 +39,11 @@ export const computesTemplateFromComponent = (
const ngComponentMetadata = getComponentDecoratorMetadata(component);
const ngComponentInputsOutputs = getComponentInputsOutputs(component);
if (!ngComponentMetadata.selector) {
// Allow to add renderer component when NgComponent selector is undefined
return `<ng-container *ngComponentOutlet="storyComponent"></ng-container>`;
}
const { inputs: initialInputs, outputs: initialOutputs } = separateInputsOutputsAttributes(
ngComponentInputsOutputs,
initialProps
@ -93,6 +98,12 @@ export const computesTemplateSourceFromComponent = (
if (!ngComponentMetadata) {
return null;
}
if (!ngComponentMetadata.selector) {
// Allow to add renderer component when NgComponent selector is undefined
return `<ng-container *ngComponentOutlet="${component.name}"></ng-container>`;
}
const ngComponentInputsOutputs = getComponentInputsOutputs(component);
const { inputs: initialInputs, outputs: initialOutputs } = separateInputsOutputsAttributes(
ngComponentInputsOutputs,

View File

@ -1,6 +1,7 @@
import { Component, EventEmitter, Input, NgModule, Output, Type } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { BrowserModule } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs';
import { ICollection } from '../types';
import { getStorybookModuleMetadata } from './StorybookModule';
@ -208,13 +209,47 @@ describe('StorybookModule', () => {
expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(newProps.input);
});
});
describe('with component without selector', () => {
@Component({
template: `The content`,
})
class WithoutSelectorComponent {}
it('should display the component', async () => {
const props = {};
const ngModule = getStorybookModuleMetadata(
{
storyFnAngular: {
props,
moduleMetadata: { entryComponents: [WithoutSelectorComponent] },
},
parameters: { component: WithoutSelectorComponent },
},
new BehaviorSubject<ICollection>(props)
);
const { fixture } = await configureTestingModule(ngModule);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toContain('The content');
});
});
});
async function configureTestingModule(ngModule: NgModule) {
await TestBed.configureTestingModule({
declarations: ngModule.declarations,
providers: ngModule.providers,
}).compileComponents();
})
.overrideModule(BrowserModule, {
set: {
entryComponents: [...ngModule.entryComponents],
},
})
.compileComponents();
const fixture = TestBed.createComponent(ngModule.bootstrap[0] as Type<unknown>);
return {

View File

@ -57,6 +57,9 @@ export const createStorybookWrapperComponent = (
@ViewChild(storyComponent ?? '', { read: ViewContainerRef, static: true })
storyComponentViewContainerRef: ViewContainerRef;
// Used in case of a component without selector
storyComponent = storyComponent ?? '';
// eslint-disable-next-line no-useless-constructor
constructor(
@Inject(STORY_PROPS) private storyProps$: Subject<ICollection | undefined>,

View File

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Basics / Component / without selector Simple Component 1`] = `
<storybook-wrapper>
<ng-component>
My name in color :
<div
style="color: rgb(30, 136, 229);"
>
Joe Bar
</div>
</ng-component>
</storybook-wrapper>
`;
exports[`Storyshots Basics / Component / without selector With Custom Ng Component Outlet Wrapper 1`] = `
<storybook-wrapper>
<without-selector-wrapper
ng-reflect-color="green"
ng-reflect-component-outlet="function WithoutSelectorCompon"
ng-reflect-name="Dixie Normous"
>
<ng-component>
My name in color :
<div
style="color: green;"
>
Dixie Normous
</div>
Ng-content : Inspired by
https://angular.io/api/common/NgComponentOutlet
</ng-component>
</without-selector-wrapper>
</storybook-wrapper>
`;
exports[`Storyshots Basics / Component / without selector With Injection Token And Args 1`] = `
<storybook-wrapper>
<ng-component>
My name in color :
<div
style="color: red;"
>
Dixie Normous
</div>
</ng-component>
</storybook-wrapper>
`;

View File

@ -0,0 +1,131 @@
import {
Component,
Inject,
InjectionToken,
Injector,
Input,
OnInit,
Optional,
Type,
} from '@angular/core';
import { componentWrapperDecorator, moduleMetadata, Story, Meta } from '@storybook/angular';
const WITHOUT_SELECTOR_DATA = new InjectionToken<{ color: string; name: string }>(
'WITHOUT_SELECTOR_DATA'
);
@Component({
template: `My name in color :
<div [style.color]="color">{{ name }}</div>
<ng-content></ng-content> <ng-content></ng-content>`,
})
class WithoutSelectorComponent {
color = '#1e88e5';
name = 'Joe Bar';
constructor(
@Inject(WITHOUT_SELECTOR_DATA)
@Optional()
data: {
color: string;
name: string;
} | null
) {
if (data) {
this.color = data.color;
this.name = data.name;
}
}
}
export default {
title: 'Basics / Component / without selector',
component: WithoutSelectorComponent,
decorators: [
moduleMetadata({
entryComponents: [WithoutSelectorComponent],
}),
],
} as Meta;
export const SimpleComponent: Story = () => ({});
// Live changing of args by controls does not work for now. When changing args storybook does not fully
// reload and therefore does not take into account the change of provider.
export const WithInjectionTokenAndArgs: Story = (args) => ({
props: args,
moduleMetadata: {
providers: [
{ provide: WITHOUT_SELECTOR_DATA, useValue: { color: args.color, name: args.name } },
],
},
});
WithInjectionTokenAndArgs.argTypes = {
name: { control: 'text' },
color: { control: 'color' },
};
WithInjectionTokenAndArgs.args = { name: 'Dixie Normous', color: 'red' };
// Advanced example with custom *ngComponentOutlet
@Component({
selector: 'without-selector-wrapper',
template: `<ng-container
*ngComponentOutlet="componentOutlet; injector: componentInjector; content: componentContent"
></ng-container>`,
})
class WithoutSelectorWrapperComponent implements OnInit {
@Input()
componentOutlet: Type<unknown>;
@Input()
name: string;
@Input()
color: string;
componentInjector: Injector;
componentContent = [
// eslint-disable-next-line no-undef
[document.createTextNode('Ng-content : Inspired by ')],
// eslint-disable-next-line no-undef
[document.createTextNode('https://angular.io/api/common/NgComponentOutlet')],
];
// eslint-disable-next-line no-useless-constructor
constructor(private readonly injector: Injector) {}
ngOnInit(): void {
console.log({ color: this.color, name: this.name });
this.componentInjector = Injector.create({
providers: [
{ provide: WITHOUT_SELECTOR_DATA, useValue: { color: this.color, name: this.name } },
],
parent: this.injector,
});
}
}
// Live changing of args by controls does not work at the moment. When changing args storybook does not fully
// reload and therefore does not take into account the change of provider.
export const WithCustomNgComponentOutletWrapper: Story = (args) => ({
props: args,
});
WithCustomNgComponentOutletWrapper.argTypes = {
name: { control: 'text' },
color: { control: 'color' },
};
WithCustomNgComponentOutletWrapper.args = { name: 'Dixie Normous', color: 'green' };
WithCustomNgComponentOutletWrapper.decorators = [
moduleMetadata({
declarations: [WithoutSelectorWrapperComponent],
}),
componentWrapperDecorator(WithoutSelectorWrapperComponent, (args) => ({
name: args.name,
color: args.color,
componentOutlet: WithoutSelectorComponent,
})),
];