/
StorybookWrapperComponent.ts
132 lines (115 loc) · 4.63 KB
/
StorybookWrapperComponent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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';
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 = (
selector: string,
template: string,
storyComponent: Type<unknown> | undefined,
styles: string[],
initialProps?: ICollection
): Type<any> => {
// In ivy, a '' selector is not allowed, therefore we need to just set it to anything if
// storyComponent was not provided.
const viewChildSelector = storyComponent ?? '__storybook-noop';
@Component({
selector,
template,
styles,
})
class StorybookWrapperComponent implements AfterViewInit, OnDestroy {
private storyComponentPropsSubscription: Subscription;
private storyWrapperPropsSubscription: Subscription;
@ViewChild(viewChildSelector, { static: true }) storyComponentElementRef: ElementRef;
@ViewChild(viewChildSelector, { 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 properties than are not Input|Output up to date
this.storyComponentPropsSubscription = this.storyProps$
.pipe(
skip(1),
map((props) => {
const propsKeyToKeep = getNonInputsOutputsProps(ngComponentInputsOutputs, props);
return propsKeyToKeep.reduce((acc, p) => ({ ...acc, [p]: props[p] }), {});
})
)
.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;
};