Skip to content

feat: allow tracking of dom changes and preventing events from firing during one #133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 104 additions & 11 deletions packages/angular/src/lib/nativescript-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Inject, Injectable, NgZone, Optional, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ViewEncapsulation } from '@angular/core';
import { Inject, Injectable, Injector, NgZone, Optional, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ViewEncapsulation, inject, runInInjectionContext } from '@angular/core';
import { addTaggedAdditionalCSS, Application, ContentView, Device, getViewById, Observable, profile, Utils, View } from '@nativescript/core';
import { getViewClass, isKnownView } from './element-registry';
import { getFirstNativeLikeView, NgView, TextNode } from './views';

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

Expand Down Expand Up @@ -34,17 +34,68 @@ function inRootZone() {
};
}

@Injectable({
providedIn: 'root',
})
export class NativeScriptRendererHelperService {
private _executingDomChanges = 0;
get executingDomChanges() {
return this._executingDomChanges;
}
get isExecutingDomChanges() {
return this._executingDomChanges > 0;
}
beginDomChanges() {
this._executingDomChanges++;
}
endDomChanges() {
this._executingDomChanges--;
}
executeDomChange<T>(fn: () => T): T {
try {
this.beginDomChanges();
return fn();
} finally {
this.endDomChanges();
}
}
}

function modifiesDom() {
return function (
target: {
_rendererHelper: NativeScriptRendererHelperService;
},
key: string | symbol,
descriptor: PropertyDescriptor,
) {
const childFunction = descriptor.value;
descriptor.value = function (...args: unknown[]) {
const fn = childFunction.bind(this);
return this._rendererHelper.executeDomChange(() => fn(...args));
};
return descriptor;
};
}

