Skip to content

Commit 4ebddbb

Browse files
committed
fix: clean up change detection runs
1 parent c938839 commit 4ebddbb

File tree

14 files changed

+104
-117
lines changed

14 files changed

+104
-117
lines changed

libs/angular-three/src/lib/canvas.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { NgtRxStore } from './stores/rx-store';
2626
import { NgtStore, rootStateMap } from './stores/store';
2727
import type { NgtAnyRecord, NgtCanvasInputs, NgtDomEvent, NgtDpr, NgtState } from './types';
2828
import { is } from './utils/is';
29+
import { safeDetectChanges } from './utils/safe-detect-changes';
2930
import { createPointerEvents } from './web/events';
3031

3132
@Component({
@@ -162,7 +163,7 @@ export class NgtCanvas extends NgtRxStore<NgtCanvasInputs> implements OnInit, On
162163
this.store.set({
163164
onPointerMissed: (event: MouseEvent) => {
164165
this.pointerMissed.emit(event);
165-
this.cdr.detectChanges();
166+
safeDetectChanges(this.cdr);
166167
},
167168
});
168169
}
@@ -210,24 +211,20 @@ export class NgtCanvas extends NgtRxStore<NgtCanvasInputs> implements OnInit, On
210211
// emit created event if observed
211212
if (this.created.observed) {
212213
this.created.emit(this.store.get());
213-
this.cdr.detectChanges();
214214
}
215215

216216
// render
217217
if (this.glRef) this.glRef.destroy();
218218

219219
requestAnimationFrame(() => {
220+
const { store, cdr: changeDetectorRef, compoundPrefixes } = this;
220221
this.glEnvInjector = createEnvironmentInjector(
221-
[
222-
provideNgtRenderer({
223-
store: this.store,
224-
changeDetectorRef: this.cdr,
225-
compoundPrefixes: this.compoundPrefixes,
226-
}),
227-
],
222+
[provideNgtRenderer({ store, changeDetectorRef, compoundPrefixes })],
228223
this.envInjector
229224
);
230225
this.glRef = this.glAnchor.createComponent(this.sceneGraph, { environmentInjector: this.glEnvInjector });
226+
// detach the Scene graph from Change Detection mechanism
227+
// everything from this point forward will trigger CD manually with detectChanges
231228
this.glRef.changeDetectorRef.detach();
232229
this.setSceneGraphInputs();
233230

@@ -249,14 +246,14 @@ export class NgtCanvas extends NgtRxStore<NgtCanvasInputs> implements OnInit, On
249246
const originalDetectChanges = this.cdr.detectChanges.bind(this.cdr);
250247
this.cdr.detectChanges = () => {
251248
originalDetectChanges();
252-
this.glRef?.changeDetectorRef.detectChanges();
249+
safeDetectChanges(this.glRef?.changeDetectorRef);
253250
};
254251
}
255252

256253
private setSceneGraphInputs() {
257254
for (const key of Object.keys(this.sceneGraphInputs)) {
258-
this.glRef?.setInput(key, this.sceneGraphInputs[key]);
255+
this.glRef!.setInput(key, this.sceneGraphInputs[key]);
259256
}
260-
this.cdr.detectChanges();
257+
safeDetectChanges(this.cdr);
261258
}
262259
}

libs/angular-three/src/lib/di/ref.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export function injectNgtRef<T>(initialValue: NgtInjectedRef<T> | (T | null) = n
5353
});
5454
};
5555

