forked from storybookjs/storybook
/
app.component.ts
128 lines (113 loc) 路 4.11 KB
/
app.component.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
/* eslint-disable no-useless-constructor */
// We could use NgComponentOutlet here but there's currently no easy way
// to provide @Inputs and subscribe to @Outputs, see
// https://github.com/angular/angular/issues/15360
// For the time being, the ViewContainerRef approach works pretty well.
import {
Component,
Inject,
OnInit,
ViewChild,
ViewContainerRef,
ComponentFactoryResolver,
OnDestroy,
EventEmitter,
SimpleChanges,
SimpleChange,
ChangeDetectorRef,
NgZone,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { first } from 'rxjs/operators';
import { STORY } from '../app.token';
import { StoryFnAngularReturnType, ICollection } from '../../types';
@Component({
selector: 'storybook-dynamic-app-root',
template: '<ng-template #target></ng-template>',
})
export class AppComponent implements OnInit, OnDestroy {
@ViewChild('target', { read: ViewContainerRef, static: true })
target: ViewContainerRef;
readonly previousValues: { [key: string]: any } = {};
subscription: Subscription;
constructor(
private cfr: ComponentFactoryResolver,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone,
@Inject(STORY) private data: Observable<StoryFnAngularReturnType>
) {}
ngOnInit(): void {
this.data.pipe(first()).subscribe((data: StoryFnAngularReturnType) => {
this.target.clear();
const compFactory = this.cfr.resolveComponentFactory(data.component);
const componentRef = this.target.createComponent(compFactory);
const { instance } = componentRef;
// For some reason, manual change detection ref is only working when getting the ref from the injector (rather than componentRef.changeDetectorRef)
const childChangeDetectorRef: ChangeDetectorRef = componentRef.injector.get(
ChangeDetectorRef
);
this.subscription = this.data.subscribe((newData) => {
this.ngZone.run(() => this.setProps(instance, newData));
childChangeDetectorRef.markForCheck();
// Must detect changes on the current component in order to update any changes in child component's @HostBinding properties (angular/angular#22560)
this.changeDetectorRef.detectChanges();
});
});
}
ngOnDestroy(): void {
this.target.clear();
if (this.subscription) {
this.subscription.unsubscribe();
}
}
/**
* Set inputs and outputs
*/
private setProps(instance: any, { props = {} }: StoryFnAngularReturnType): void {
const changes: SimpleChanges = {};
const hasNgOnChangesHook = !!instance.ngOnChanges;
Object.keys(props).forEach((key: string) => {
const value = props[key];
const instanceProperty = instance[key];
if (!(instanceProperty instanceof EventEmitter) && value !== undefined && value !== null) {
// eslint-disable-next-line no-param-reassign
instance[key] = value;
if (hasNgOnChangesHook) {
const previousValue = this.previousValues[key];
if (previousValue !== value) {
changes[key] = new SimpleChange(
previousValue,
value,
!Object.prototype.hasOwnProperty.call(this.previousValues, key)
);
this.previousValues[key] = value;
}
}
} else if (typeof value === 'function' && key !== 'ngModelChange') {
instanceProperty.subscribe(value);
}
});
this.callNgOnChangesHook(instance, changes);
this.setNgModel(instance, props);
}
/**
* Manually call 'ngOnChanges' hook because angular doesn't do that for dynamic components
* Issue: [https://github.com/angular/angular/issues/8903]
*/
private callNgOnChangesHook(instance: any, changes: SimpleChanges): void {
if (Object.keys(changes).length) {
instance.ngOnChanges(changes);
}
}
/**
* If component implements ControlValueAccessor interface try to set ngModel
*/
private setNgModel(instance: any, props: ICollection): void {
if (props.ngModel) {
instance.writeValue(props.ngModel);
}
if (typeof props.ngModelChange === 'function') {
instance.registerOnChange(props.ngModelChange);
}
}
}