Skip to content

Commit a009096

Browse files
authored
feat: allow tracking of dom changes and preventing events from firing during one (#133)
1 parent 68525b0 commit a009096

File tree

4 files changed

+145
-12
lines changed

4 files changed

+145
-12
lines changed

Diff for: packages/angular/src/lib/nativescript-renderer.ts

+104-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Inject, Injectable, NgZone, Optional, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ViewEncapsulation } from '@angular/core';
1+
import { Inject, Injectable, Injector, NgZone, Optional, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ViewEncapsulation, inject, runInInjectionContext } from '@angular/core';
22
import { addTaggedAdditionalCSS, Application, ContentView, Device, getViewById, Observable, profile, Utils, View } from '@nativescript/core';
33
import { getViewClass, isKnownView } from './element-registry';
44
import { getFirstNativeLikeView, NgView, TextNode } from './views';
55

66
import { NamespaceFilter, NAMESPACE_FILTERS } from './property-filter';
7-
import { APP_ROOT_VIEW, ENABLE_REUSABE_VIEWS, NATIVESCRIPT_ROOT_MODULE_ID } from './tokens';
7+
import { APP_ROOT_VIEW, ENABLE_REUSABE_VIEWS, NATIVESCRIPT_ROOT_MODULE_ID, PREVENT_SPECIFIC_EVENTS_DURING_CD } from './tokens';
88
import { NativeScriptDebug } from './trace';
99
import { ViewUtil } from './view-util';
1010

@@ -34,17 +34,68 @@ function inRootZone() {
3434
};
3535
}
3636

