Skip to content

Commit

Permalink
feat(animations): Add the possibility of lazy loading animations code. (
Browse files Browse the repository at this point in the history
#50738)

`provideLazyLoadedAnimations()` returns providers which allow the lazy loading of the animation module.

Lazy loading of the animation code can shave off up to 16KB gzipped of the main bundle.

PR Close #50738
  • Loading branch information
JeanMeche authored and alxhub committed Sep 29, 2023
1 parent e1728a2 commit e753278
Show file tree
Hide file tree
Showing 24 changed files with 1,020 additions and 204 deletions.
14 changes: 14 additions & 0 deletions goldens/public-api/platform-browser/animations/async/index.md
@@ -0,0 +1,14 @@
## API Report File for "@angular/platform-browser_animations_async"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import { Provider } from '@angular/core';

// @public
export function provideAnimationsAsync(type?: 'animations' | 'noop'): Provider[];

// (No @packageDocumentation comment for this package)

```
1 change: 1 addition & 0 deletions packages.bzl
Expand Up @@ -93,6 +93,7 @@ DOCS_ENTRYPOINTS = [
"platform-browser-dynamic",
"platform-browser-dynamic/testing",
"platform-browser/animations",
"platform-browser/animations/async",
"platform-browser/testing",
"platform-server",
"platform-server/init",
Expand Down
22 changes: 22 additions & 0 deletions packages/animations/browser/src/create_engine.ts
@@ -0,0 +1,22 @@
/**
* @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 {NoopAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
import {WebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
import {NoopAnimationDriver} from './render/animation_driver';
import {AnimationEngine} from './render/animation_engine_next';
import {WebAnimationsDriver} from './render/web_animations/web_animations_driver';

export function createEngine(type: 'animations'|'noop', doc: Document): AnimationEngine {
// TODO: find a way to make this tree shakable.
if (type === 'noop') {
return new AnimationEngine(doc, new NoopAnimationDriver(), new NoopAnimationStyleNormalizer());
}

return new AnimationEngine(doc, new WebAnimationsDriver(), new WebAnimationsStyleNormalizer());
}
4 changes: 3 additions & 1 deletion packages/animations/browser/src/private_export.ts
Expand Up @@ -5,11 +5,13 @@
* 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
*/
export {createEngine as ɵcreateEngine} from './create_engine';
export {Animation as ɵAnimation} from './dsl/animation';
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationStyleNormalizer as ɵNoopAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine_next';
export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './render/animation_renderer';
export {AnimationRendererFactory as ɵAnimationRendererFactory} from './render/animation_renderer';
export {AnimationRenderer as ɵAnimationRenderer, BaseAnimationRenderer as ɵBaseAnimationRenderer} from './render/renderer';
export {containsElement as ɵcontainsElement, getParentElement as ɵgetParentElement, invokeQuery as ɵinvokeQuery, validateStyleProperty as ɵvalidateStyleProperty, validateWebAnimatableStyleProperty as ɵvalidateWebAnimatableStyleProperty} from './render/shared';
export {WebAnimationsDriver as ɵWebAnimationsDriver} from './render/web_animations/web_animations_driver';
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';
Expand Down
198 changes: 12 additions & 186 deletions packages/animations/browser/src/render/animation_renderer.ts
Expand Up @@ -6,20 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationTriggerMetadata} from '@angular/animations';
import {Injectable, NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '@angular/core';
import type {NgZone, Renderer2, RendererFactory2, RendererType2} from '@angular/core';

import {AnimationEngine} from './animation_engine_next';

const ANIMATION_PREFIX = '@';
const DISABLE_ANIMATIONS_FLAG = '@.disabled';
import {AnimationRenderer, BaseAnimationRenderer} from './renderer';

// Define a recursive type to allow for nested arrays of `AnimationTriggerMetadata`. Note that an
// interface declaration is used as TypeScript prior to 3.7 does not support recursive type
// references, see https://github.com/microsoft/TypeScript/pull/33050 for details.
type NestedAnimationTriggerMetadata = AnimationTriggerMetadata|RecursiveAnimationTriggerMetadata;
interface RecursiveAnimationTriggerMetadata extends Array<NestedAnimationTriggerMetadata> {}

@Injectable()
export class AnimationRendererFactory implements RendererFactory2 {
private _currentId: number = 0;
private _microtaskId: number = 1;
Expand Down Expand Up @@ -47,16 +44,17 @@ export class AnimationRendererFactory implements RendererFactory2 {
// cache the delegates to find out which cached delegate can
// be used by which cached renderer
const delegate = this.delegate.createRenderer(hostElement, type);
if (!hostElement || !type || !type.data || !type.data['animation']) {
let renderer: BaseAnimationRenderer|undefined = this._rendererCache.get(delegate);
if (!hostElement || !type?.data?.['animation']) {
const cache = this._rendererCache;
let renderer: BaseAnimationRenderer|undefined = cache.get(delegate);
if (!renderer) {
// Ensure that the renderer is removed from the cache on destroy
// since it may contain references to detached DOM nodes.
const onRendererDestroy = () => this._rendererCache.delete(delegate);
const onRendererDestroy = () => cache.delete(delegate);
renderer =
new BaseAnimationRenderer(EMPTY_NAMESPACE_ID, delegate, this.engine, onRendererDestroy);
// only cache this result when the base renderer is used
this._rendererCache.set(delegate, renderer);
cache.set(delegate, renderer);
}
return renderer;
}
Expand Down Expand Up @@ -100,19 +98,19 @@ export class AnimationRendererFactory implements RendererFactory2 {
return;
}

if (this._animationCallbacksBuffer.length == 0) {
const animationCallbacksBuffer = this._animationCallbacksBuffer;
if (animationCallbacksBuffer.length == 0) {
queueMicrotask(() => {
this._zone.run(() => {
this._animationCallbacksBuffer.forEach(tuple => {
animationCallbacksBuffer.forEach(tuple => {
const [fn, data] = tuple;
fn(data);
});
this._animationCallbacksBuffer = [];
});
});
}

this._animationCallbacksBuffer.push([fn, data]);
animationCallbacksBuffer.push([fn, data]);
}

end() {
Expand All @@ -134,176 +132,4 @@ export class AnimationRendererFactory implements RendererFactory2 {
whenRenderingDone(): Promise<any> {
return this.engine.whenRenderingDone();
}
}

export class BaseAnimationRenderer implements Renderer2 {
constructor(
protected namespaceId: string, public delegate: Renderer2, public engine: AnimationEngine,
private _onDestroy?: () => void) {}

get data() {
return this.delegate.data;
}

destroyNode(node: any): void {
this.delegate.destroyNode?.(node);
}

destroy(): void {
this.engine.destroy(this.namespaceId, this.delegate);
this.engine.afterFlushAnimationsDone(() => {
// Call the renderer destroy method after the animations has finished as otherwise
// styles will be removed too early which will cause an unstyled animation.
queueMicrotask(() => {
this.delegate.destroy();
});
});

this._onDestroy?.();
}

createElement(name: string, namespace?: string|null|undefined) {
return this.delegate.createElement(name, namespace);
}

createComment(value: string) {
return this.delegate.createComment(value);
}

createText(value: string) {
return this.delegate.createText(value);
}

appendChild(parent: any, newChild: any): void {
this.delegate.appendChild(parent, newChild);
this.engine.onInsert(this.namespaceId, newChild, parent, false);
}

insertBefore(parent: any, newChild: any, refChild: any, isMove: boolean = true): void {
this.delegate.insertBefore(parent, newChild, refChild);
// If `isMove` true than we should animate this insert.
this.engine.onInsert(this.namespaceId, newChild, parent, isMove);
}

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

selectRootElement(selectorOrNode: any, preserveContent?: boolean) {
return this.delegate.selectRootElement(selectorOrNode, preserveContent);
}

parentNode(node: any) {
return this.delegate.parentNode(node);
}

nextSibling(node: 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 {
if (name.charAt(0) == ANIMATION_PREFIX && name == DISABLE_ANIMATIONS_FLAG) {
this.disableAnimations(el, !!value);
} else {
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 {
return this.delegate.listen(target, eventName, callback);
}

protected disableAnimations(element: any, value: boolean) {
this.engine.disableAnimations(element, value);
}
}

export class AnimationRenderer extends BaseAnimationRenderer implements Renderer2 {
constructor(
public factory: AnimationRendererFactory, namespaceId: string, delegate: Renderer2,
engine: AnimationEngine, onDestroy?: () => void) {
super(namespaceId, delegate, engine, onDestroy);
this.namespaceId = namespaceId;
}

override setProperty(el: any, name: string, value: any): void {
if (name.charAt(0) == ANIMATION_PREFIX) {
if (name.charAt(1) == '.' && name == DISABLE_ANIMATIONS_FLAG) {
value = value === undefined ? true : !!value;
this.disableAnimations(el, value as boolean);
} else {
this.engine.process(this.namespaceId, el, name.slice(1), value);
}
} else {
this.delegate.setProperty(el, name, value);
}
}

override listen(
target: 'window'|'document'|'body'|any, eventName: string,
callback: (event: any) => any): () => void {
if (eventName.charAt(0) == ANIMATION_PREFIX) {
const element = resolveElementFromTarget(target);
let name = eventName.slice(1);
let phase = '';
// @listener.phase is for trigger animation callbacks
// @@listener is for animation builder callbacks
if (name.charAt(0) != ANIMATION_PREFIX) {
[name, phase] = parseTriggerCallbackName(name);
}
return this.engine.listen(this.namespaceId, element, name, phase, event => {
const countId = (event as any)['_data'] || -1;
this.factory.scheduleListenerCallback(countId, callback, event);
});
}
return this.delegate.listen(target, eventName, callback);
}
}

function resolveElementFromTarget(target: 'window'|'document'|'body'|any): any {
switch (target) {
case 'body':
return document.body;
case 'document':
return document;
case 'window':
return window;
default:
return target;
}
}

function parseTriggerCallbackName(triggerName: string) {
const dotIndex = triggerName.indexOf('.');
const trigger = triggerName.substring(0, dotIndex);
const phase = triggerName.slice(dotIndex + 1);
return [trigger, phase];
}
}

0 comments on commit e753278

Please sign in to comment.