-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
ComponentClassFromStoryComponent.ts
146 lines (131 loc) · 5.34 KB
/
ComponentClassFromStoryComponent.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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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 './app.token';
import {
ComponentInputsOutputs,
getComponentDecoratorMetadata,
getComponentInputsOutputs,
} from './NgComponentAnalyzer';
import { RenderNgAppService } from './RenderNgAppService';
const getNamesOfInputsOutputsDefinedInProps = (
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 {
inputs,
outputs,
otherProps: Object.keys(props).filter((k) => ![...inputs, ...outputs].includes(k)),
};
};
/**
* Wraps the story component into a component
*
* @param component
* @param initialProps
*/
export const createComponentClassFromStoryComponent = (
component: any,
initialProps?: ICollection
): Type<any> => {
const ngComponentMetadata = getComponentDecoratorMetadata(component);
const ngComponentInputsOutputs = getComponentInputsOutputs(component);
const {
inputs: initialInputs,
outputs: initialOutputs,
otherProps: initialOtherProps,
} = getNamesOfInputsOutputsDefinedInProps(ngComponentInputsOutputs, initialProps);
const templateInputs = initialInputs.map((i) => `[${i}]="${i}"`).join(' ');
const templateOutputs = initialOutputs.map((i) => `(${i})="${i}($event)"`).join(' ');
@Component({
selector: RenderNgAppService.SELECTOR_STORYBOOK_WRAPPER,
// Simulates the `component` integration in a template
// `props` are converted into Inputs/Outputs to be added directly in the template so as the component can use them during its initailization
// - The outputs are connected only once here
// - Only inputs present in initial `props` value are added. They will be overwritten and completed as necessary after the component is initialized
template: `<${ngComponentMetadata.selector} ${templateInputs} ${templateOutputs} #storyComponentRef></${ngComponentMetadata.selector}>`,
})
class StoryBookComponentWrapperComponent implements AfterViewInit, OnDestroy {
private storyPropsSubscription: Subscription;
@ViewChild('storyComponentRef', { static: true }) storyComponentElementRef: ElementRef;
@ViewChild('storyComponentRef', { read: ViewContainerRef, static: true })
storyComponentViewContainerRef: ViewContainerRef;
constructor(
@Inject(STORY_PROPS) private storyProps$: Subject<ICollection | undefined>,
private changeDetectorRef: ChangeDetectorRef
) {
// Initializes template Inputs/Outputs values
Object.assign(this, initialProps);
}
ngAfterViewInit(): void {
// 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.storyPropsSubscription = 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.storyPropsSubscription != null) {
this.storyPropsSubscription.unsubscribe();
}
}
}
return StoryBookComponentWrapperComponent;
};