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

Make class fields observable #79

Open
phillipskevin opened this issue Jun 5, 2019 · 14 comments
Open

Make class fields observable #79

phillipskevin opened this issue Jun 5, 2019 · 14 comments

Comments

@phillipskevin
Copy link
Contributor

phillipskevin commented Jun 5, 2019

We should eventually make this work:

class Obj extends ObservableObject {
  prop = "Hello";
  static props = {};
}

const obj = new Obj();
obj.on("prop", () => {
    console.log("prop changed");
});

obj.prop = "Hi";
@justinbmeyer
Copy link

var base = new Proxy({},{
  defineProperty(){
    console.log("hit")
  }
});
var obj = Object.create(base);
Object.defineProperty(obj, "foo", {value: "abc"})

@justinbmeyer
Copy link

justinbmeyer commented Jul 26, 2019

This is prevented by tc39/proposal-class-fields#151 (comment)

I wrote up a bit on why in that thread: tc39/proposal-class-fields#151 (comment)

@justinbmeyer
Copy link

I think we could do this with ObservableObject, but not with StacheElement:

class HelloWorld extends StacheElement {
  static view = `<h1>Hello {{this.subject}}</h1>`;
  subject = "World";
}
customElements.define("hello-world", HelloWorld);

@justinbmeyer
Copy link

justinbmeyer commented Jul 26, 2019

Solution BrainStorm

Parsing

Could we parse?

class Obj3 {
  foo = "bar"
  constructor(){
  }
}
Obj3.toString() // -> includes `foo = "bar"`

Call a method

We could ask folks to call a method:

class HelloWorld extends StacheElement {
  static view = `<h1>Hello {{this.subject}}</h1>`;
  subject = "World";
  constructor(){
    super();
    this.checkForProps();
  }
}

Check for these props in lifecycles

Like calling a method, we could check for these properties in the lifecycle methods.

This would mean that new MyElement().initialize() will always need to be called as new MyElement({foo: "bar"}) will not be able to make any class fields observable.

Post initialization hook

If there was a way to capture when new MyElement() was complete, we could look for these properties.

Decorators could help here, we could make sure the final constructor has some "completion" code.

Overwrite Object.defineProperty

super() doesn't directly use Object.defineProperty() to set these properties.

Wait for decorators

https://github.com/tc39/proposal-decorators#set

class HelloWorld extends StacheElement {
  static view = `<h1>Hello {{this.subject}}</h1>`;
  @set subject = "World";
}

@phillipskevin
Copy link
Contributor Author

phillipskevin commented Jul 29, 2019

For ObservableObject/ObservableArray we can proxy instances, so we could make these constructors return a proxy that traps defineProperty and makes these observable.

For StacheElement, one of the solutions above could be used. I think doing this check in initialize() makes sense.

If that works though, maybe we should just do the same in initialize for the observables as well to keep the behavior consistent.

@matthewp
Copy link
Contributor

matthewp commented Aug 7, 2019

I think the initialize() suggestion will work fine for StacheElement.

For ObservableObject/ObservableArray, however, the "proxy instance" idea would have a few downsides.

  1. There is already a proxy on the prototype. Would we remove that and only have one on the instance or have 2?
  2. All of the code ported from can-define operates on defineProperty. If we are trapping defineProperty how do we differentiate between calls made by fields vs. calls made by define.property, etc?
  3. This method would not work with static seal = true

@justinbmeyer
Copy link

Can you elaborate on the reasons for #3?

@matthewp
Copy link
Contributor

matthewp commented Aug 7, 2019

Our static seal = true option seal the instance inside of the constructor. Fields define after the super() calls, so it will throw. So there's an inherit incompatibility between how fields work and how our seal behavior works, if sealing happens in the constructor phase (as opposed to the initialize phase).

@phillipskevin
Copy link
Contributor Author

For ObservableObject/ObservableArray, could we also do it in initialize()?

@matthewp
Copy link
Contributor

matthewp commented Aug 7, 2019

Currently initialize() is called in the constructor. Where would we move it to?

@phillipskevin
Copy link
Contributor Author

All of the code ported from can-define operates on defineProperty. If we are trapping defineProperty how do we differentiate between calls made by fields vs. calls made by define.property, etc?

We don't ever defineProperty on the instance, right? I don't think this is an issue.

@justinbmeyer
Copy link

justinbmeyer commented Aug 8, 2019

@matthewp (not saying we should do this), but we could make a "weak" seal ... essentially allow Object.defineProperty() sets, but not foo.bar = value sets. This would probably catch 99% of the user problems that an actual seal would catch.

@phillipskevin
Copy link
Contributor Author

We just found another issue this would fix. If you set a property on an element instance before it is upgraded (before customElements.define is called), its observability is broken: https://codepen.io/kphillips86/pen/GRKNgVw?editors=1010

It's not incredibly likely that people will do this, but it should be possible.

@phillipskevin
Copy link
Contributor Author

phillipskevin commented Apr 17, 2020

@cherifGsoul for ObservableArray, if someone does this:

class Foo extends ObservableArray {
  static items = type.convert(ObservableObject);
  items = "Baz";
}

We should warn for the items field and suggest using static items instead.

ObservableArray does not support a class field named items. Try using a different name or using static items

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

No branches or pull requests

3 participants