Skip to content

Instantly share code, notes, and snippets.

@rdlabo
Created October 20, 2024 08:27
Show Gist options
  • Save rdlabo/b032dfe2dd4fc6de3be328ab54088bd8 to your computer and use it in GitHub Desktop.
Save rdlabo/b032dfe2dd4fc6de3be328ab54088bd8 to your computer and use it in GitHub Desktop.
import { Signal, signal, WritableSignal } from '@angular/core';
import { IsArrayNullable, IsKnownRecordNullable, IsPrimitiveNullable } from './type-utils';
export type DeepModel<T> = IsPrimitiveNullable<T> extends true
? WritableSignal<T>
: IsArrayNullable<T> extends true
? { [K in keyof T]: DeepModel<T[K]> } & WritableSignal<T>
: IsKnownRecordNullable<T> extends true
? { [K in keyof T]: DeepModel<T[K]> } & WritableSignal<T>
: Signal<T>;
class DeepModelHandler<T> implements ProxyHandler<DeepModel<T>> {
private readonly _modelSignal: WritableSignal<T>;
private readonly _cache: Partial<Record<keyof T, DeepModel<any>>> = {};
constructor(private model: T) {
this._modelSignal = signal<T>(this.model);
}
apply(target: DeepModel<T>, thisArg: any, argArray: any[]): T {
let currentModel = this._modelSignal();
for (const prop in this._cache) {
const key = prop as keyof T;
const signalObj = this._cache[key];
if (signalObj) {
/**
* currentModel[key] = signalObj() as T[keyof T];
* console.log(Object.getOwnPropertyDescriptor(currentModel, key) ? 'readonly' : 'not readonly') で調べたところ
* なぜか何回目かのレンダリングでreadonlyになってしまうため、強制的に上書き
*/
currentModel = { ...currentModel, [key]: signalObj() };
}
}
return currentModel as T;
}
get(target: DeepModel<T>, prop: string | symbol, receiver: any) {
if (typeof prop != 'string') {
return null;
}
let propStr = prop as string;
{
if (propStr.toLowerCase() == 'tostring') {
return JSON.stringify(this._modelSignal());
}
if (propStr.toLowerCase() == 'set') {
return (value: T) => {
this.clearCache();
this._modelSignal.set(value);
};
}
if (propStr.toLowerCase() == 'update') {
return (updateFn: (value: T) => T) => {
this.clearCache();
this._modelSignal.update(updateFn);
};
}
if (propStr.toLowerCase() == 'asReadonly') {
return this._modelSignal.asReadonly.bind(this._modelSignal);
}
}
const key = propStr as keyof T;
const model = this._modelSignal();
if (!Object.hasOwnProperty.apply(model, [key])) {
throw Error('property not found: ' + propStr);
}
if (key in this._cache) {
return this._cache[key];
}
const value = model[key];
if (typeof value === 'object') {
let deepSignalObj = deepModel(value);
this._cache[key] = deepSignalObj;
return deepSignalObj;
} else {
let signalObj = signal(value);
this._cache[key] = signalObj;
return signalObj;
}
}
private clearCache() {
for (const prop in this._cache) {
const key = prop as keyof T;
delete this._cache[key];
}
}
}
export function deepModel<T>(model: T): DeepModel<T> {
let target: Function = function () {};
return new Proxy(target, new DeepModelHandler<T>(model)) as DeepModel<T>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment