mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-02 05:03:44 +08:00
feat(angular): handle angular components without selector
This commit is contained in:
parent
3a9ef82903
commit
59c7bc8ed8
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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>,
|
||||
|
@ -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>
|
||||
`;
|
@ -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,
|
||||
})),
|
||||
];
|
Loading…
x
Reference in New Issue
Block a user