diff --git a/libs/ngx-lottie/src/index.ts b/libs/ngx-lottie/src/index.ts index a53997f..9f92578 100644 --- a/libs/ngx-lottie/src/index.ts +++ b/libs/ngx-lottie/src/index.ts @@ -1,6 +1,3 @@ -export { LottieModule } from './lib/lottie.module'; -export { LottieCacheModule } from './lib/cacheable-animation-loader/lottie-cache.module'; - export { AnimationLoader } from './lib/animation-loader'; export { provideLottieOptions, provideCacheableAnimationLoader } from './lib/providers'; diff --git a/libs/ngx-lottie/src/lib/animation-loader.ts b/libs/ngx-lottie/src/lib/animation-loader.ts index c5ab7cb..0b76a77 100644 --- a/libs/ngx-lottie/src/lib/animation-loader.ts +++ b/libs/ngx-lottie/src/lib/animation-loader.ts @@ -1,47 +1,40 @@ -import { Injectable, NgZone, Inject } from '@angular/core'; +import { Injectable, NgZone, inject } from '@angular/core'; -import { Observable, from, of, animationFrameScheduler } from 'rxjs'; -import { map, observeOn, shareReplay, tap } from 'rxjs/operators'; +import { Observable, from, of } from 'rxjs'; +import { map, mergeMap, shareReplay, tap } from 'rxjs/operators'; import { LOTTIE_OPTIONS, LottiePlayer, - LottieOptions, AnimationItem, AnimationOptions, AnimationConfigWithData, AnimationConfigWithPath, - LottiePlayerFactoryOrLoader, } from './symbols'; -function convertPlayerOrLoaderToObservable( - player: LottiePlayerFactoryOrLoader, - useWebWorker?: boolean, -): Observable { - const playerOrLoader = player(); +function convertPlayerOrLoaderToObservable(): Observable { + const ngZone = inject(NgZone); + const { player, useWebWorker } = inject(LOTTIE_OPTIONS); + const playerOrLoader = ngZone.runOutsideAngular(() => player()); const player$ = playerOrLoader instanceof Promise ? from(playerOrLoader).pipe(map(module => module.default || module)) : of(playerOrLoader); return player$.pipe( - tap(player => - (player as unknown as { useWebWorker: (useWebWorker?: boolean) => void }).useWebWorker( - useWebWorker, - ), - ), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tap(player => (player as any).useWebWorker?.(useWebWorker)), shareReplay({ bufferSize: 1, refCount: true }), ); } @Injectable({ providedIn: 'root' }) export class AnimationLoader { - protected player$ = convertPlayerOrLoaderToObservable( - this.options.player, - this.options.useWebWorker, - ).pipe(observeOn(animationFrameScheduler)); + protected player$ = convertPlayerOrLoaderToObservable().pipe( + mergeMap(player => raf$(this.ngZone).pipe(map(() => player))), + ); - constructor(private ngZone: NgZone, @Inject(LOTTIE_OPTIONS) private options: LottieOptions) {} + private ngZone = inject(NgZone); loadAnimation( options: AnimationConfigWithData | AnimationConfigWithPath, @@ -71,3 +64,15 @@ export class AnimationLoader { return this.ngZone.runOutsideAngular(() => player.loadAnimation(options)); } } + +function raf$(ngZone: NgZone) { + return new Observable(subscriber => { + const requestId = ngZone.runOutsideAngular(() => + requestAnimationFrame(() => { + subscriber.next(); + subscriber.complete(); + }), + ); + return () => cancelAnimationFrame(requestId); + }); +} diff --git a/libs/ngx-lottie/src/lib/base.directive.ts b/libs/ngx-lottie/src/lib/base.directive.ts index d01d4c8..0570438 100644 --- a/libs/ngx-lottie/src/lib/base.directive.ts +++ b/libs/ngx-lottie/src/lib/base.directive.ts @@ -2,16 +2,17 @@ import { Directive, Input, Output, - Inject, PLATFORM_ID, - OnDestroy, SimpleChanges, NgZone, + inject, + OnDestroy, } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Subject, BehaviorSubject, Observable, defer } from 'rxjs'; -import { filter, switchMap, takeUntil } from 'rxjs/operators'; +import { filter, switchMap } from 'rxjs/operators'; import { AnimationOptions, @@ -38,83 +39,81 @@ export class BaseDirective implements OnDestroy { /** * `animationCreated` is dispatched after calling `loadAnimation`. */ - @Output() animationCreated = this.getAnimationItem(); + @Output() readonly animationCreated = this.getAnimationItem(); /** * `complete` is dispatched after completing the last frame. */ - @Output() complete = this.awaitAnimationItemAndStartListening('complete'); + @Output() readonly complete = + this.awaitAnimationItemAndStartListening('complete'); /** * `loopComplete` is dispatched after completing the frame loop. */ - @Output() loopComplete = + @Output() readonly loopComplete = this.awaitAnimationItemAndStartListening('loopComplete'); /** * `enterFrame` is dispatched after entering the new frame. */ - @Output() enterFrame = this.awaitAnimationItemAndStartListening('enterFrame'); + @Output() readonly enterFrame = + this.awaitAnimationItemAndStartListening('enterFrame'); /** * `segmentStart` is dispatched when the new segment is adjusted. */ - @Output() segmentStart = + @Output() readonly segmentStart = this.awaitAnimationItemAndStartListening('segmentStart'); /** * Original event name is `config_ready`. `config_ready` is dispatched * after the needed renderer is configured. */ - @Output() configReady = this.awaitAnimationItemAndStartListening('config_ready'); + @Output() readonly configReady = this.awaitAnimationItemAndStartListening('config_ready'); /** * Original event name is `data_ready`. `data_ready` is dispatched * when all parts of the animation have been loaded. */ - @Output() dataReady = this.awaitAnimationItemAndStartListening('data_ready'); + @Output() readonly dataReady = this.awaitAnimationItemAndStartListening('data_ready'); /** * Original event name is `DOMLoaded`. `DOMLoaded` is dispatched * when elements have been added to the DOM. */ - @Output() domLoaded = this.awaitAnimationItemAndStartListening('DOMLoaded'); + @Output() readonly domLoaded = this.awaitAnimationItemAndStartListening('DOMLoaded'); /** * `destroy` will be dispatched when the component gets destroyed, * it's handy for releasing resources. */ - @Output() destroy = this.awaitAnimationItemAndStartListening('destroy'); + @Output() readonly destroy = this.awaitAnimationItemAndStartListening('destroy'); /** * `error` will be dispatched if the Lottie player could not render * some frame or parse config. */ - @Output() error = this.awaitAnimationItemAndStartListening< + @Output() readonly error = this.awaitAnimationItemAndStartListening< BMRenderFrameErrorEvent | BMConfigErrorEvent >('error'); - private destroy$ = new Subject(); + private ngZone = inject(NgZone); + private isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); + + private animationLoader = inject(AnimationLoader); + private loadAnimation$ = new Subject<[SimpleChanges, HTMLElement]>(); private animationItem$ = new BehaviorSubject(null); - constructor( - private ngZone: NgZone, - @Inject(PLATFORM_ID) private platformId: string, - private animationLoader: AnimationLoader, - ) { + constructor() { this.setupLoadAnimationListener(); } ngOnDestroy(): void { - this.destroy$.next(); this.destroyAnimation(); } protected loadAnimation(changes: SimpleChanges, container: HTMLElement): void { - // The `loadAnimation` may load `lottie-web` asynchronously and also pipes the player - // with `animationFrameScheduler`, which schedules an animation task and triggers change - // detection. We'll trigger change detection only once when the animation item is created. this.ngZone.runOutsideAngular(() => this.loadAnimation$.next([changes, container])); } @@ -150,7 +149,7 @@ export class BaseDirective implements OnDestroy { private setupLoadAnimationListener(): void { const loadAnimation$ = this.loadAnimation$.pipe( - filter(([changes]) => isPlatformBrowser(this.platformId) && changes.options !== undefined), + filter(([changes]) => this.isBrowser && changes.options !== undefined), ); loadAnimation$ @@ -161,7 +160,7 @@ export class BaseDirective implements OnDestroy { this.animationLoader.resolveOptions(changes.options.currentValue, container), ); }), - takeUntil(this.destroy$), + takeUntilDestroyed(), ) .subscribe(animationItem => { this.ngZone.run(() => this.animationItem$.next(animationItem)); diff --git a/libs/ngx-lottie/src/lib/cacheable-animation-loader/lottie-cache.module.ts b/libs/ngx-lottie/src/lib/cacheable-animation-loader/lottie-cache.module.ts deleted file mode 100644 index e4a77e8..0000000 --- a/libs/ngx-lottie/src/lib/cacheable-animation-loader/lottie-cache.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ModuleWithProviders, NgModule } from '@angular/core'; - -import { AnimationLoader } from '../animation-loader'; -import { CacheableAnimationLoader } from './cacheable-animation-loader'; - -@NgModule() -export class LottieCacheModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: LottieCacheModule, - providers: [ - { - provide: AnimationLoader, - useExisting: CacheableAnimationLoader, - }, - ], - }; - } -} diff --git a/libs/ngx-lottie/src/lib/lottie.component.ts b/libs/ngx-lottie/src/lib/lottie.component.ts index a1edb9a..41391e4 100644 --- a/libs/ngx-lottie/src/lib/lottie.component.ts +++ b/libs/ngx-lottie/src/lib/lottie.component.ts @@ -2,18 +2,14 @@ import { Component, ChangeDetectionStrategy, Input, - Inject, ElementRef, ViewChild, - PLATFORM_ID, OnChanges, SimpleChanges, - NgZone, } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; import { BaseDirective } from './base.directive'; -import { AnimationLoader } from './animation-loader'; @Component({ selector: 'ng-lottie', @@ -28,7 +24,7 @@ import { AnimationLoader } from './animation-loader'; `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule], + imports: [NgStyle, NgClass], }) export class LottieComponent extends BaseDirective implements OnChanges { @Input() width: string | null = null; @@ -36,14 +32,6 @@ export class LottieComponent extends BaseDirective implements OnChanges { @ViewChild('container', { static: true }) container: ElementRef = null!; - constructor( - ngZone: NgZone, - @Inject(PLATFORM_ID) platformId: string, - animationLoader: AnimationLoader, - ) { - super(ngZone, platformId, animationLoader); - } - ngOnChanges(changes: SimpleChanges): void { super.loadAnimation(changes, this.container.nativeElement); } diff --git a/libs/ngx-lottie/src/lib/lottie.directive.ts b/libs/ngx-lottie/src/lib/lottie.directive.ts index 2342a9b..cf09613 100644 --- a/libs/ngx-lottie/src/lib/lottie.directive.ts +++ b/libs/ngx-lottie/src/lib/lottie.directive.ts @@ -1,27 +1,10 @@ -import { - Directive, - Inject, - Self, - ElementRef, - PLATFORM_ID, - OnChanges, - SimpleChanges, - NgZone, -} from '@angular/core'; +import { Directive, ElementRef, OnChanges, SimpleChanges, inject } from '@angular/core'; import { BaseDirective } from './base.directive'; -import { AnimationLoader } from './animation-loader'; @Directive({ selector: '[lottie]', standalone: true }) export class LottieDirective extends BaseDirective implements OnChanges { - constructor( - ngZone: NgZone, - @Inject(PLATFORM_ID) platformId: string, - @Self() private host: ElementRef, - animationLoader: AnimationLoader, - ) { - super(ngZone, platformId, animationLoader); - } + private host = inject(ElementRef); ngOnChanges(changes: SimpleChanges): void { super.loadAnimation(changes, this.host.nativeElement); diff --git a/libs/ngx-lottie/src/lib/lottie.module.ts b/libs/ngx-lottie/src/lib/lottie.module.ts deleted file mode 100644 index a4d0dcd..0000000 --- a/libs/ngx-lottie/src/lib/lottie.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NgModule, ModuleWithProviders } from '@angular/core'; - -import { LottieDirective } from './lottie.directive'; -import { LottieComponent } from './lottie.component'; -import { LottieOptions, LOTTIE_OPTIONS } from './symbols'; - -@NgModule({ - imports: [LottieDirective, LottieComponent], - exports: [LottieDirective, LottieComponent], -}) -export class LottieModule { - static forRoot(options: LottieOptions): ModuleWithProviders { - return { - ngModule: LottieModule, - providers: [ - { - provide: LOTTIE_OPTIONS, - useValue: options, - }, - ], - }; - } -} diff --git a/libs/ngx-lottie/tests/lottie.spec.ts b/libs/ngx-lottie/tests/lottie.spec.ts index c1422a3..de05c6e 100644 --- a/libs/ngx-lottie/tests/lottie.spec.ts +++ b/libs/ngx-lottie/tests/lottie.spec.ts @@ -31,7 +31,7 @@ HTMLCanvasElement.prototype.getContext = () => ({ import * as lottie from 'lottie-web'; -import { LottieModule, BMDestroyEvent } from '../src'; +import { provideLottieOptions, BMDestroyEvent, LottieComponent, LottieDirective } from '../src'; import { AnimationOptions, AnimationItem } from '../src/lib/symbols'; import animationData = require('./data.json'); @@ -65,6 +65,8 @@ describe('ngx-lottie', () => { (destroy)="destroy($event)" > `, + standalone: true, + imports: [LottieComponent], }) class MockComponent { options: AnimationOptions = { @@ -102,8 +104,8 @@ describe('ngx-lottie', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [LottieModule.forRoot({ player: playerFactory })], - declarations: [MockComponent], + imports: [MockComponent], + providers: [provideLottieOptions({ player: playerFactory })], }); }); @@ -197,6 +199,8 @@ describe('ngx-lottie', () => { (destroy)="destroy($event)" > `, + standalone: true, + imports: [LottieDirective], }) class MockComponent { options: AnimationOptions = { @@ -230,8 +234,8 @@ describe('ngx-lottie', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [LottieModule.forRoot({ player: playerFactory })], - declarations: [MockComponent], + imports: [MockComponent], + providers: [provideLottieOptions({ player: playerFactory })], }); }); @@ -278,6 +282,8 @@ describe('ngx-lottie', () => { (animationCreated)="animationCreated($event)" > `, + standalone: true, + imports: [LottieComponent], }) class MockComponent { options: AnimationOptions = { @@ -301,8 +307,8 @@ describe('ngx-lottie', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [LottieModule.forRoot({ player: () => import('lottie-web') })], - declarations: [MockComponent], + imports: [MockComponent], + providers: [provideLottieOptions({ player: () => import('lottie-web') })], }); }); diff --git a/package.json b/package.json index 3d61a54..ab2cdbb 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "husky": "~6.0.0", "jest": "29.4.3", "jest-environment-jsdom": "29.4.3", - "jest-preset-angular": "13.1.6", + "jest-preset-angular": "14.0.0", "lint-staged": "^10.2.11", "lottie-web": "^5.9.2", "ng-packagr": "17.0.3", diff --git a/yarn.lock b/yarn.lock index 73867f5..a99e582 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7538,10 +7538,10 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-preset-angular@13.1.6: - version "13.1.6" - resolved "https://registry.yarnpkg.com/jest-preset-angular/-/jest-preset-angular-13.1.6.tgz#c32af7b3071a917c7fc75534f5aa00db1f36700a" - integrity sha512-0pXSm6168Qn+qKp7DpzYoaIp0uyMHdQaWYVp8jlw7Mh+NEBtrBjKqts3kLeBHgAhGMQArp07S2IxZ6eCr8fc7Q== +jest-preset-angular@14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/jest-preset-angular/-/jest-preset-angular-14.0.0.tgz#5062a4ec6992388ae17b3df60d4539715d2c6f8b" + integrity sha512-gXGgzuGbpw3MRBMe/NGCu3r2E//GKmhtFveo0XUIXMvQ3je0vcOtK+WYjxtxFTTh2xFgrA/loY5BxBcKia/GaA== dependencies: bs-logger "^0.2.6" esbuild-wasm ">=0.13.8"