37+
@Injectable({
38+
providedIn: 'root',
39+
})
40+
export class NativeScriptRendererHelperService {
41+
private _executingDomChanges = 0;
42+
get executingDomChanges() {
43+
return this._executingDomChanges;
44+
}
45+
get isExecutingDomChanges() {
46+
return this._executingDomChanges > 0;
47+
}
48+
beginDomChanges() {
49+
this._executingDomChanges++;
50+
}
51+
endDomChanges() {
52+
this._executingDomChanges--;
53+
}
54+
executeDomChange<T>(fn: () => T): T {
55+
try {
56+
this.beginDomChanges();
57+
return fn();
58+
} finally {
59+
this.endDomChanges();
60+
}
61+
}
62+
}
63+
64+
function modifiesDom() {
65+
return function (
66+
target: {
67+
_rendererHelper: NativeScriptRendererHelperService;
68+
},
69+
key: string | symbol,
70+
descriptor: PropertyDescriptor,
71+
) {
72+
const childFunction = descriptor.value;
73+
descriptor.value = function (...args: unknown[]) {
74+
const fn = childFunction.bind(this);
75+
return this._rendererHelper.executeDomChange(() => fn(...args));
76+
};
77+
return descriptor;
78+
};
79+
}
80+
3781
export class NativeScriptRendererFactory implements RendererFactory2 {
3882
private componentRenderers = new Map<string, Renderer2>();
3983
private defaultRenderer: Renderer2;
4084
// backwards compatibility with RadListView
85+
private rootView = inject(APP_ROOT_VIEW);
86+
private namespaceFilters = inject(NAMESPACE_FILTERS);
87+
private rootModuleID = inject(NATIVESCRIPT_ROOT_MODULE_ID);
88+
private reuseViews = inject(ENABLE_REUSABE_VIEWS, {
89+
optional: true,
90+
});
91+
private injector = inject(Injector);
4192
private viewUtil = new ViewUtil(this.namespaceFilters, this.reuseViews);
4293

43-
constructor(@Inject(APP_ROOT_VIEW) private rootView: View, @Inject(NAMESPACE_FILTERS) private namespaceFilters: NamespaceFilter[], @Inject(NATIVESCRIPT_ROOT_MODULE_ID) private rootModuleID: string | number, @Optional() @Inject(ENABLE_REUSABE_VIEWS) private reuseViews) {
94+
constructor() {
4495
if (typeof this.reuseViews !== 'boolean') {
4596
this.reuseViews = false; // default to false
4697
}
47-
this.defaultRenderer = new NativeScriptRenderer(rootView, namespaceFilters, this.reuseViews);
98+
this.defaultRenderer = new NativeScriptRenderer(this.rootView);
4899
}
49100
createRenderer(hostElement: any, type: RendererType2): Renderer2 {
50101
if (NativeScriptDebug.enabled) {
@@ -77,7 +128,9 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
77128
type.styles.map((s) => s.toString()).forEach((v) => addStyleToCss(v, this.rootModuleID));
78129
renderer = this.defaultRenderer;
79130
} else {
80-
renderer = new EmulatedRenderer(type, hostElement, this.namespaceFilters, this.rootModuleID, this.reuseViews);
131+
runInInjectionContext(this.injector, () => {
132+
renderer = new EmulatedRenderer(type, hostElement);
133+
});
81134
(<EmulatedRenderer>renderer).applyToHost(hostElement);
82135
}
83136

@@ -126,9 +179,23 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
126179
}
127180

128181
class NativeScriptRenderer implements Renderer2 {
182+
private namespaceFilters = inject(NAMESPACE_FILTERS);
183+
private reuseViews = inject(ENABLE_REUSABE_VIEWS, {
184+
optional: true,
185+
});
129186
private viewUtil = new ViewUtil(this.namespaceFilters, this.reuseViews);
187+
_rendererHelper = inject(NativeScriptRendererHelperService);
188+
private specificPreventedEvents = new Set(
189+
inject(PREVENT_SPECIFIC_EVENTS_DURING_CD, {
190+
optional: true,
191+
}) ?? [],
192+
);
193+
private preventChangeEvents =
194+
inject(PREVENT_SPECIFIC_EVENTS_DURING_CD, {
195+
optional: true,
196+
}) ?? false;
130197

131-
constructor(private rootView: View, private namespaceFilters?: NamespaceFilter[], private reuseViews?: boolean) {}
198+
constructor(private rootView: View) {}
132199
get data(): { [key: string]: any } {
133200
throw new Error('Method not implemented.');
134201
}
@@ -138,6 +205,7 @@ class NativeScriptRenderer implements Renderer2 {
138205
}
139206
}
140207
@inRootZone()
208+
@modifiesDom()
141209
createElement(name: string, namespace?: string) {
142210
if (NativeScriptDebug.enabled) {
143211
NativeScriptDebug.rendererLog(`NativeScriptRenderer.createElement: ${name}`);
@@ -154,13 +222,15 @@ class NativeScriptRenderer implements Renderer2 {
154222
return view;
155223
}
156224
@inRootZone()
225+
@modifiesDom()
157226
createComment(value: string) {
158227
if (NativeScriptDebug.enabled) {
159228
NativeScriptDebug.rendererLog(`NativeScriptRenderer.createComment ${value}`);
160229
}
161230
return this.viewUtil.createComment(value);
162231
}
163232
@inRootZone()
233+
@modifiesDom()
164234
createText(value: string) {
165235
if (NativeScriptDebug.enabled) {
166236
NativeScriptDebug.rendererLog(`NativeScriptRenderer.createText ${value}`);
@@ -177,20 +247,23 @@ class NativeScriptRenderer implements Renderer2 {
177247
}
178248
});
179249
@inRootZone()
250+
@modifiesDom()
180251
appendChild(parent: View, newChild: View): void {
181252
if (NativeScriptDebug.enabled) {
182253
NativeScriptDebug.rendererLog(`NativeScriptRenderer.appendChild child: ${newChild} parent: ${parent}`);
183254
}
184255
this.viewUtil.appendChild(parent, newChild);
185256
}
186257
@inRootZone()
258+
@modifiesDom()
187259
insertBefore(parent: any, newChild: any, refChild: any): void {
188260
if (NativeScriptDebug.enabled) {
189261
NativeScriptDebug.rendererLog(`NativeScriptRenderer.insertBefore child: ${newChild} ` + `parent: ${parent} refChild: ${refChild}`);
190262
}
191263
this.viewUtil.insertBefore(parent, newChild, refChild);
192264
}
193265
@inRootZone()
266+
@modifiesDom()
194267
removeChild(parent: any, oldChild: any, isHostElement?: boolean): void {
195268
if (NativeScriptDebug.enabled) {
196269
NativeScriptDebug.rendererLog(`NativeScriptRenderer.removeChild child: ${oldChild} parent: ${parent}`);
@@ -231,6 +304,7 @@ class NativeScriptRenderer implements Renderer2 {
231304
return node.nextSibling;
232305
}
233306
@inRootZone()
307+
@modifiesDom()
234308
setAttribute(el: any, name: string, value: string, namespace?: string): void {
235309
if (NativeScriptDebug.enabled) {
236310
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setAttribute ${namespace ? namespace + ':' : ''}${el}.${name} = ${value}`);
@@ -243,40 +317,47 @@ class NativeScriptRenderer implements Renderer2 {
243317
}
244318
}
245319
@inRootZone()
320+
@modifiesDom()
246321
addClass(el: any, name: string): void {
247322
if (NativeScriptDebug.enabled) {
248323
NativeScriptDebug.rendererLog(`NativeScriptRenderer.addClass ${name}`);
249324
}
250325
this.viewUtil.addClass(el, name);
251326
}
252327
@inRootZone()
328+
@modifiesDom()
253329
removeClass(el: any, name: string): void {
254330
if (NativeScriptDebug.enabled) {
255331
NativeScriptDebug.rendererLog(`NativeScriptRenderer.removeClass ${name}`);
256332
}
257333
this.viewUtil.removeClass(el, name);
258334
}
259335
@inRootZone()
336+
@modifiesDom()
260337
setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void {
261338
if (NativeScriptDebug.enabled) {
262339
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setStyle: ${el}, ${style} = ${value}`);
263340
}
264341
this.viewUtil.setStyle(el, style, value);
265342
}
266343
@inRootZone()
344+
@modifiesDom()
267345
removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void {
268346
if (NativeScriptDebug.enabled) {
269347
NativeScriptDebug.rendererLog('NativeScriptRenderer.removeStyle: ${styleName}');
270348
}
271349
this.viewUtil.removeStyle(el, style);
272350
}
273351
@inRootZone()
352+
@modifiesDom()
274353
setProperty(el: any, name: string, value: any): void {
275354
if (NativeScriptDebug.enabled) {
276355
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setProperty ${el}.${name} = ${value}`);
277356
}
278357
this.viewUtil.setProperty(el, name, value);
279358
}
359+
@inRootZone()
360+
@modifiesDom()
280361
setValue(node: any, value: string): void {
281362
if (NativeScriptDebug.enabled) {
282363
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setValue renderNode: ${node}, value: ${value}`);
@@ -291,17 +372,26 @@ class NativeScriptRenderer implements Renderer2 {
291372
if (NativeScriptDebug.enabled) {
292373
NativeScriptDebug.rendererLog(`NativeScriptRenderer.listen: ${eventName}`);
293374
}
294-
target.on(eventName, callback);
375+
let modifiedCallback = callback;
376+
if ((this.preventChangeEvents && eventName.endsWith('Change')) || this.specificPreventedEvents.has(eventName)) {
377+
modifiedCallback = (...args) => {
378+
if (this._rendererHelper.isExecutingDomChanges) {
379+
return;
380+
}
381+
return callback(...args);
382+
};
383+
}
384+
target.on(eventName, modifiedCallback);
295385
if (eventName === View.loadedEvent && target.isLoaded) {
296386
// we must create a new obervable here to ensure that the event goes through whatever zone patches are applied
297387
const obs = new Observable();
298-
obs.once(eventName, callback);
388+
obs.once(eventName, modifiedCallback);
299389
obs.notify({
300390
eventName,
301391
object: target,
302392
});
303393
}
304-
return () => target.off(eventName, callback);
394+
return () => target.off(eventName, modifiedCallback);
305395
}
306396
}
307397

@@ -328,9 +418,10 @@ const addScopedStyleToCss = profile(`"renderer".addScopedStyleToCss`, function a
328418
export class EmulatedRenderer extends NativeScriptRenderer {
329419
private contentAttr: string;
330420
private hostAttr: string;
421+
private rootModuleId = inject(NATIVESCRIPT_ROOT_MODULE_ID);
331422

332-
constructor(component: RendererType2, rootView: View, namespaceFilters: NamespaceFilter[], private rootModuleId: string | number, reuseViews: boolean) {
333-
super(rootView, namespaceFilters, reuseViews);
423+
constructor(component: RendererType2, rootView: View) {
424+
super(rootView);
334425

335426
const componentId = component.id.replace(ATTR_SANITIZER, '_');
336427
this.contentAttr = replaceNgAttribute(CONTENT_ATTR, componentId);
@@ -357,6 +448,8 @@ export class EmulatedRenderer extends NativeScriptRenderer {
357448
}
358449

359450
@profile
451+
@inRootZone()
452+
@modifiesDom()
360453
private addStyles(styles: (string | any[])[], componentId: string) {
361454
styles
362455
.map((s) => s.toString())

Diff for: packages/angular/src/lib/public_api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export * from './detached-loader-utils';
2525
export { AppLaunchView, AppRunOptions, NgModuleEvent, NgModuleReason, disableRootViewHanding, onAfterLivesync, onBeforeLivesync, postAngularBootstrap$, preAngularDisposal$, runNativeScriptAngularApp, ApplicationConfig, bootstrapApplication } from './application';
2626
export * from './element-registry';
2727
export * from './nativescript-xhr-factory';
28-
export { EmulatedRenderer, NativeScriptRendererFactory, COMPONENT_VARIABLE as ɵCOMPONENT_VARIABLE, CONTENT_ATTR as ɵCONTENT_ATTR, HOST_ATTR as ɵHOST_ATTR } from './nativescript-renderer';
28+
export { EmulatedRenderer, NativeScriptRendererFactory, COMPONENT_VARIABLE as ɵCOMPONENT_VARIABLE, CONTENT_ATTR as ɵCONTENT_ATTR, HOST_ATTR as ɵHOST_ATTR, NativeScriptRendererHelperService } from './nativescript-renderer';
2929
export * from './utils';
3030
export * from './forms';
3131
export * from './animations';

Diff for: packages/angular/src/lib/tokens.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ export const PAGE_FACTORY = new InjectionToken<PageFactory>('NativeScriptPageFac
2222
export const defaultPageFactory: PageFactory = function (_opts: PageFactoryOptions) {
2323
return new Page();
2424
};
25+
26+
export const PREVENT_CHANGE_EVENTS_DURING_CD = new InjectionToken<boolean>('NativeScriptPreventChangeEventsDuringCd');
27+
export const PREVENT_SPECIFIC_EVENTS_DURING_CD = new InjectionToken<string[]>('NativeScriptPreventSpecificEventsDuringCd');
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Type, reflectComponentType } from '@angular/core';
2+
import { GridLayout, View } from '@nativescript/core';
3+
import { registerElement } from '../element-registry/registry';
4+
5+
function createClass<T extends { new (...args: any[]): any }>(className: string, extendsClassName: T) {
6+
return { [className]: class extends extendsClassName {} }[className];
7+
}
8+
9+
export function NativeElementHost(
10+
fn: () => typeof View,
11+
{
12+
forcedSelector,
13+
createProxyClass = true,
14+
}: {
15+
forcedSelector?: string;
16+
createProxyClass?: boolean;
17+
} = {},
18+
) {
19+
return function <T extends Type<any>>(v: T) {
20+
((forcedSelector || reflectComponentType(v)?.selector)?.split(',') || [])
21+
.map((v) => v.trim())
22+
.filter((v) => !v.includes('['))
23+
.forEach((selector) => {
24+
if (createProxyClass) {
25+
let cachedCls: typeof View;
26+
registerElement(selector, () => {
27+
if (!cachedCls) {
28+
cachedCls = createClass(selector, fn() as any);
29+
}
30+
return cachedCls;
31+
});
32+
} else {
33+
registerElement(selector, fn);
34+
}
35+
});
36+
};
37+
}

0 commit comments

Comments
 (0)