ngx-lottie
Version:
<table> <thead> <tr> <th>ngx-lottie</th> <th>Angular</th> </tr> </thead> <tbody> <tr> <td> 7.x </td> <td> >= 8 < 13 </td> </tr> <tr> <td> 8.x </td> <td> 13 </td> </tr> <tr> <td> 9.x </td> <td> 14 </td> </tr> <tr> <td> 10.x </td> <td> 15 </td> </tr> <tr> <
370 lines (358 loc) • 17.9 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, NgZone, ɵisPromise as _isPromise, Injectable, input, PLATFORM_ID, Directive, Output, ElementRef, Component, ChangeDetectionStrategy, ViewChild, TransferState, makeStateKey } from '@angular/core';
import { Observable, Subject, BehaviorSubject, defer } from 'rxjs';
import { tap, shareReplay, mergeMap, map, filter, switchMap } 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: "20.0.3", ngImport: i0, type: AnimationLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AnimationLoader, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", 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(...arguments);
this.cache = new Map();
}
ngOnDestroy() {
this.cache.clear();
}
loadAnimation(options) {
return this.player$.pipe(map(player => {
const animationItem = this.createAnimationItem(player, this.transformOptions(options));
this.awaitConfigAndCache(options, animationItem);
return animationItem;
}));
}
awaitConfigAndCache(options, animationItem) {
if (this.isAnimationConfigWithPath(options)) {
// Don't wait for the `config_ready` event if it has been cached previously.
if (this.cache.has(options.path)) {
return;
}
animationItem.addEventListener('config_ready', () => {
// See the comments below on why we're storing the animation data as a string.
this.cache.set(options.path, JSON.stringify(animationItem['animationData']));
});
}
}
transformOptions(options) {
if (this.isAnimationConfigWithPath(options) && this.cache.has(options.path)) {
return {
...options,
path: undefined,
// Caretaker note: `lottie-web` cannot re-use the `animationData` object between animations, and we
// have to retrieve a new object each time an animation is created.
// https://github.com/airbnb/lottie-web#html
// See comments for the `animationData` property.
animationData: JSON.parse(this.cache.get(options.path)),
};
}
else {
return options;
}
}
isAnimationConfigWithPath(options) {
return typeof options.path === 'string';
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CacheableAnimationLoader, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CacheableAnimationLoader, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CacheableAnimationLoader, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
function provideCacheableAnimationLoader() {
return [
{
provide: AnimationLoader,
useExisting: CacheableAnimationLoader,
},
];
}
function provideLottieOptions(options) {
return [
{
provide: LOTTIE_OPTIONS,
useValue: options,
},
];
}
class BaseDirective {
constructor() {
this.options = input(null);
this.containerClass = input(null);
this.styles = input(null);
/**
* `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(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(([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: "20.0.3", ngImport: i0, type: BaseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.3", 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: "20.0.3", ngImport: i0, type: BaseDirective, decorators: [{
type: Directive,
args: [{ selector: '[lottie]' }]
}], ctorParameters: () => [], propDecorators: { 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: "20.0.3", ngImport: i0, type: LottieDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: LottieDirective, isStandalone: true, selector: "[lottie]", usesInheritance: true, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: LottieDirective, decorators: [{
type: Directive,
args: [{ selector: '[lottie]', standalone: true }]
}] });
class LottieComponent extends BaseDirective {
constructor() {
super(...arguments);
this.width = input(null);
this.height = input(null);
this.container = null;
}
ngOnChanges(changes) {
super.loadAnimation(changes, this.container.nativeElement);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: LottieComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", 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: "20.0.3", 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: { 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: "20.0.3", ngImport: i0, type: LottieTransferState, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: LottieTransferState, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", 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