56+
const useCDR = (cdr: ChangeDetectorRef) => void cdRefs.push(cdr);
57+
5658
const $ = obs$.pipe(
5759
filter((value, index) => index > 0 || value != null),
5860
takeUntil(destroy$)
@@ -83,6 +85,7 @@ export function injectNgtRef<T>(initialValue: NgtInjectedRef<T> | (T | null) = n
8385
takeUntil(destroy$)
8486
);
8587

88+
// here, we override nativeElement to add more functionalities to nativeElement
8689
Object.defineProperty(ref, 'nativeElement', {
8790
set: (newVal: T) => {
8891
if (ref.nativeElement !== newVal) {
@@ -110,5 +113,5 @@ export function injectNgtRef<T>(initialValue: NgtInjectedRef<T> | (T | null) = n
110113
get: () => ref$.value,
111114
});
112115

113-
return Object.assign(ref, { subscribe, $, children$, useCDR: (cdr: ChangeDetectorRef) => void cdRefs.push(cdr) });
116+
return Object.assign(ref, { subscribe, $, children$, useCDR });
114117
}

libs/angular-three/src/lib/di/run-in-context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { EnvironmentInjector, inject, Injector } from '@angular/core';
22

3+
/**
4+
* Please use this sparringly and know what you're doing.
5+
*
6+
* Create a runInContext function that has access to the NodeInjector as well
7+
*/
38
export function createRunInContext() {
49
const nodeInjector = inject(Injector);
510
const envInjector = inject(EnvironmentInjector);

libs/angular-three/src/lib/directives/common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Directive, EmbeddedViewRef, inject, TemplateRef, ViewContainerRef } from '@angular/core';
2+
import { safeDetectChanges } from '../utils/safe-detect-changes';
23

34
@Directive()
45
export abstract class NgtCommonDirective {
@@ -25,7 +26,7 @@ export abstract class NgtCommonDirective {
2526
this.view.destroy();
2627
}
2728
this.view = this.vcr.createEmbeddedView(this.template);
28-
this.view.detectChanges();
29+
safeDetectChanges(this.view);
2930
}
3031
}
3132
}

libs/angular-three/src/lib/loader.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
1818
import type { NgtBranchingReturn, NgtLoaderExtensions, NgtLoaderResult, NgtObjectMap } from './types';
1919
import { makeObjectGraph } from './utils/make';
20+
import { safeDetectChanges } from './utils/safe-detect-changes';
2021

2122
interface NgtLoader {
2223
<TReturnType, TUrl extends string | string[] | Record<string, string>>(
@@ -46,15 +47,13 @@ export type NgtLoaderResults<
4647

4748
const cached = new Map<string, Observable<any>>();
4849

49-
function injectLoader<TReturnType, TUrl extends string | string[] | Record<string, string>>(
50+
function load<TReturnType, TUrl extends string | string[] | Record<string, string>>(
5051
loaderConstructorFactory: (inputs: TUrl) => new (...args: any[]) => NgtLoaderResult<TReturnType>,
5152
input: TUrl | Observable<TUrl>,
5253
extensions?: NgtLoaderExtensions,
5354
onProgress?: (event: ProgressEvent) => void
54-
): Observable<NgtLoaderResults<TUrl, NgtBranchingReturn<TReturnType, GLTF, GLTF & NgtObjectMap>>> {
55+
) {
5556
const urls$ = isObservable(input) ? input : of(input);
56-
const cdr = inject(ChangeDetectorRef);
57-
5857
return urls$.pipe(
5958
map((inputs) => {
6059
const loaderConstructor = loaderConstructorFactory(inputs);
@@ -83,7 +82,19 @@ function injectLoader<TReturnType, TUrl extends string | string[] | Record<strin
8382
}),
8483
inputs,
8584
] as [Array<Observable<any>>, TUrl | TUrl[]];
86-
}),
85+
})
86+
);
87+
}
88+
89+
function injectLoader<TReturnType, TUrl extends string | string[] | Record<string, string>>(
90+
loaderConstructorFactory: (inputs: TUrl) => new (...args: any[]) => NgtLoaderResult<TReturnType>,
91+
input: TUrl | Observable<TUrl>,
92+
extensions?: NgtLoaderExtensions,
93+
onProgress?: (event: ProgressEvent) => void
94+
): Observable<NgtLoaderResults<TUrl, NgtBranchingReturn<TReturnType, GLTF, GLTF & NgtObjectMap>>> {
95+
const cdr = inject(ChangeDetectorRef);
96+
97+
return load(loaderConstructorFactory, input, extensions, onProgress).pipe(
8798
switchMap(([observables$, inputs]) => {
8899
return forkJoin(observables$).pipe(
89100
map((results) => {
@@ -96,7 +107,7 @@ function injectLoader<TReturnType, TUrl extends string | string[] | Record<strin
96107
}, {} as { [key in keyof TUrl]: NgtBranchingReturn<TReturnType, GLTF, GLTF & NgtObjectMap> });
97108
}),
98109
tap(() => {
99-
requestAnimationFrame(() => cdr.detectChanges());
110+
requestAnimationFrame(() => void safeDetectChanges(cdr));
100111
})
101112
);
102113
})
@@ -108,7 +119,7 @@ function injectLoader<TReturnType, TUrl extends string | string[] | Record<strin
108119
};
109120

