To me this does not look like a bug. You programmed elm to respond in two different ways to the same click. The first way is with a Command. Basically a command is an instruction to TEA. In this case to Send a ClickButton message to the update function.
The second way is that you made a subscription. A subscription is when you tell the Elm architecture that you want to be notified when something happens. In this case a click event. You handle this notification by sending another ClickButton message to the update function.
It does look like a bug to me, because the subscription for the click was not even set up until after the click happened. Also removing the NoOp from the subscription makes the event only happen once, which is just plain weird. I don’t really understand why this happens, but it does not seem right.
@koenusz the issue here is the time at which the subscription gets registered. Note that it is not registered at the time of the click event. Is it reasonable for the subscription to be triggered by the very same click event that caused itself to be registered?
I wonder if it is a case of Mouse.clicks getting confused or a more general issue with respect to event handling. The fact that removing the NoOp fixes the problem indicates the former?
I haven’t studied the Elm code and the NoOp subscription puzzles me a bit as to how it is involved, but the symptoms suggest that what happens is that Elm processes the click event at the button and updates the model and subscriptions at that time. The event then keeps bubbling up the DOM where it gets picked up by the now updated subscription.
I suspect that in JavaScript, if you attached an event handler further up the DOM when processing an event lower down in the DOM, you would see similar effects. Or perhaps you wouldn’t because perhaps the DOM takes precautions against this. And that right there would explain why the NoOp has an effect. With the NoOp subscription, Elm was already subscribing to the mouse clicks and had already installed an event handler in the DOM. Elm was just changing what it did with those events when they arrived from the DOM. So, any filtering behavior on JavaScript’s part gets wiped out by the NoOp subscription and by the time the click listener at the root gets the event, the update has already been called and processed when the event was at the button level.
In a side note, I would not count on this behavior in an application. It’s a bug from your perspective but maybe someone else would have a reason for writing code this way. The thing that makes it dubious to count on is that Elm is vague about how often subscriptions gets called. So, I think Elm is free to call or not call subscriptions as the run time sees fit just as it is free(*) to not call the view function until the next animation frame.
Mark
(*) This optimization appears to cause problems with textfields if you use the value attribute because the model value can be tracking behind the actual textfield state.
It makes sense that the runtime is free to evaluate subscriptions whenever it sees fit, but I think the intuition one might have for how it realizes subscription changes is betrayed in this example. Based on the way view changes are realized—that is, the view is updated atomically, after all events in a tick—one might expect subscription changes to be realized similarly. But in this example we can observe that some subscription changes are realized in the middle of event propagation and others after.
Expected?
1. Process messages from Html.Events.onClick
2. Process messages from Mouse.clicks
3. Add/replace/remove subscriptions as needed
Observed
1. Process messages from Html.Events.onClick
2. Replace/remove existing Mouse.clicks subscriptions
3. Process messages from Mouse.clicks(using “partially” updated subscriptions)
4. Replace/remove existing Mouse.clicks subscriptions
5. Add new Mouse.clicks subscriptions
The observed sequence explains why using NoOp and always subscribing to Mouse.clicks (mid-event replacement) produces different behavior than conditionally subscribing (mid-event removal, post-event addition).
The naive conceptual model for subscriptions is probably that they are processed after every update — which they may actually be — and that they can immediately start or stop listening for particular “events”. The latter part is what is potentially naive since if there are any queues involved anywhere either in the Elm runtime or on the browser side, there is a question about what happens for things that are queued for delivery but haven’t actually been delivered. For example, consider what the interaction needs to be or could be between the command querying the window’s size and the resize subscription if the window is in the midst of resizing. Unless the window size query command immediately invokes an update with its result, it is in a race condition with the notifications from the callback driving the subscription. Many commands act asynchronously but this one can’t.
Turning back to the case of mouse clicks, does the root level click notification happen at the same conceptual time as the element level notification or not? Since everything is serialized, the answer is practically not. The browser may well, however, be trying to make it appear to be so. For example, the browser could capture the list of potential callbacks to invoke at the point when it starts dealing with the event. That way changes to the callback structure won’t change the handling for this event.
Where this comes back to Elm is that the browser’s behavior for the event is theorized to be based on which callbacks Elm has registered at the time of the click. From Elm’s standpoint, Elm probably just has or doesn’t have a root level callback for window clicks. It doesn’t add and remove callbacks as the subscriptions change but only as the presence or absence of any click subscriptions changes. Changing what the subscriptions do changes how Elm handles the invocation of the click callback. So, in the NoOp case, Elm already had a click subscription and hence a registered callback and hence the browser picked that callback up as on the list of callbacks to invoke for the event. Without the NoOp, Elm responds to the click by gaining a click subscription and hence it registers a root-level click callback but that callback is too late to be captured by the browser as a potential handler for this particular event.
Finishing up and looking again at the broad question, what this seems to mean is that if Elm is going to the expense of calling subscriptions after every update and Elm is just responding to callbacks from outside rather than maintaining an internal queue of messages to be handled, then stopping a subscription will guarantee that you won’t receive a message for that subscription because it is already in flight. (It would be nice to have that be an explicit guarantee. I code defensively since it isn’t.) On the other hand, given browser behaviors around deciding which callbacks are called, it can be unpredictable when exactly a new subscription will be invoked.
not much idea of what’s happening but I tweaked a bit you example to have 4 messages: ClickButton | ClickDiv | ClickSub | NoOp. That might help understanding the behavior. Mouse events propagations are tricky stuff because of bubbling. I personally prefer to prevent default and stop propagation by default.