/
async_animation_renderer.ts
236 lines (194 loc) · 8.35 KB
/
async_animation_renderer.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵAnimationEngine as AnimationEngine, ɵAnimationRenderer as AnimationRenderer, ɵAnimationRendererFactory as AnimationRendererFactory} from '@angular/animations/browser';
import {NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ɵAnimationRendererType as AnimationRendererType, ɵRuntimeError as RuntimeError} from '@angular/core';
import {ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/platform-browser';
const ANIMATION_PREFIX = '@';
export class AsyncAnimationRendererFactory implements RendererFactory2 {
private _rendererFactoryPromise: Promise<AnimationRendererFactory>|null = null;
/**
*
* @param moduleImpl allows to provide a mock implmentation (or will load the animation module)
*/
constructor(
private doc: Document, private delegate: RendererFactory2, private zone: NgZone,
private animationType: 'animations'|'noop', private moduleImpl?: Promise<{
ɵcreateEngine: (type: 'animations'|'noop', doc: Document) => AnimationEngine,
ɵAnimationRendererFactory: typeof AnimationRendererFactory
}>) {}
/**
* @internal
*/
private loadImpl(): Promise<AnimationRendererFactory> {
const moduleImpl = this.moduleImpl ?? import('@angular/animations/browser');
return moduleImpl
.catch((e) => {
throw new RuntimeError(
RuntimeErrorCode.ANIMATION_RENDERER_ASYNC_LOADING_FAILURE,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Async loading for animations package was ' +
'enabled, but loading failed. Angular falls back to using regular rendering. ' +
'No animations will be displayed and their styles won\'t be applied.');
})
.then(({ɵcreateEngine, ɵAnimationRendererFactory}) => {
// We can't create the renderer yet because we might need the hostElement and the type
// Both are provided in createRenderer().
const engine = ɵcreateEngine(this.animationType, this.doc);
const rendererFactory = new ɵAnimationRendererFactory(this.delegate, engine, this.zone);
this.delegate = rendererFactory;
return rendererFactory;
});
}
/**
* This method is delegating the renderer creation to the factories.
* It uses default factory while the animation factory isn't loaded
* and will rely on the animation factory once it is loaded.
*
* Calling this method will trigger as side effect the loading of the animation module
* if the renderered component uses animations.
*/
createRenderer(hostElement: any, rendererType: RendererType2): Renderer2 {
const renderer = this.delegate.createRenderer(hostElement, rendererType);
if ((renderer as AnimationRenderer).ɵtype === AnimationRendererType.Regular) {
// The factory is already loaded, this is an animation renderer
return renderer;
}
// We need to prevent the DomRenderer to throw an error because of synthetic properties
if (typeof (renderer as any).throwOnSyntheticProps === 'boolean') {
(renderer as any).throwOnSyntheticProps = false;
}
// Using a dynamic renderer to switch the renderer implementation once the module is loaded.
const dynamicRenderer = new DynamicDelegationRenderer(renderer);
// Kick off the module loading if the component uses animations but the module hasn't been
// loaded yet.
if (rendererType?.data?.['animation'] && !this._rendererFactoryPromise) {
this._rendererFactoryPromise = this.loadImpl();
}
this._rendererFactoryPromise
?.then((animationRendererFactory) => {
const animationRenderer =
animationRendererFactory.createRenderer(hostElement, rendererType);
dynamicRenderer.use(animationRenderer);
})
.catch(e => {
// Permanently use regular renderer when loading fails.
dynamicRenderer.use(renderer);
});
return dynamicRenderer;
}
begin(): void {
this.delegate.begin?.();
}
end(): void {
this.delegate.end?.();
}
whenRenderingDone?(): Promise<any> {
return this.delegate.whenRenderingDone?.() ?? Promise.resolve();
}
}
/**
* The class allows to dynamicly switch between different renderer implementations
* by changing the delegate renderer.
*/
export class DynamicDelegationRenderer implements Renderer2 {
// List of callbacks that need to be replayed on the animation renderer once its loaded
private replay: ((renderer: Renderer2) => void)[]|null = [];
readonly ɵtype = AnimationRendererType.Delegated;
constructor(private delegate: Renderer2) {}
use(impl: Renderer2) {
this.delegate = impl;
if (this.replay !== null) {
// Replay queued actions using the animation renderer to apply
// all events and properties collected while loading was in progress.
for (const fn of this.replay) {
fn(impl);
}
// Set to `null` to indicate that the queue was processed
// and we no longer need to collect events and properties.
this.replay = null;
}
}
get data(): {[key: string]: any} {
return this.delegate.data;
}
destroy(): void {
this.replay = null;
this.delegate.destroy();
}
createElement(name: string, namespace?: string|null) {
return this.delegate.createElement(name, namespace);
}
createComment(value: string): void {
return this.delegate.createComment(value);
}
createText(value: string): any {
return this.delegate.createText(value);
}
get destroyNode(): ((node: any) => void)|null {
return this.delegate.destroyNode;
}
appendChild(parent: any, newChild: any): void {
this.delegate.appendChild(parent, newChild);
}
insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean|undefined): void {
this.delegate.insertBefore(parent, newChild, refChild, isMove);
}
removeChild(parent: any, oldChild: any, isHostElement?: boolean|undefined): void {
this.delegate.removeChild(parent, oldChild, isHostElement);
}
selectRootElement(selectorOrNode: any, preserveContent?: boolean|undefined): any {
return this.delegate.selectRootElement(selectorOrNode, preserveContent);
}
parentNode(node: any): any {
return this.delegate.parentNode(node);
}
nextSibling(node: any): any {
return this.delegate.nextSibling(node);
}
setAttribute(el: any, name: string, value: string, namespace?: string|null|undefined): void {
this.delegate.setAttribute(el, name, value, namespace);
}
removeAttribute(el: any, name: string, namespace?: string|null|undefined): void {
this.delegate.removeAttribute(el, name, namespace);
}
addClass(el: any, name: string): void {
this.delegate.addClass(el, name);
}
removeClass(el: any, name: string): void {
this.delegate.removeClass(el, name);
}
setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2|undefined): void {
this.delegate.setStyle(el, style, value, flags);
}
removeStyle(el: any, style: string, flags?: RendererStyleFlags2|undefined): void {
this.delegate.removeStyle(el, style, flags);
}
setProperty(el: any, name: string, value: any): void {
// We need to keep track of animation properties set on default renderer
// So we can also set them also on the animation renderer
if (this.shouldReplay(name)) {
this.replay!.push((renderer: Renderer2) => renderer.setProperty(el, name, value));
}
this.delegate.setProperty(el, name, value);
}
setValue(node: any, value: string): void {
this.delegate.setValue(node, value);
}
listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void {
// We need to keep track of animation events registred by the default renderer
// So we can also register them against the animation renderer
if (this.shouldReplay(eventName)) {
this.replay!.push((renderer: Renderer2) => renderer.listen(target, eventName, callback));
}
return this.delegate.listen(target, eventName, callback);
}
private shouldReplay(propOrEventName: string): boolean {
//`null` indicates that we no longer need to collect events and properties
return this.replay !== null && propOrEventName.startsWith(ANIMATION_PREFIX);
}
}