mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-03 05:04:51 +08:00
155 lines
5.4 KiB
TypeScript
155 lines
5.4 KiB
TypeScript
import {
|
|
AfterViewInit,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
ElementRef,
|
|
Inject,
|
|
OnDestroy,
|
|
Type,
|
|
ViewChild,
|
|
ViewContainerRef,
|
|
} from '@angular/core';
|
|
import { Subscription, Subject } from 'rxjs';
|
|
import { map, skip } from 'rxjs/operators';
|
|
|
|
import { ICollection } from '../types';
|
|
import { STORY_PROPS } from './StorybookProvider';
|
|
import { ComponentInputsOutputs, getComponentInputsOutputs } from './utils/NgComponentAnalyzer';
|
|
import { RendererService } from './RendererService';
|
|
|
|
const getNonInputsOutputsProps = (
|
|
ngComponentInputsOutputs: ComponentInputsOutputs,
|
|
props: ICollection = {}
|
|
) => {
|
|
const inputs = ngComponentInputsOutputs.inputs
|
|
.filter((i) => i.templateName in props)
|
|
.map((i) => i.templateName);
|
|
const outputs = ngComponentInputsOutputs.outputs
|
|
.filter((o) => o.templateName in props)
|
|
.map((o) => o.templateName);
|
|
return Object.keys(props).filter((k) => ![...inputs, ...outputs].includes(k));
|
|
};
|
|
|
|
/**
|
|
* Wraps the story template into a component
|
|
*
|
|
* @param storyComponent
|
|
* @param initialProps
|
|
*/
|
|
export const createStorybookWrapperComponent = (
|
|
template: string,
|
|
storyComponent: Type<unknown>,
|
|
styles: string[],
|
|
initialProps?: ICollection
|
|
): Type<any> => {
|
|
@Component({
|
|
selector: RendererService.SELECTOR_STORYBOOK_WRAPPER,
|
|
template,
|
|
styles,
|
|
})
|
|
class StorybookWrapperComponent implements AfterViewInit, OnDestroy {
|
|
private storyComponentPropsSubscription: Subscription;
|
|
|
|
private storyWrapperPropsSubscription: Subscription;
|
|
|
|
@ViewChild(storyComponent ?? '', { static: true }) storyComponentElementRef: ElementRef;
|
|
|
|
@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>,
|
|
private changeDetectorRef: ChangeDetectorRef
|
|
) {}
|
|
|
|
ngOnInit(): void {
|
|
// Subscribes to the observable storyProps$ to keep these properties up to date
|
|
this.storyWrapperPropsSubscription = this.storyProps$.subscribe((storyProps = {}) => {
|
|
// All props are added as component properties
|
|
Object.assign(this, storyProps);
|
|
|
|
this.changeDetectorRef.detectChanges();
|
|
this.changeDetectorRef.markForCheck();
|
|
});
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
// Bind properties to component, if the story have component
|
|
if (this.storyComponentElementRef) {
|
|
const ngComponentInputsOutputs = getComponentInputsOutputs(storyComponent);
|
|
|
|
const initialOtherProps = getNonInputsOutputsProps(ngComponentInputsOutputs, initialProps);
|
|
|
|
// Initializes properties that are not Inputs | Outputs
|
|
// Allows story props to override local component properties
|
|
initialOtherProps.forEach((p) => {
|
|
(this.storyComponentElementRef as any)[p] = initialProps[p];
|
|
});
|
|
// `markForCheck` the component in case this uses changeDetection: OnPush
|
|
// And then forces the `detectChanges`
|
|
this.storyComponentViewContainerRef.injector.get(ChangeDetectorRef).markForCheck();
|
|
this.changeDetectorRef.detectChanges();
|
|
|
|
// Once target component has been initialized, the storyProps$ observable keeps target component inputs up to date
|
|
this.storyComponentPropsSubscription = this.storyProps$
|
|
.pipe(
|
|
skip(1),
|
|
map((props) => {
|
|
// removes component output in props
|
|
const outputsKeyToRemove = ngComponentInputsOutputs.outputs.map(
|
|
(o) => o.templateName
|
|
);
|
|
return Object.entries(props).reduce(
|
|
(prev, [key, value]) => ({
|
|
...prev,
|
|
...(!outputsKeyToRemove.includes(key) && {
|
|
[key]: value,
|
|
}),
|
|
}),
|
|
{} as ICollection
|
|
);
|
|
}),
|
|
map((props) => {
|
|
// In case a component uses an input with `bindingPropertyName` (ex: @Input('name'))
|
|
// find the value of the local propName in the component Inputs
|
|
// otherwise use the input key
|
|
return Object.entries(props).reduce((prev, [propKey, value]) => {
|
|
const input = ngComponentInputsOutputs.inputs.find(
|
|
(o) => o.templateName === propKey
|
|
);
|
|
|
|
return {
|
|
...prev,
|
|
...(input ? { [input.propName]: value } : { [propKey]: value }),
|
|
};
|
|
}, {} as ICollection);
|
|
})
|
|
)
|
|
.subscribe((props) => {
|
|
// Replace inputs with new ones from props
|
|
Object.assign(this.storyComponentElementRef, props);
|
|
|
|
// `markForCheck` the component in case this uses changeDetection: OnPush
|
|
// And then forces the `detectChanges`
|
|
this.storyComponentViewContainerRef.injector.get(ChangeDetectorRef).markForCheck();
|
|
this.changeDetectorRef.detectChanges();
|
|
});
|
|
}
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (this.storyComponentPropsSubscription != null) {
|
|
this.storyComponentPropsSubscription.unsubscribe();
|
|
}
|
|
if (this.storyWrapperPropsSubscription != null) {
|
|
this.storyWrapperPropsSubscription.unsubscribe();
|
|
}
|
|
}
|
|
}
|
|
return StorybookWrapperComponent;
|
|
};
|