Support setters
rozek opened this issue · comments
Sorry for bothering you, but I must be completely blind right now...
Consider the following code
import { deepSignal } from 'https://unpkg.com/deepsignal@1.3.6/dist/deepsignal.module.js'
let Test = deepSignal({
_x:0,
get x () { return this._x },
set x (newX) {
if (newX !== this.x) { this._x = newX }
}
})
console.log('Test.x is defined as',Object.getOwnPropertyDescriptor(Test,'x'))
console.log('\ntrying to set "Test.x" to 1\n')
Test.x = 1
This code crashes with:
Test.x is defined as {enumerable: true, configurable: true, get: ƒ, set: ƒ}
configurable: true
enumerable: true
get: ƒ x()
set: ƒ x(newX)
[[Prototype]]: Object
trying to set "Test.x" to 1
index.ts:103 Uncaught TypeError: Cannot set property value of [object Object] which has only a getter
at Object.set (index.ts:103:30)
at VM6069 about:srcdoc:28:10
Do you have any idea why my code fails? I had the impression, that deepSignal
would support getters and setters...
I think it should support setters, yes. I'll take a look 🙂
I don't know if it will be of any help, but I had to use untracked
around my Reflect
calls in order to prevent undesired signal subscriptions.
Here is the code I am currently using in order to observe objects (not arrays) with getters and setters without nesting (as this may cause real headaches) and without properties with names starting with '_' - it's quite simple
import { expectObject } from 'javascript-interface-library'
import { computed, signal, untracked } from '@preact/signals-core'
namespace observableObject {
const ProxyForObject = new WeakMap()
const SignalsOfProxy = new WeakMap()
/**** observableObject - makes a given object observable ****/
export function observableObject (Target:object):object {
expectObject('target object',Target)
if (ProxyForObject.has(Target)) {
return ProxyForObject.get(Target)
}
const TargetProxy = new Proxy(Target,ObservationTraps)
ProxyForObject.set(Target,TargetProxy)
return TargetProxy
}
/**** ObservationTraps - actual implementation of object content observation ****/
const ObservationTraps = { // nota bene: target.x is observed, not receiver.x!
get (Target:object, Property:string, Receiver:object):any {
const Value = untracked(() => Reflect.get(Target,Property,Receiver))
if (ProxyForObject.get(Target) !== Receiver) { return Value }
if (PropertyShouldBeObserved(Property)) {
let PropertySignal = SignalForProperty(Target,Property,Value,Target)
let Dummy = PropertySignal.value // triggers signal usage
} // important: the above assignment must not be optimized away!
return Value
},
set (Target:object, Property:string, Value:any, Receiver:object):boolean {
const successful = untracked(() => Reflect.set(Target,Property,Value,Receiver))
if (! successful) { return false } // will lead to a "TypeError"
if (ProxyForObject.get(Target) !== Receiver) { return Value }
if (PropertyShouldBeObserved(Property)) {
let PropertySignal = SignalForProperty(Target,Property,Value,Target)
PropertySignal.value = Value // triggers signal value change
}
return true
},
deleteProperty (Target:object, Property:string):boolean {
const successful = Reflect.deleteProperty(Target,Property)
if (! successful) { return false } // will lead to a "TypeError"
if (PropertyShouldBeObserved(Property)) {
let PropertySignal = SignalForProperty(Target,Property)
if (PropertySignal != null) {
SignalForProperty(Target,Property).value = undefined // triggers chng.
}
}
return true
},
defineProperty (Target:object, Property:string, Descriptor:object):boolean {
const successful = Reflect.defineProperty(Target,Property,Descriptor)
if (! successful) { return false } // will lead to a "TypeError"
if (PropertyShouldBeObserved(Property)) {
const Value = untracked(() => Reflect.get(Target,Property))
let PropertySignal = SignalForProperty(Target,Property,Value,Target)
PropertySignal.value = Value // triggers signal value change
}
return true
}
}
/**** PropertyShouldBeObserved ****/
function PropertyShouldBeObserved (Property:string):boolean {
return ! Property.startsWith('_')
}
/**** SignalForProperty ****/
function SignalForProperty (
Proxy:object, Property:string, Value?:any, Target?:object
):any {
let ProxySignalSet = SignalsOfProxy.get(Proxy)
if (ProxySignalSet == null) {
if (Target == null) { // no implicit signal creation needed
return undefined
} else { // implicit signal creation requested
SignalsOfProxy.set(Proxy,ProxySignalSet = new Map())
}
}
let PropertySignal = ProxySignalSet.get(Property)
if (PropertySignal == null) {
if (Target != null) { // implicit signal creation requested
PropertySignal = signal(Value)
ProxySignalSet.set(Property,PropertySignal)
}
}
return PropertySignal
}
}
const global = (new Function('return this'))()
global.observableObject = observableObject.observableObject
just a small note on the above code: after let observable = observableObject(original)
, only original
is observed (with observable
as its proxy) not any object derived from these (in other words target
must equal receiver
in the trap functions)
Released as part of deepsignal@1.4.0.