UNPKG

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
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