Skip to content

Instantly share code, notes, and snippets.

@NathanWalker
Last active April 30, 2024 19:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NathanWalker/15aab533750623a1139e33cb46d63b25 to your computer and use it in GitHub Desktop.
Save NathanWalker/15aab533750623a1139e33cb46d63b25 to your computer and use it in GitHub Desktop.
nsIf - Specialized NativeScript directive for Angular to optimize view show/hide with change detection under mobile constraints
import {
Directive,
ElementRef,
EmbeddedViewRef,
Input,
OnDestroy,
OnInit,
Optional,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { LayoutBase, View } from '@nativescript/core';
import { Subscription, fromEventPattern } from 'rxjs';
function fromNativeScriptEventTarget(view: View, eventName: string) {
return fromEventPattern(
(handler: (data: unknown) => void) => {
view.on(eventName, handler);
},
(handler: (data: unknown) => void) => {
view.off(eventName, handler);
}
);
}
export class NsIfContext<T = unknown> {
$implicit: T;
nsIf: T;
}
/**
* This directive differs from *ngIf in that it will not be removed from the DOM, only hidden
* to use this you have to ensure that your "template" will not break if this condition is false
* if you use it in the following format: *nsIf="condition" or *nsIf="condition; visibilityType: 'hidden'"
* then the view will be created instantly but will be detached from change detection if it's not visible.
*/
@Directive({
selector: '[nsIf]',
standalone: true,
})
export class nsIfDirective<T = unknown> implements OnInit, OnDestroy {
private _context = new NsIfContext<T>();
private _isVisible: T;
/**
* The Boolean expression to evaluate as the condition for showing a template.
*/
@Input('nsIf')
private set isVisible(v: T) {
this._context.$implicit = this._context.nsIf = this._isVisible = v;
this.setVisibility();
}
private get isVisible(): T {
return this._isVisible;
}
private _visibilityType: 'collapse' | 'hidden' = 'collapse';
@Input()
private set visibilityType(v: 'collapse' | 'hidden') {
this._visibilityType = v;
this.setVisibility();
}
@Input()
private set nsIfVisibilityType(v: 'collapse' | 'hidden') {
this._visibilityType = v;
this.setVisibility();
}
private _unloadWhenHidden = false;
@Input()
set nsIfUnloadWhenHidden(v: boolean) {
this._unloadWhenHidden = v;
this.handleLoadedState();
}
private viewRef: EmbeddedViewRef<NsIfContext<T>>;
private currentVisibility: string | null = null;
private initialized = false;
private isDetached = true;
private loadedSubscription: Subscription;
constructor(
private viewContainer: ViewContainerRef,
@Optional() private elemRef: ElementRef<LayoutBase>,
@Optional() private templateRef: TemplateRef<NsIfContext<T>>
) {
this.ensureViewCreated();
}
ngOnInit() {
this.initialized = true;
this.setVisibility();
if (this.isDetached) {
// run first change detection so the children are properly created and initialized
this.viewRef?.detectChanges();
this.handleLoadedState();
}
}
setVisibility() {
if (!this.initialized) {
return;
}
const targetVisibility = this.isVisible ? 'visible' : this._visibilityType;
if (this.currentVisibility === targetVisibility) {
// Early return improves performance by skipping setting the visibility
// (which is kinda slow) and prevents the call to detectChanges()
return;
} else {
this.currentVisibility = targetVisibility;
}
if (this.templateRef) {
if (this.isVisible) {
this.ensureViewAttached();
this.viewRef.detectChanges();
} else {
this.detachView();
}
this.viewRef.rootNodes.forEach(node => (node.visibility = targetVisibility));
} else {
this.elemRef.nativeElement.visibility = targetVisibility;
}
this.handleLoadedState();
}
private detachView() {
if (!this.viewRef) {
return;
}
this.viewRef.detach();
this.isDetached = true;
}
private ensureViewCreated() {
if (!this.templateRef) {
return;
}
if (!this.viewRef) {
this.viewRef = this.viewContainer.createEmbeddedView(this.templateRef, this._context);
this.viewRef.detach();
}
}
private ensureViewAttached() {
if (!this.templateRef) {
return;
}
this.ensureViewCreated();
if (this.isDetached && this.initialized) {
this.viewRef.reattach();
this.isDetached = false;
}
}
handleLoadedState() {
if (!this.viewRef) {
return;
}
this.loadedSubscription?.unsubscribe();
this.loadedSubscription = new Subscription();
this.viewRef.rootNodes.forEach((node: unknown) => {
if (node instanceof View) {
if (this.isVisible && !node.isLoaded && node.isLoaded !== node.parent?.isLoaded) {
node.callLoaded();
} else if (!this.isVisible && node.isLoaded && this._unloadWhenHidden) {
node.callUnloaded();
} else {
this.loadedSubscription.add(
fromNativeScriptEventTarget(node, 'loaded').subscribe(() => {
this.handleLoadedState();
})
);
}
}
});
}
ngOnDestroy(): void {
this.loadedSubscription?.unsubscribe();
}
/**
* Asserts the correct type of the context for the template that `NgIf` will render.
*
* The presence of this method is a signal to the Ivy template type-check compiler that the
* `NgIf` structural directive renders its template with a specific context type.
*/
static ngTemplateContextGuard<T>(
dir: nsIfDirective<T>,
ctx: any
): ctx is NsIfContext<Exclude<T, false | 0 | '' | null | undefined>> {
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment