/
app.component.ts
156 lines (136 loc) · 5.03 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
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
156
/* 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,
} 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;
propSubscriptions = new Map<any, { prop: any; sub: Subscription }>();
constructor(
private cfr: ComponentFactoryResolver,
private changeDetectorRef: ChangeDetectorRef,
@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.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();
}
this.propSubscriptions.forEach((v) => {
if (!v.sub.closed) {
v.sub.unsubscribe();
}
});
this.propSubscriptions.clear();
}
/**
* 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') {
this.setPropSubscription(key, instanceProperty, 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);
}
}
/**
* Store ref to subscription for cleanup in 'ngOnDestroy' and check if
* observable needs to be resubscribed to, before creating a new subscription.
*/
private setPropSubscription(key: string, instanceProperty: Observable<any>, value: any): void {
if (this.propSubscriptions.has(key)) {
const v = this.propSubscriptions.get(key);
if (v.prop === value) {
// Prop hasn't changed, so the existing subscription can stay.
return;
}
// Now that the value has changed, unsubscribe from the previous value's subscription.
if (!v.sub.closed) {
v.sub.unsubscribe();
}
}
const sub = instanceProperty.subscribe(value);
this.propSubscriptions.set(key, { prop: value, sub });
}
}