Better know a framework #27: using watch decorator

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

When using @watch on a method, I think having newValue as first parameter is quite an expected behavior. When using @watch on a class, though, it feels like it’s sometimes better to have the instance as the first parameter. But if we are to shuffle the parameters based on @watch usage, it could feel weird or inconsistent. If this was controllable, then inside an app, we could encounter different styles in different sections, and won’t be able to even recognise anything quickly, probably a funnier situation.

For the @watch decorator, or automatic observation in general, there’s a need to know when to start/stop the observation, for that maybe observing static properties/fields doesn’t feel like feasible. It can still be done via user-controlled observation API though.

For the ability to distinguish between the source expression of the callback method, it may be useful, actually. We could add that. Though it would be incompatible with a future version of @watch, where we support real function as a way to collect dependencies.

Nice suggestions, btw :+1:

1 Like

Yes. I agree with the fact that the intuitively best argument order of the decorator would be dependent on its context (as a class decorator or property decorator). I understand the difficulty of this design decision. Since it is pretty challenging (if not impossible) to determine how the community will use this decorator, I doubt if that criterium should be taken into account for your design decision here. Perhaps the choice should be mainly based on other criteria, like usage consistency, expected future usage (regarding TypeScript and JavaScript evolution), internal scalability in Aurelia, etc. Anyway, to me, as a TypeScript-oriented developer, I would prefer property decoration over class decoration, and thus - together with the consistency argument - the current order (newValue, oldValue, instance) does seem to be the most logical choice to me. Unless it is already known that this choice would complicate any important future design choices in Aurelia.

I also agree with the static issues. It was just a question. Using singletons would be a design choice as well, and a very valid one in the Aurelia architecture. IMHO, it would probably be better to avoid using static stuff as much as possible.

For the source expression issues (if they would be important for me as an Aurelia consumer), instead of this:

class App {

  counter1 = 0;
  counter2 = 0;

  @watch(abc => abc.counter1)
  @watch(abc => abc.counter2)
  log(newValue) {
    // which expression triggered me???
  }
}

… I could just as easily do something like this:

class App {
  counter1 = 0;
  counter2 = 0;

  @watch(abc => abc.counter1)
  logCounter1(newValue) {
    // log counter 1
    this.log(newValue, 'counter1');
  }

  @watch(abc => abc.counter2)
  logCounter2(newValue) {
    // log counter 2
    this.log(newValue, 'counter2');
  }

  log(newValue, expressionName) {
    // Great, I now know who triggered me.
  }
}

… or with a class decorator:

@watch(abc => abc.counter1, (newValue, oldValue, app) => app.log(newValue, 'counter1'))
@watch(abc => abc.counter2, (newValue, oldValue, app) => app.log(newValue, 'counter2'))
class App {
  counter1 = 0;
  counter2 = 0;

  log(newValue, expressionName) {
    // Great, I now know who triggered me.
  }
}

… if it is necessary. Perhaps it would be more straightforward to handle specific logic in the logCounter1 and logCounter2 methods themselves.

1 Like

for me, even a simple decorator that just calls the function (without any parameters) is already good enough. :slight_smile:

but of course “knowledge is power” and I would prefer to have the power and knowledge of what exactly trigger the watch.,

I think that keeping the same order of parameters in all use-cases is an absolute must.

  1. it’s a lot simpler in term of the plugin code [for future maintaining]. no need to have multiple path’s in the code for multiple usage/context styles.
  2. it’s a lot simpler in term of the user code. you learn once, you use many times. in different contexts.

I think that we should have the below list of parameters for all use-cases.

@watch(abc => abc.counter1)
@watch(abc => abc.counter2)
log(newValue, oldValue, instance, expressionName) {
}

this is consistent with already familiar aurelia api’s, like @observable and @bindable [with the addition of instance etc.]
it simplify the (likely) most often used use-case where we will have only 1 watched expression and we will only want to know about the newValue.

if the expression is a complex one [eg someObj.someProperty.something.value],
instance should be something? or someObj?
the answer to that design question also dictate what will go into expressionName.
instance: something, expressionName: ‘value’.
instance: someObj, expressionName: ‘someProperty.something.value’.

2 Likes

Yes, I agree about those proposed parameters. But I think that the @watch signature requires an additional argument as well, which will be passed as the fourth parameter to the method. Or is it possible to let Aurelia “automatically” pass a generated readable/usable value in the method’s expressionName argument?

Adding such an additional argument for the expression name to the decorator might make its implementation quite complex, I guess, since it should also already support a second parameter when used as a class decorator…

But if this change would interfere with upcoming features, as @bigopon fears, I would not mind if it does not get implemented this way. IMHO, there are other ways to get the information I need (if I actually need it).

2 Likes

Well, I think expressions could be more complex. What would be expressionName's value in the scenario below?:

class App {
  counter1 = 0;
  counter2 = 0;

  @watch(abc => abc.counter1 < abc.counter2)
  log(newValue, oldValue, instance, expressionName) {
  }
}

(The intention of the above code is to invoke the log method based on a comparison of counter1 and counter2. The passed newValue and oldValue arguments will be booleans in this case.)

And, by the way, as I see it, instance will always refer to either the instance of the decorated class on which the change is detected (when @watch is used as a class decorator) or the decorated method’s class instance on which the change is detected (when @watch is used as a property decorator). But I might be mistaken, so it would indeed be useful if this could be explicitly clarified.

2 Likes

I think expression like @watch(abc => abc.counter1 < abc.counter2) should not be allowed at all.
one should simply observe the changes to the core values, and apply logic inside the function.

I assume that the watch decorator is using the BindingEngine behind the scene.
and therefor will likely suffer some limitations as well (like observing array mutation etc.)

2 Likes