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 theitem.quantity
- total shopping list is derived from all the
item
s and theirquantity
s
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.