export class NativeScriptRendererFactory implements RendererFactory2 {
private componentRenderers = new Map<string, Renderer2>();
private defaultRenderer: Renderer2;
// backwards compatibility with RadListView
private rootView = inject(APP_ROOT_VIEW);
private namespaceFilters = inject(NAMESPACE_FILTERS);
private rootModuleID = inject(NATIVESCRIPT_ROOT_MODULE_ID);
private reuseViews = inject(ENABLE_REUSABE_VIEWS, {
optional: true,
});
private injector = inject(Injector);
private viewUtil = new ViewUtil(this.namespaceFilters, this.reuseViews);

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) {
constructor() {
if (typeof this.reuseViews !== 'boolean') {
this.reuseViews = false; // default to false
}
this.defaultRenderer = new NativeScriptRenderer(rootView, namespaceFilters, this.reuseViews);
this.defaultRenderer = new NativeScriptRenderer(this.rootView);
}
createRenderer(hostElement: any, type: RendererType2): Renderer2 {
if (NativeScriptDebug.enabled) {
Expand Down Expand Up @@ -77,7 +128,9 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
type.styles.map((s) => s.toString()).forEach((v) => addStyleToCss(v, this.rootModuleID));
renderer = this.defaultRenderer;
} else {
renderer = new EmulatedRenderer(type, hostElement, this.namespaceFilters, this.rootModuleID, this.reuseViews);
runInInjectionContext(this.injector, () => {
renderer = new EmulatedRenderer(type, hostElement);
});
(<EmulatedRenderer>renderer).applyToHost(hostElement);
}

Expand Down Expand Up @@ -126,9 +179,23 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
}

class NativeScriptRenderer implements Renderer2 {
private namespaceFilters = inject(NAMESPACE_FILTERS);
private reuseViews = inject(ENABLE_REUSABE_VIEWS, {
optional: true,
});
private viewUtil = new ViewUtil(this.namespaceFilters, this.reuseViews);
_rendererHelper = inject(NativeScriptRendererHelperService);
private specificPreventedEvents = new Set(
inject(PREVENT_SPECIFIC_EVENTS_DURING_CD, {
optional: true,
}) ?? [],
);
private preventChangeEvents =
inject(PREVENT_SPECIFIC_EVENTS_DURING_CD, {
optional: true,
}) ?? false;

constructor(private rootView: View, private namespaceFilters?: NamespaceFilter[], private reuseViews?: boolean) {}
constructor(private rootView: View) {}
get data(): { [key: string]: any } {
throw new Error('Method not implemented.');
}
Expand All @@ -138,6 +205,7 @@ class NativeScriptRenderer implements Renderer2 {
}
}
@inRootZone()
@modifiesDom()
createElement(name: string, namespace?: string) {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.createElement: ${name}`);
Expand All @@ -154,13 +222,15 @@ class NativeScriptRenderer implements Renderer2 {
return view;
}
@inRootZone()
@modifiesDom()
createComment(value: string) {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.createComment ${value}`);
}
return this.viewUtil.createComment(value);
}
@inRootZone()
@modifiesDom()
createText(value: string) {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.createText ${value}`);
Expand All @@ -177,20 +247,23 @@ class NativeScriptRenderer implements Renderer2 {
}
});
@inRootZone()
@modifiesDom()
appendChild(parent: View, newChild: View): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.appendChild child: ${newChild} parent: ${parent}`);
}
this.viewUtil.appendChild(parent, newChild);
}
@inRootZone()
@modifiesDom()
insertBefore(parent: any, newChild: any, refChild: any): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.insertBefore child: ${newChild} ` + `parent: ${parent} refChild: ${refChild}`);
}
this.viewUtil.insertBefore(parent, newChild, refChild);
}
@inRootZone()
@modifiesDom()
removeChild(parent: any, oldChild: any, isHostElement?: boolean): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.removeChild child: ${oldChild} parent: ${parent}`);
Expand Down Expand Up @@ -231,6 +304,7 @@ class NativeScriptRenderer implements Renderer2 {
return node.nextSibling;
}
@inRootZone()
@modifiesDom()
setAttribute(el: any, name: string, value: string, namespace?: string): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setAttribute ${namespace ? namespace + ':' : ''}${el}.${name} = ${value}`);
Expand All @@ -243,40 +317,47 @@ class NativeScriptRenderer implements Renderer2 {
}
}
@inRootZone()
@modifiesDom()
addClass(el: any, name: string): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.addClass ${name}`);
}
this.viewUtil.addClass(el, name);
}
@inRootZone()
@modifiesDom()
removeClass(el: any, name: string): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.removeClass ${name}`);
}
this.viewUtil.removeClass(el, name);
}
@inRootZone()
@modifiesDom()
setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setStyle: ${el}, ${style} = ${value}`);
}
this.viewUtil.setStyle(el, style, value);
}
@inRootZone()
@modifiesDom()
removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog('NativeScriptRenderer.removeStyle: ${styleName}');
}
this.viewUtil.removeStyle(el, style);
}
@inRootZone()
@modifiesDom()
setProperty(el: any, name: string, value: any): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setProperty ${el}.${name} = ${value}`);
}
this.viewUtil.setProperty(el, name, value);
}
@inRootZone()
@modifiesDom()
setValue(node: any, value: string): void {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setValue renderNode: ${node}, value: ${value}`);
Expand All @@ -291,17 +372,26 @@ class NativeScriptRenderer implements Renderer2 {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.listen: ${eventName}`);
}
target.on(eventName, callback);
let modifiedCallback = callback;
if ((this.preventChangeEvents && eventName.endsWith('Change')) || this.specificPreventedEvents.has(eventName)) {
modifiedCallback = (...args) => {
if (this._rendererHelper.isExecutingDomChanges) {
return;
}
return callback(...args);
};
}
target.on(eventName, modifiedCallback);
if (eventName === View.loadedEvent && target.isLoaded) {
// we must create a new obervable here to ensure that the event goes through whatever zone patches are applied
const obs = new Observable();
obs.once(eventName, callback);
obs.once(eventName, modifiedCallback);
obs.notify({
eventName,
object: target,
});
}
return () => target.off(eventName, callback);
return () => target.off(eventName, modifiedCallback);
}
}

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

constructor(component: RendererType2, rootView: View, namespaceFilters: NamespaceFilter[], private rootModuleId: string | number, reuseViews: boolean) {
super(rootView, namespaceFilters, reuseViews);
constructor(component: RendererType2, rootView: View) {
super(rootView);

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

@profile
@inRootZone()
@modifiesDom()
private addStyles(styles: (string | any[])[], componentId: string) {
styles
.map((s) => s.toString())
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/src/lib/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export * from './detached-loader-utils';
export { AppLaunchView, AppRunOptions, NgModuleEvent, NgModuleReason, disableRootViewHanding, onAfterLivesync, onBeforeLivesync, postAngularBootstrap$, preAngularDisposal$, runNativeScriptAngularApp, ApplicationConfig, bootstrapApplication } from './application';
export * from './element-registry';
export * from './nativescript-xhr-factory';
export { EmulatedRenderer, NativeScriptRendererFactory, COMPONENT_VARIABLE as ɵCOMPONENT_VARIABLE, CONTENT_ATTR as ɵCONTENT_ATTR, HOST_ATTR as ɵHOST_ATTR } from './nativescript-renderer';
export { EmulatedRenderer, NativeScriptRendererFactory, COMPONENT_VARIABLE as ɵCOMPONENT_VARIABLE, CONTENT_ATTR as ɵCONTENT_ATTR, HOST_ATTR as ɵHOST_ATTR, NativeScriptRendererHelperService } from './nativescript-renderer';
export * from './utils';
export * from './forms';
export * from './animations';
Expand Down
3 changes: 3 additions & 0 deletions packages/angular/src/lib/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ export const PAGE_FACTORY = new InjectionToken<PageFactory>('NativeScriptPageFac
export const defaultPageFactory: PageFactory = function (_opts: PageFactoryOptions) {
return new Page();
};

export const PREVENT_CHANGE_EVENTS_DURING_CD = new InjectionToken<boolean>('NativeScriptPreventChangeEventsDuringCd');
export const PREVENT_SPECIFIC_EVENTS_DURING_CD = new InjectionToken<string[]>('NativeScriptPreventSpecificEventsDuringCd');
37 changes: 37 additions & 0 deletions packages/angular/src/lib/utils/native-element-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Type, reflectComponentType } from '@angular/core';
import { GridLayout, View } from '@nativescript/core';
import { registerElement } from '../element-registry/registry';

function createClass<T extends { new (...args: any[]): any }>(className: string, extendsClassName: T) {
return { [className]: class extends extendsClassName {} }[className];
}

export function NativeElementHost(
fn: () => typeof View,
{
forcedSelector,
createProxyClass = true,
}: {
forcedSelector?: string;
createProxyClass?: boolean;
} = {},
) {
return function <T extends Type<any>>(v: T) {
((forcedSelector || reflectComponentType(v)?.selector)?.split(',') || [])
.map((v) => v.trim())
.filter((v) => !v.includes('['))
.forEach((selector) => {
if (createProxyClass) {
let cachedCls: typeof View;
registerElement(selector, () => {
if (!cachedCls) {
cachedCls = createClass(selector, fn() as any);
}
return cachedCls;
});
} else {
registerElement(selector, fn);
}
});
};
}
Loading