110121
(injectLoader as NgtLoader).preLoad = (loaderConstructorFactory, inputs, extensions) => {
111-
injectLoader(loaderConstructorFactory, inputs, extensions).pipe(take(1)).subscribe();
122+
load(loaderConstructorFactory, inputs, extensions).pipe(take(1)).subscribe();
112123
};
113124

114125
export const injectNgtLoader = injectLoader as NgtLoader;

libs/angular-three/src/lib/pipes/push.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { ChangeDetectorRef, inject, OnDestroy, Pipe, PipeTransform } from '@angular/core';
1+
import { ChangeDetectorRef, EnvironmentInjector, inject, OnDestroy, Pipe, PipeTransform } from '@angular/core';
22
import { isObservable, ObservableInput, Subscription } from 'rxjs';
3+
import { safeDetectChanges } from '../utils/safe-detect-changes';
34

45
function isPromise(value: unknown): value is Promise<unknown> {
56
return (
@@ -12,45 +13,36 @@ function isPromise(value: unknown): value is Promise<unknown> {
1213
export class NgtPush<T> implements PipeTransform, OnDestroy {
1314
private readonly cdr = inject(ChangeDetectorRef);
1415
private readonly parentCdr = inject(ChangeDetectorRef, { skipSelf: true, optional: true });
16+
private readonly envCdr = inject(EnvironmentInjector).get(ChangeDetectorRef, null);
1517

1618
private sub?: Subscription;
1719
private obj?: ObservableInput<T>;
1820
private latestValue?: T;
1921

2022
transform(value: ObservableInput<T>, defaultValue: T = null!): T {
21-
if (this.obj === value) {
22-
return this.latestValue as T;
23-
}
23+
if (this.obj === value) return this.latestValue as T;
24+
2425
this.obj = value;
2526
this.latestValue = defaultValue;
2627

27-
if (this.sub) {
28-
this.sub.unsubscribe();
29-
}
28+
if (this.sub) this.sub.unsubscribe();
3029

31-
if (isObservable(this.obj)) {
32-
this.sub = this.obj.subscribe(this.updateValue.bind(this));
33-
} else if (isPromise(this.obj)) {
34-
(this.obj as Promise<T>).then(this.updateValue.bind(this));
35-
} else {
36-
throw new Error(`[NGT] Invalid value passed to ngtPush pipe`);
37-
}
30+
if (isObservable(this.obj)) this.sub = this.obj.subscribe(this.updateValue.bind(this));
31+
else if (isPromise(this.obj)) (this.obj as Promise<T>).then(this.updateValue.bind(this));
32+
else throw new Error(`[NGT] Invalid value passed to ngtPush pipe`);
3833

3934
return this.latestValue as T;
4035
}
4136

4237
private updateValue(val: T) {
4338
this.latestValue = val;
44-
this.cdr.detectChanges();
45-
if (this.parentCdr) {
46-
this.parentCdr.detectChanges();
47-
}
39+
safeDetectChanges(this.cdr);
40+
safeDetectChanges(this.parentCdr);
41+
safeDetectChanges(this.envCdr);
4842
}
4943

5044
ngOnDestroy() {
51-
if (this.sub) {
52-
this.sub.unsubscribe();
53-
}
45+
if (this.sub) this.sub.unsubscribe();
5446
this.latestValue = undefined;
5547
this.obj = undefined;
5648
}

libs/angular-three/src/lib/portal.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,11 @@ export class NgtPortal extends NgtRxStore<NgtPortalInputs> implements OnInit, On
197197
});
198198

199199
this.hold(this.parentStore.select(), (previous) =>
200-
this.portalStore.set((state) => this.#inject(previous, state))
200+
this.portalStore.set((state) => this.inject(previous, state))
201201
);
202202

203203
requestAnimationFrame(() => {
204-
this.portalStore.set((injectState) => this.#inject(this.parentStore.get(), injectState));
204+
this.portalStore.set((injectState) => this.inject(this.parentStore.get(), injectState));
205205
});
206206
this.portalContentView = this.portalContentAnchor.createEmbeddedView(this.portalContentTemplate);
207207
this.portalContentView.detectChanges();
@@ -222,7 +222,7 @@ export class NgtPortal extends NgtRxStore<NgtPortalInputs> implements OnInit, On
222222
super.ngOnDestroy();
223223
}
224224

225-
#inject(rootState: NgtState, injectState: NgtState) {
225+
private inject(rootState: NgtState, injectState: NgtState) {
226226
const intersect: Partial<NgtState> = { ...rootState };
227227

228228
Object.keys(intersect).forEach((key) => {

libs/angular-three/src/lib/renderer/renderer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { NgtStore } from '../stores/store';
1414
import { NgtAnyRecord } from '../types';
1515
import { getLocalState, prepare } from '../utils/instance';
1616
import { is } from '../utils/is';
17+
import { safeDetectChanges } from '../utils/safe-detect-changes';
1718
import { NGT_COMPOUND_PREFIXES } from './di';
1819
import { NgtRendererClassId } from './enums';
1920
import { NgtRendererNode, NgtRendererState, NgtRendererStore } from './state';
@@ -342,8 +343,8 @@ export class NgtRenderer implements Renderer2 {
342343
// if target is DOM node, then we pass that to delegate Renderer
343344
const callbackWithCdr = (event: any) => {
344345
const value = callback(event);
345-
if (targetCdr) targetCdr.detectChanges();
346-
this.store.rootCdr.detectChanges();
346+
safeDetectChanges(targetCdr);
347+
safeDetectChanges(this.store.rootCdr);
347348
return value;
348349
};
349350
if (this.store.isDOM(target)) {

libs/angular-three/src/lib/renderer/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { NgtEventHandlers, NgtInstanceNode } from '../types';
44
import { attach, detach } from '../utils/attach';
55
import { getLocalState, invalidateInstance } from '../utils/instance';
66
import { is } from '../utils/is';
7+
import { safeDetectChanges } from '../utils/safe-detect-changes';
78
import { supportedEvents } from '../web/events';
89
import { NgtRendererClassId } from './enums';
910

@@ -197,8 +198,8 @@ export function processThreeEvent(
197198
export function eventToHandler(callback: (event: any) => void, cdr: ChangeDetectorRef, targetCdr?: ChangeDetectorRef) {
198199
return (event: Parameters<Exclude<NgtEventHandlers[(typeof supportedEvents)[number]], undefined>>[0]) => {
199200
callback(event);
200-
if (targetCdr) targetCdr.detectChanges();
201-
cdr.detectChanges();
201+
safeDetectChanges(targetCdr);
202+
safeDetectChanges(cdr);
202203
};
203204
}
204205

libs/angular-three/src/lib/routed-scene.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
22
import { ActivationEnd, Router, RouterOutlet } from '@angular/router';
33
import { filter, takeUntil } from 'rxjs';
44
import { injectNgtDestroy } from './di/destroy';
5+
import { safeDetectChanges } from './utils/safe-detect-changes';
56

67
@Component({
78
standalone: true,
@@ -19,6 +20,6 @@ export class NgtRoutedScene {
1920
filter((event) => event instanceof ActivationEnd),
2021
takeUntil(destroy$)
2122
)
22-
.subscribe(cdr.detectChanges.bind(cdr));
23+
.subscribe(() => safeDetectChanges(cdr));
2324
}
2425
}

0 commit comments

Comments
 (0)