Better know a framework #27: using watch decorator

If you have ever found that you need to observe some property, or an expression of a custom element or a custom attribute, and call a method when the value of that property or expression change, then you probably have looked at something like the ObserverLocator or BindingEngine. This is not always ergonomic, and could result it forgotten subscription. We are trying to evaluate an official API for v2 around this, and thanks to @jods4 for api idea, we’ve got a watch decorator: https://www.npmjs.com/package/aurelia-watch-decorator

Decorate a class:

@watch(abc => abc.counter, (newValue, oldValue, app) => app.log(newValue))
class App {
 
  counter = 0;
 
  log(whatToLog) {
    console.log(whatToLog);
  }
}

Decorate a method:

class App {
 
  counter = 0;
 
  @watch(abc => abc.counter)
  log(whatToLog) {
    console.log(whatToLog);
  }
}

The watch decorator is planned to be a core part of v2, please help discuss & shape the api for this potentially useful decorator.

8 Likes

Is this intended to be a replacement for the @observable decorator? Or will it complement @observable? Will there be an overview of the functional differences between @watch and @observable and when to apply which?

1 Like

Thanks for the important question, I forgot this.

For v1, the @watch decorator can do everything the @observable decorator can do, in an opposite way. For v2, they will be complementary, there’ some planned new capabilities for the @observable decorator.

2 Likes

Nice. :slight_smile:

I am not sure what you mean with “in an opposite way”. As far as I can see, @watch and @observable have almost the same purpose, but with different functional (and interesting) capabilities.

Furthermore, you also say that @watch will be able to observe expressions. Can it also observe property getters? Will it use a dirty checking observation strategy by default in such cases where such an expression or property getter is dependent on another class member? Will it be possible to combine @watch with @computedFrom to avoid such dirty checking?

2 Likes

With @observable, we decorate a property to observe, and invoke a method
With @watch, we decorate a method, and observe a property
… kind of opposite :stuck_out_tongue:

For @watch, you can decorate multiple times on a method:

class MyClass {
  @watch(o => o.data1)
  @watch(o => o.data2)
  log(newData) {
    console.log(newData);
  }
}

Can it also observe property getters?

It can, but will use different strategies to observe. And as you recognised, If there’s no @computedFrom, it will use dirty-checking strategy. It will also work well with plugin like this GitHub - bigopon/aurelia-deep-computed: A plugin for declaring deep/shallow computed observation

3 Likes

Ah. Yes. Now I see what you mean with “opposite”.

Looks great. :+1:
I will try it out very soon. :slight_smile:

1 Like

Looks great, and I can see the added benefits over decorating the method instead of the property.

One thing that would be nice OOTB is some kind of debouncing mechanism as a parameter, like the debounce and throttle binding behaviors?

2 Likes

Hi @arnederuwe,

Yes, I agree that could be useful in some scenarios. However, I doubt if this new watching/observing functionality is directly related to Aurelia’s binding features. I think these are two separate concerns.

But I guess it would be pretty straightforward to implement some debouncing/throttling functionality yourself as you desire, or simply use a library that already implements such functionality (like Lodash or so).

1 Like

I use https://www.npmjs.com/package/typescript-debounce-decorator on my V1 projects in combination with @observable and it works great

They are indeed 2 separate concerns, but it would help if we could keep the terminology the same IMO

2 Likes

I think this is a great idea.

for big flaw with regular get + computedFrom is:
get logic are only applied if you have something that is bound to it in the view.

but sometimes you want a piece of logic to run and change based on other values.
but you have nothing in particular to be bound to.
(this is not some weird example, I really had this kind of problem in a real app).
I wish I knew about watch then. it would solve the problem so nicely…

1 Like

You can also do:

  bindingEngine.propertyObserver(obj, 'someGetter').subscribe(() => ...)

But the core idea of your comment is valid, and kind of an issue we can & should fix: it only starts after you have triggered an observation.

1 Like

while we’re here, happy birthday @Bart :smile:
image

3 Likes

For the sake of clarity, while @watch decorates a method, and @observable wraps a property, the effect is essentially the same: watch a property and invoke a method. If that’s correct, I’d like to hear use-cases were this is helpful. Other than providing the convenience of avoiding wrapping calls like:

colorChanged(newValue, oldValue) {
  log(newValue);
}

are there other benefits to using @watch, or reasons why it would be more semantic in certain scenarios?

1 Like

you start to have a benefit when you have more that one property to observe.

also - the dependency is stated on the target, instead of on the dependent.
this alone is a reason to bake watch into the core framework.

1 Like

With @observable, you observe a single property and react to it.
With @watch, you observe an expression (v1), so it’s more ergonomic in some cases:

@watch('some.expression')
myMethod() {

}

// with typescript, you can also have intellisense:
@watch((abc: Abc) => abc.props.counter, newValue => 'new value is of type number')
class Abc {
  props = { counter: 0 }

}

Also you can setup multiple watches:

@watch('data1.prop')
@watch('data2.prop')
logEverything() {

}

And watch is guaranteed to work during bind/and stop before unbind, so no it’s kind of mentally easier to deal with.
Plus @avrahamcool said :point_up:

EDIT: one obvious benefit of @observable is that it’s synchronous, in case where it really matters.

@bigopon areyou planing on adding the watch decorator in aurelia 2?
this is realy a core feature that should be embeded in the framework. rather than an external dependency.

EDIT:
i just re-read the first message here and got my answer.
thanks again.

1 Like

Yes, definitely. I’m not sure about the arguments of the callback:

(newValue, oldValue, instance)

vs

(instance, newValue, oldValue)

Also, could be missing creative ideas/possible further improvement, so hoping that people would use it a bit more before adding it to v2.

When using @observable on properties, I am quite often only interested in the newValue parameter in the corresponding *Changed callback function. I consider it an elegant advantage to have the possibility to avoid specifying the oldValue parameter when I don’t need it.

So my question is if the instance argument is important enough to specify it as the first argument. It would be a pity if I would always have to specify a parameter for it, even when I do not need it in most cases.

1 Like

Would it be possible to decorate static class methods with @watch? Perhaps it might be nice to be able to do something like this:

Decorating a class:

@watch(abc => abc.counter, App.log)
class App {

  counter = 0;

  static log(newValue) {
    console.log(newValue);
  }
}

Decorating a static method:

class App {

  counter = 0;

  @watch(abc => abc.counter)
  static log(newValue) {
    console.log(newValue);
  }
}

And will it be possible to watch static expressions as well? Something like this?:

Decorating a class to watch a static property/expression:

@watch('App.counter', App.log)
class App {

  static counter = 0;

  static log(newValue) {
    console.log(newValue);
  }
}

Decorating a static method to watch a static property/expression:

class App {

  static counter = 0;

  @watch('App.counter')
  static log(newValue) {
    console.log(newValue);
  }
}
1 Like

Being able to specify multiple @watch decorators for the same method is very powerful. But could it be useful to be able to determine which expression triggered the decorated method? (This would probably require an additional name argument to the @watch decorator, which is passed as a fourth argument to the decorated method or the callback.)

But if this would become too complicated, I could certainly settle with decorating different methods instead (thus avoiding multiple @watch decorators on the same method).

1 Like