Skip to content

Commit

Permalink
fix(platform-browser): set animation properties when using async anim…
Browse files Browse the repository at this point in the history
…ations. (#52087)

Animations properties set on the default renderer weren't set on the animation renderer once it was loaded. This commit fixes this.

PR Close #52087
  • Loading branch information
JeanMeche authored and atscott committed Oct 10, 2023
1 parent 5b375d1 commit 75d610d
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 20 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/platform-browser/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

// @public
export const enum RuntimeErrorCode {
// (undocumented)
ANIMATION_RENDERER_ASYNC_LOADING_FAILURE = 5300,
// (undocumented)
BROWER_MODULE_ALREADY_LOADED = 5100,
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion packages/animations/browser/src/render/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class BaseAnimationRenderer implements Renderer2 {
this.engine.onInsert(this.namespaceId, newChild, parent, isMove);
}

removeChild(parent: any, oldChild: any): void {
removeChild(parent: any, oldChild: any, isHostElement?: boolean): void {
this.engine.onRemove(this.namespaceId, oldChild, this.delegate);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
*/

import type {ɵAnimationRendererFactory as AnimationRendererFactory, ɵAnimationRenderer as AnimationRenderer, ɵBaseAnimationRenderer as BaseAnimationRenderer} from '@angular/animations/browser';
import {NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '@angular/core';

import {NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ɵRuntimeError as RuntimeError} from '@angular/core';
import {ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/platform-browser';
/**
* This alias narrows down to only the properties we need when lazy loading (or mock) the module
*/
type AnimationBrowserModuleImports =
Pick<typeof import('@angular/animations/browser'), 'ɵcreateEngine'|'ɵAnimationRendererFactory'>;


const ANIMATION_PREFIX = '@';

export class AsyncAnimationRendererFactory implements RendererFactory2 {
private _rendererFactoryPromise: Promise<AnimationRendererFactory>|null = null;

Expand All @@ -35,8 +38,12 @@ export class AsyncAnimationRendererFactory implements RendererFactory2 {

return moduleImpl
.catch((e) => {
// TODO: Create a runtime error
throw new Error('Failed to load the @angular/animations/browser module');
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
Expand Down Expand Up @@ -78,10 +85,16 @@ export class AsyncAnimationRendererFactory implements RendererFactory2 {
this._rendererFactoryPromise = this.loadImpl();
}

this._rendererFactoryPromise?.then((animationRendererFactory) => {
const animationRenderer = animationRendererFactory.createRenderer(hostElement, rendererType);
dynamicRenderer.use(animationRenderer);
});
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;
}
Expand All @@ -104,21 +117,23 @@ export class AsyncAnimationRendererFactory implements RendererFactory2 {
* by changing the delegate renderer.
*/
export class DynamicDelegationRenderer implements Renderer2 {
// List of animation events registered by the default renderer
private animationEvents:|
Array<{target: any; eventName: string; callback: (event: any) => boolean | void}>|null = [];
// List of callbacks that need to be replayed on the animation renderer once its loaded
private replay: ((renderer: Renderer2) => void)[]|null = [];

constructor(private delegate: Renderer2) {}

use(impl: BaseAnimationRenderer) {
use(impl: Renderer2) {
this.delegate = impl;

if (this.animationEvents) {
// Register the event against the animation renderer
for (const {target, eventName, callback} of this.animationEvents) {
this.delegate.listen(target, eventName, callback);
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);
}
this.animationEvents = null;
// Set to `null` to indicate that the queue was processed
// and we no longer need to collect events and properties.
this.replay = null;
}
}

Expand All @@ -127,6 +142,7 @@ export class DynamicDelegationRenderer implements Renderer2 {
}

destroy(): void {
this.replay = null;
this.delegate.destroy();
}

Expand Down Expand Up @@ -195,6 +211,11 @@ export class DynamicDelegationRenderer implements Renderer2 {
}

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);
}

Expand All @@ -205,9 +226,14 @@ export class DynamicDelegationRenderer implements Renderer2 {
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.animationEvents !== null && eventName.startsWith('@')) {
this.animationEvents.push({target, eventName, callback});
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,33 @@ describe('AnimationRenderer', () => {
});
});

it('should store animations properties set on the default renderer and set them also on the animation renderer',
async () => {
const type = <RendererType2>{
id: 'id',
encapsulation: null!,
styles: [],
data: {'animation': []},
};

const factory = TestBed.inject(RendererFactory2) as AsyncAnimationRendererFactory;
const renderer = factory.createRenderer(element, type) as DynamicDelegationRenderer;

renderer.setProperty(element, '@openClose', 'closed');
renderer.setProperty(element, '@openClose', 'open');

// The animation renderer is not loaded yet
expect((renderer['delegate'] as AnimationRenderer).engine).toBeUndefined();

// This will change the delegate renderer from the default one to the AnimationRenderer
await factory['_rendererFactoryPromise']!.then(() => renderer);

const engine = (renderer['delegate'] as AnimationRenderer).engine as MockAnimationEngine;

expect(engine.captures['setProperty'][0][2]).toBe('closed');
expect(engine.captures['setProperty'][1][2]).toBe('open');
});

describe('registering animations', () => {
it('should only create a trigger definition once even if the registered multiple times');
});
Expand Down
3 changes: 3 additions & 0 deletions packages/platform-browser/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ export const enum RuntimeErrorCode {
SANITIZATION_UNSAFE_SCRIPT = 5200,
SANITIZATION_UNSAFE_RESOURCE_URL = 5201,
SANITIZATION_UNEXPECTED_CTX = 5202,

// Animations related errors (5300-5400 range)
ANIMATION_RENDERER_ASYNC_LOADING_FAILURE = 5300
}
1 change: 1 addition & 0 deletions packages/platform-browser/src/private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export {DomEventsPlugin as ɵDomEventsPlugin} from './dom/events/dom_events';
export {HammerGesturesPlugin as ɵHammerGesturesPlugin} from './dom/events/hammer_gestures';
export {KeyEventsPlugin as ɵKeyEventsPlugin} from './dom/events/key_events';
export {SharedStylesHost as ɵSharedStylesHost} from './dom/shared_styles_host';
export {RuntimeErrorCode as ɵRuntimeErrorCode} from './errors';
export {DomSanitizerImpl as ɵDomSanitizerImpl} from './security/dom_sanitization_service';

0 comments on commit 75d610d

Please sign in to comment.