Input Helper fires action before binding value

If I have an input like this;

<Input @value={{item.quantity}}  @type="number" min="0" max="1000" step="1" class="accessory-quantity-spinners" {{on "input"  this.changedValue}} /> 

When you click the up or down arrow on this, it first fires changedValue action, and the value field is not actually bound to the updated value inside the action because it must be happening afterwards. Does anyone know why it was built like this because it makes it difficult to so anything with. If your action updates a shopping list based on what the bound value (item.quantity) is, it’s going to be one step behind all the time because it didnt get updated until AFTER the action was fired. The action, surely will want the latest value?

This is more or less a weird-seeming (but actually quite straightforward, as I’ll cover below) interaction between Ember Classic’s 2-way-binding model and browser event handling, which Ember Octane’s stricter data down, actions up/one way data flow model makes much clearer.

The @value argument here is (unlike with Glimmer components!) two-way bound to whatever property it is passed. This dates all the way back to Ember 1.x—before the DDAU paradigm was introduced, much less before it became the very strong standard in the Ember 2.x days, muuuuuuch less before we moved almost entirely away from 2-way-binding with the move to Glimmer components as part of Octane. This is why you’re expecting (correctly) that it’ll get updated by the input value changing.

But fundamentally, what two-way binding actually means is, roughly, installing an event listener which then updates the property—notionally, exactly the same way you would write it by hand using set in the Classic world or just setting a @tracked property in the Octane world; there are other details involved, but that’s the gist of it. And it’s worth seeing that there isn’t any other way it could be implemented: it’s fundamentally just a wrapper around <input> and the only way to keep the thing passed to @value in sync with whatever is in the <input> is to listen to an event and update it.

Given that, the behavior you’re seeing is the only possible behavior. The two-way binding is just doing the same thing you are: listening to the input events when they fire and then updating the value!

(This kind of thing is one of many reasons we moved away from two-way binding: it is confusing, even if it ultimately makes sense if you reason all the way through it. Having to reason all the way through it like this to understand the behavior is a problem!)

Another thing to notice here: there are, in the way you’re handling the action, two different sources of truth for the value in question—you’re trying to use item.quantity, expecting it to have been implicitly updated for you. But there’s also the event which passes along that value in the first place, which is what you’re using to drive it. And then if you’re trying to update a shopping list from that, you might be introducing a third! It would be much better (in terms of reliability, eliminating bugs, maintaining it longer term) for your flow to be more like:

  • event triggers action
  • action uses event’s valueAsNumber to set the item.quantity
  • total shopping list is derived from all the items and their quantitys

Then there’s just that one action setting one value, and everything else falls out more or less automatically. The weird bit, of course, is that item.quantity ends up set twice: by your action and by the two-way binding. Happily, that won’t hurt anything. But it’s still weird.

Net, my own view here is that for exactly this reason, it’s often better to use a regular <input> instead of the <Input> component, and to wire up your own event listeners. That will make you responsible to set the item.quantity value in the action, but it also eliminates that last problem of having two different ways of setting the same value, and it also gives you a chance to do other things with the event handling.

Hi Chris,

Thanks very much for your full reply. Your suggestion of the alternative flow (e.g not relying on @value) sums up what i did in the end to amend the issue. But I will bear in mind the possibility of not using the built in helper for next time.

I had been only using the action as a general update to derive and update the shopping list assuming the value was already set by the binding and indeed the shopping list was derived from that. But the need to take the value and set it in the event is surprising when there is a @value but yes now that you point it out there is not much else it could have done cleanly.

I think it would help if the tutorials mentioned this because there is an example of an Input with @value and action and its easy to jump to assumptions.