/
RenderNgAppService.ts
155 lines (134 loc) · 5.4 KB
/
RenderNgAppService.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
147
148
149
150
151
152
153
154
155
/* eslint-disable no-undef */
import { enableProdMode, NgModule, NO_ERRORS_SCHEMA, PlatformRef, Type } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { StoryFn } from '@storybook/addons';
import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs';
import { ICollection, StoryFnAngularReturnType } from '../types';
import { storyPropsProvider } from './app.token';
import { createComponentClassFromStoryComponent } from './ComponentClassFromStoryComponent';
import { createComponentClassFromStoryTemplate } from './ComponentClassFromStoryTemplate';
import { getExistenceOfComponentInModules } from './NgModulesAnalyzer';
/**
* Bootstrap angular application and allows to change the rendering dynamically
* To be used as a singleton so has to set global properties of render function
*/
export class RenderNgAppService {
private static instance: RenderNgAppService;
public static getInstance() {
if (!RenderNgAppService.instance) {
RenderNgAppService.instance = new RenderNgAppService();
}
return RenderNgAppService.instance;
}
public static SELECTOR_STORYBOOK_WRAPPER = 'storybook-wrapper';
private platform: PlatformRef;
private staticRoot = document.getElementById('root');
// Observable to change the properties dynamically without reloading angular module&component
private storyProps$: Subject<ICollection | undefined>;
constructor() {
// Adds DOM element that angular will use as bootstrap component
const storybookWrapperElement = document.createElement(
RenderNgAppService.SELECTOR_STORYBOOK_WRAPPER
);
this.staticRoot.innerHTML = '';
this.staticRoot.appendChild(storybookWrapperElement);
if (typeof NODE_ENV === 'string' && NODE_ENV !== 'development') {
try {
enableProdMode();
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
// platform should be set after enableProdMode()
this.platform = platformBrowserDynamic();
}
/**
* Bootstrap main angular module with main component or send only new `props` with storyProps$
*
* @param storyFn {StoryFn<StoryFnAngularReturnType>}
* @param forced {boolean} If :
* - true render will only use the StoryFn `props' in storyProps observable that will update sotry's component/template properties. Improves performance without reloading the whole module&component if props changes
* - false fully recharges or initializes angular module & component
*/
public async render(storyFn: StoryFn<StoryFnAngularReturnType>, forced: boolean) {
const storyObj = storyFn();
if (forced && this.storyProps$) {
this.storyProps$.next(storyObj.props);
return;
}
// Complete last BehaviorSubject and create a new one for the current module
if (this.storyProps$) {
this.storyProps$.complete();
}
this.storyProps$ = new BehaviorSubject<ICollection>(storyObj.props);
if (typeof NODE_ENV === 'string' && NODE_ENV !== 'development') {
try {
enableProdMode();
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
// platform should be set after enableProdMode()
this.platform = platformBrowserDynamic();
await this.platform.bootstrapModule(
createModuleFromMetadata(getNgModuleMetadata(storyObj, this.storyProps$))
);
}
}
const getNgModuleMetadata = (
storyFnAngular: StoryFnAngularReturnType,
storyProps$: Subject<ICollection>
): NgModule => {
const { component, moduleMetadata = {} } = storyFnAngular;
const ComponentToInject = createComponentToInject(storyFnAngular);
// Look recursively (deep) if the component is not already declared by an import module
const requiresComponentDeclaration =
component &&
!getExistenceOfComponentInModules(
component,
moduleMetadata.declarations,
moduleMetadata.imports
);
return {
declarations: [
...(requiresComponentDeclaration ? [component] : []),
ComponentToInject,
...(moduleMetadata.declarations ?? []),
],
imports: [BrowserModule, ...(moduleMetadata.imports ?? [])],
providers: [storyPropsProvider(storyProps$), ...(moduleMetadata.providers ?? [])],
entryComponents: [...(moduleMetadata.entryComponents ?? [])],
schemas: [
// Required because`props` don't only contain Inputs/Outputs which generates a bad template for component wrapper
// This happens with the addon Controls + Doc
// The private and public properties of the component are also added in `props` (wrongly)
// FIXME : To be removed when the above behavior is corrected
NO_ERRORS_SCHEMA,
...(moduleMetadata.schemas ?? []),
],
bootstrap: [ComponentToInject],
};
};
const createModuleFromMetadata = (ngModule: NgModule) => {
@NgModule(ngModule)
class StoryBookAppModule {}
return StoryBookAppModule;
};
/**
* Create a specific component according to whether the story uses a template or a component.
*/
const createComponentToInject = ({
template,
styles,
component,
props,
}: StoryFnAngularReturnType): Type<any> => {
// Template has priority over the component
const isCreatingComponentFromTemplate = !!template;
return isCreatingComponentFromTemplate
? createComponentClassFromStoryTemplate(template, styles)
: createComponentClassFromStoryComponent(component, props);
};