Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support setters #47

Closed
rozek opened this issue Sep 21, 2023 · 4 comments · Fixed by #62
Closed

Support setters #47

rozek opened this issue Sep 21, 2023 · 4 comments · Fixed by #62

Comments

@rozek
Copy link

rozek commented Sep 21, 2023

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...

@luisherranz
Copy link
Owner

I think it should support setters, yes. I'll take a look 🙂

@luisherranz luisherranz changed the title inaccessible setter Support setters Sep 21, 2023
@rozek
Copy link
Author

rozek commented Sep 21, 2023

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

@rozek
Copy link
Author

rozek commented Sep 21, 2023

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)

@luisherranz
Copy link
Owner

Released as part of deepsignal@1.4.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants