luisherranz / deepsignal

DeepSignal 🧶 - Preact signals, but using regular JavaScript objects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.