ngx-lottie
Version:
<h1 align="center"> <img src="https://raw.githubusercontent.com/ngx-lottie/ngx-lottie/refs/heads/master/docs/assets/logo.png"> </h1>
420 lines (408 loc) • 21.2 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, NgZone, ɵisPromise as _isPromise, Injectable, DestroyRef, input, PLATFORM_ID, Output, Directive, ElementRef, ViewChild, ChangeDetectionStrategy, Component, TransferState, makeStateKey } from '@angular/core';
import { Observable, map as map$1, switchMap, Subject, BehaviorSubject, defer } from 'rxjs';
import { tap, shareReplay, mergeMap, map, filter, switchMap as switchMap$1 } from 'rxjs/operators';
import { isPlatformBrowser, NgStyle, NgClass } from '@angular/common';
const LOTTIE_OPTIONS = new InjectionToken('LottieOptions');
function convertPlayerOrLoaderToObservable() {
const ngZone = inject(NgZone);
const { player, useWebWorker } = inject(LOTTIE_OPTIONS);
const player$ = new Observable(subscriber => {
// Call the `player` function lazily — only when a subscriber
// arrives — to avoid importing `lottie-web` on the server, as it will
// fail with a "document is not defined" error.
const playerOrLoader = ngZone.runOutsideAngular(() => player());
// We need to use `isPromise` instead of checking whether
// `result instanceof Promise`. In zone.js patched environments, `global.Promise`
// is the `ZoneAwarePromise`. Some APIs, which are likely not patched by zone.js
// for certain reasons, might not work with `instanceof`. For instance, the dynamic
// import `() => import('./chunk.js')` returns a native promise (not a `ZoneAwarePromise`),
// causing this check to be falsy.
if (_isPromise(playerOrLoader)) {
playerOrLoader
.then(module => {
subscriber.next(module.default || module);
subscriber.complete();
})
.catch(error => subscriber.error(error));
}
else {
subscriber.next(playerOrLoader);
subscriber.complete();
}
});
return player$.pipe(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tap(player => player.useWebWorker?.(useWebWorker)), shareReplay({ bufferSize: 1, refCount: true }));
}
class AnimationLoader {
constructor() {
this.player$ = convertPlayerOrLoaderToObservable().pipe(mergeMap(player => raf$(this.ngZone).pipe(map(() => player))));
this.ngZone = inject(NgZone);
}
loadAnimation(options) {
return this.player$.pipe(map(player => this.createAnimationItem(player, options)));
}
resolveOptions(options, container) {
return Object.assign({
container,
renderer: 'svg',
loop: true,
autoplay: true,
}, options);
}
createAnimationItem(player, options) {
return this.ngZone.runOutsideAngular(() => player.loadAnimation(options));
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: AnimationLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: AnimationLoader, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: AnimationLoader, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
function raf$(ngZone) {
return new Observable(subscriber => {
const requestId = ngZone.runOutsideAngular(() => requestAnimationFrame(() => {
subscriber.next();
subscriber.complete();
}));
return () => cancelAnimationFrame(requestId);
});
}
class CacheableAnimationLoader extends AnimationLoader {
constructor() {
super();
/** Cache storing animation data as JSON strings, keyed by file path */
this.cache = new Map();
/** Tracks in-flight HTTP requests to prevent duplicate fetches for the same animation */
this.pending = new Map();
inject(DestroyRef).onDestroy(() => {
this.cache.clear();
this.pending.clear();
});
}
loadAnimation(options) {
return this.player$.pipe(map$1(async (player) => {
// Transform options to either use cached data or wait for pending requests.
// This prevents duplicate HTTP requests when multiple animations with the
// same path are loaded simultaneously before the first request completes.
const transformedOptions = await this.transformOptions(options);
const animationItem = this.createAnimationItem(player, transformedOptions);
// For path-based animations, listen for `config_ready` to cache the data.
this.awaitConfigAndCache(options, animationItem);
return animationItem;
}),
// Flatten Promise<AnimationItem> to AnimationItem for RxJS stream compatibility.
switchMap(promise => promise));
}
/**
* Sets up caching for path-based animations by listening to the `config_ready` event.
* Only the first request for a given path will register this listener - subsequent
* requests will be handled by transformOptions.
*/
awaitConfigAndCache(options, animationItem) {
if (!this.isAnimationConfigWithPath(options)) {
return;
}
const path = options.path;
// Skip if already cached or another instance is already listening.
if (this.cache.has(path) || this.pending.has(path)) {
return;
}
// Create a promise that resolves when lottie-web fires `config_ready`.
const promise = new Promise(resolve => {
animationItem.addEventListener('config_ready', () => {
// Serialize animation data to string to avoid object mutation issues.
// lottie-web cannot re-use animationData objects between animations.
const data = JSON.stringify(animationItem['animationData']);
this.cache.set(path, data);
this.pending.delete(path);
resolve(data);
});
});
this.pending.set(path, promise);
}
/**
* Transforms animation options to use cached data when available or wait for
* pending requests to complete. This prevents duplicate HTTP requests.
*
* Flow:
* 1. If cached: return immediately with `animationData`
* 2. If loading: await the pending promise, then return with `animationData`
* 3. If first request: return original options with path (triggers HTTP fetch)
*/
async transformOptions(options) {
if (!this.isAnimationConfigWithPath(options)) {
return options;
}
const path = options.path;
// Cache hit: return parsed `animationData` immediately.
if (this.cache.has(path)) {
return {
...options,
path: undefined,
// Parse JSON to create a new object - lottie-web requires fresh objects.
animationData: JSON.parse(this.cache.get(path)),
};
}
// Pending request: wait for the first request to complete, then use its data.
if (this.pending.has(path)) {
const data = await this.pending.get(path);
return {
...options,
path: undefined,
animationData: JSON.parse(data),
};
}
// First request: let lottie-web fetch the animation via the path option.
return options;
}
isAnimationConfigWithPath(options) {
return typeof options.path === 'string';
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CacheableAnimationLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CacheableAnimationLoader, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CacheableAnimationLoader, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
function provideCacheableAnimationLoader() {
return [
{
provide: AnimationLoader,
useExisting: CacheableAnimationLoader,
},
];
}
function provideLottieOptions(options) {
return [
{
provide: LOTTIE_OPTIONS,
useValue: options,
},
];
}
class BaseDirective {
constructor() {
this.options = input(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
this.containerClass = input(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "containerClass" }] : /* istanbul ignore next */ []));
this.styles = input(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "styles" }] : /* istanbul ignore next */ []));
/**
* `animationCreated` is dispatched after calling `loadAnimation`.
*/
this.animationCreated = this.getAnimationItem();
/**
* `complete` is dispatched after completing the last frame.
*/
this.complete = this.awaitAnimationItemAndStartListening('complete');
/**
* `loopComplete` is dispatched after completing the frame loop.
*/
this.loopComplete = this.awaitAnimationItemAndStartListening('loopComplete');
/**
* `enterFrame` is dispatched after entering the new frame.
*/
this.enterFrame = this.awaitAnimationItemAndStartListening('enterFrame');
/**
* `segmentStart` is dispatched when the new segment is adjusted.
*/
this.segmentStart = this.awaitAnimationItemAndStartListening('segmentStart');
/**
* Original event name is `config_ready`. `config_ready` is dispatched
* after the needed renderer is configured.
*/
this.configReady = this.awaitAnimationItemAndStartListening('config_ready');
/**
* Original event name is `data_ready`. `data_ready` is dispatched
* when all parts of the animation have been loaded.
*/
this.dataReady = this.awaitAnimationItemAndStartListening('data_ready');
/**
* Original event name is `DOMLoaded`. `DOMLoaded` is dispatched
* when elements have been added to the DOM.
*/
this.domLoaded = this.awaitAnimationItemAndStartListening('DOMLoaded');
/**
* `destroy` will be dispatched when the component gets destroyed,
* it's handy for releasing resources.
*/
this.destroy = this.awaitAnimationItemAndStartListening('destroy');
/**
* `error` will be dispatched if the Lottie player could not render
* some frame or parse config.
*/
this.error = this.awaitAnimationItemAndStartListening('error');
this.ngZone = inject(NgZone);
this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
this.animationLoader = inject(AnimationLoader);
this.loadAnimation$ = new Subject();
this.animationItem$ = new BehaviorSubject(null);
this.setupLoadAnimationListener();
}
ngOnDestroy() {
this.destroyAnimation();
this.loadAnimation$.complete();
this.animationItem$.complete();
}
loadAnimation(changes, container) {
this.ngZone.runOutsideAngular(() => this.loadAnimation$.next([changes, container]));
}
getAnimationItem() {
return defer(() => this.animationItem$).pipe(filter((animationItem) => animationItem !== null));
}
awaitAnimationItemAndStartListening(name) {
return this.getAnimationItem().pipe(switchMap$1(animationItem =>
// `fromEvent` will try to call `removeEventListener` when `unsubscribe()` is invoked.
// The problem is that `ngOnDestroy()` is called before Angular unsubscribes from
// `@Output()` properties, thus `animationItem` will be `null` already, also `lottie-web`
// removes event listeners when calling `destroy()`.
new Observable(observer => {
this.ngZone.runOutsideAngular(() => {
animationItem.addEventListener(name, event => {
this.ngZone.runOutsideAngular(() => {
observer.next(event);
});
});
});
})));
}
setupLoadAnimationListener() {
const loadAnimation$ = this.loadAnimation$.pipe(filter(([changes]) => this.isBrowser && changes.options !== undefined));
loadAnimation$
.pipe(switchMap$1(([changes, container]) => {
this.destroyAnimation();
return this.animationLoader.loadAnimation(this.animationLoader.resolveOptions(changes.options.currentValue, container));
}))
.subscribe(animationItem => {
this.ngZone.run(() => this.animationItem$.next(animationItem));
});
}
destroyAnimation() {
const animationItem = this.animationItem$.getValue();
// The `ng-lottie` component or the `lottie` directive can be destroyed
// before the `animationItem` is set, thus it will fail with
// `Cannot read property 'destroy' of null`.
// Potentially it can happen if the directive gets destroyed before change
// detection is run.
if (animationItem === null) {
return;
}
// `destroy()` will remove all events listeners.
animationItem.destroy();
this.animationItem$.next(null);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BaseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: BaseDirective, isStandalone: true, selector: "[lottie]", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, containerClass: { classPropertyName: "containerClass", publicName: "containerClass", isSignal: true, isRequired: false, transformFunction: null }, styles: { classPropertyName: "styles", publicName: "styles", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { animationCreated: "animationCreated", complete: "complete", loopComplete: "loopComplete", enterFrame: "enterFrame", segmentStart: "segmentStart", configReady: "configReady", dataReady: "dataReady", domLoaded: "domLoaded", destroy: "destroy", error: "error" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BaseDirective, decorators: [{
type: Directive,
args: [{ selector: '[lottie]' }]
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], containerClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "containerClass", required: false }] }], styles: [{ type: i0.Input, args: [{ isSignal: true, alias: "styles", required: false }] }], animationCreated: [{
type: Output
}], complete: [{
type: Output
}], loopComplete: [{
type: Output
}], enterFrame: [{
type: Output
}], segmentStart: [{
type: Output
}], configReady: [{
type: Output
}], dataReady: [{
type: Output
}], domLoaded: [{
type: Output
}], destroy: [{
type: Output
}], error: [{
type: Output
}] } });
class LottieDirective extends BaseDirective {
constructor() {
super(...arguments);
this.host = inject(ElementRef);
}
ngOnChanges(changes) {
super.loadAnimation(changes, this.host.nativeElement);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LottieDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: LottieDirective, isStandalone: true, selector: "[lottie]", usesInheritance: true, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LottieDirective, decorators: [{
type: Directive,
args: [{ selector: '[lottie]', standalone: true }]
}] });
class LottieComponent extends BaseDirective {
constructor() {
super(...arguments);
this.width = input(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
this.height = input(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
this.container = null;
}
ngOnChanges(changes) {
super.loadAnimation(changes, this.container.nativeElement);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LottieComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.2", type: LottieComponent, isStandalone: true, selector: "ng-lottie", inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
<div
#container
[style.width]="width() || '100%'"
[style.height]="height() || '100%'"
[ngStyle]="styles()"
[ngClass]="containerClass()"
></div>
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LottieComponent, decorators: [{
type: Component,
args: [{
selector: 'ng-lottie',
template: `
<div
#container
[style.width]="width() || '100%'"
[style.height]="height() || '100%'"
[ngStyle]="styles()"
[ngClass]="containerClass()"
></div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgStyle, NgClass],
}]
}], propDecorators: { width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], container: [{
type: ViewChild,
args: ['container', { static: true }]
}] } });
function transformAnimationFilenameToKey(animation) {
const [animationName] = animation.split('.json');
return `animation-${animationName}`;
}
class LottieTransferState {
constructor() {
this.transferState = inject(TransferState);
}
get(animation) {
const animationKey = transformAnimationFilenameToKey(animation);
const stateKey = makeStateKey(animationKey);
return this.transferState.get(stateKey, null);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LottieTransferState, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LottieTransferState, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LottieTransferState, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { AnimationLoader, BaseDirective, LottieComponent, LottieDirective, LottieTransferState, provideCacheableAnimationLoader, provideLottieOptions, transformAnimationFilenameToKey, CacheableAnimationLoader as ɵCacheableAnimationLoader, LOTTIE_OPTIONS as ɵLOTTIE_OPTIONS };
//# sourceMappingURL=ngx-lottie.mjs.map