Created
October 20, 2024 08:27
-
-
Save rdlabo/b032dfe2dd4fc6de3be328ab54088bd8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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