|
1 | 1 | import type tippy from 'tippy.js'; |
2 | | -import { inject, Injectable, NgZone } from '@angular/core'; |
3 | | -import { defer, from, map, type Observable, of, shareReplay } from 'rxjs'; |
| 2 | +import { inject, Injectable, NgZone, ɵisPromise as isPromise } from '@angular/core'; |
| 3 | +import { defer, map, Observable, of, tap } from 'rxjs'; |
4 | 4 | import { TIPPY_LOADER, type TippyProps } from '@ngneat/helipopper/config'; |
5 | 5 |
|
6 | | -// We need to use `isPromise` instead of checking whether |
7 | | -// `value instanceof Promise`. In zone.js patched environments, `global.Promise` |
8 | | -// is the `ZoneAwarePromise`. |
9 | | -// `import(...)` returns a native promise (not a `ZoneAwarePromise`), causing |
10 | | -// `instanceof` check to be falsy. |
11 | | -function isPromise<T>(value: any): value is Promise<T> { |
12 | | - return typeof value?.then === 'function'; |
13 | | -} |
14 | | - |
15 | 6 | @Injectable({ providedIn: 'root' }) |
16 | 7 | export class TippyFactory { |
17 | 8 | private readonly _ngZone = inject(NgZone); |
18 | 9 |
|
19 | 10 | private readonly _loader = inject(TIPPY_LOADER); |
20 | 11 |
|
21 | 12 | private _tippyImpl$: Observable<typeof tippy> | null = null; |
| 13 | + private _tippy: typeof tippy | null = null; |
22 | 14 |
|
23 | 15 | /** |
24 | 16 | * This returns an observable because the user should provide a `loader` |
25 | 17 | * function, which may return a promise if the tippy.js library is to be |
26 | 18 | * loaded asynchronously. |
27 | 19 | */ |
28 | 20 | create(target: HTMLElement, props?: Partial<TippyProps>) { |
29 | | - // We use `shareReplay` to ensure that subsequent emissions are |
30 | | - // synchronous and to avoid triggering the `defer` callback repeatedly |
31 | | - // when new subscribers arrive. |
32 | 21 | this._tippyImpl$ ||= defer(() => { |
| 22 | + if (this._tippy) return of(this._tippy); |
| 23 | + |
| 24 | + // Call the `loader` function lazily — only when a subscriber |
| 25 | + // arrives — to avoid importing `tippy.js` on the server. |
33 | 26 | const maybeTippy = this._ngZone.runOutsideAngular(() => this._loader()); |
34 | | - return isPromise(maybeTippy) |
35 | | - ? from(maybeTippy).pipe(map((tippy) => tippy.default)) |
36 | | - : of(maybeTippy); |
37 | | - }).pipe(shareReplay()); |
| 27 | + |
| 28 | + let tippy$: Observable<typeof tippy>; |
| 29 | + // We need to use `isPromise` instead of checking whether |
| 30 | + // `result instanceof Promise`. In zone.js patched environments, `global.Promise` |
| 31 | + // is the `ZoneAwarePromise`. Some APIs, which are likely not patched by zone.js |
| 32 | + // for certain reasons, might not work with `instanceof`. For instance, the dynamic |
| 33 | + // import `() => import('./chunk.js')` returns a native promise (not a `ZoneAwarePromise`), |
| 34 | + // causing this check to be falsy. |
| 35 | + if (isPromise(maybeTippy)) { |
| 36 | + // This pulls less RxJS symbols compared to using `from()` to resolve a promise value. |
| 37 | + tippy$ = new Observable((subscriber) => { |
| 38 | + maybeTippy.then((tippy) => { |
| 39 | + subscriber.next(tippy.default); |
| 40 | + subscriber.complete(); |
| 41 | + }); |
| 42 | + }); |
| 43 | + } else { |
| 44 | + tippy$ = of(maybeTippy); |
| 45 | + } |
| 46 | + |
| 47 | + return tippy$.pipe( |
| 48 | + tap((tippy) => { |
| 49 | + this._tippy = tippy; |
| 50 | + }) |
| 51 | + ); |
| 52 | + }); |
38 | 53 |
|
39 | 54 | return this._tippyImpl$.pipe( |
40 | 55 | map((tippy) => { |
|
0 commit comments