Search code examples
javascripteventssetter

In event-APIs of Javascript (like DOM), when do the setters for properties which have a corresponding change event do themselves trigger this event?


Note that "setter" is meant in the broader sense, i.e. any method that does set or change a certain property. Likewise "change event" is meant in the broader sense, i.e. an event that indicates that a certain property has changed.

I'm struggling to understand if there is a certain logic, according to which in Javascript events are sometimes fired when a setter is called programmatically and sometimes not.

Examples of setters that don't fire a corresponding change event (for them those events are only fired with user-interaction):

  • <input>-tags, <select>-tags and <textarea>-tags:
    programmatically setting tag.value = newValue does not fire a 'change'-event
  • history.pushState() changes history.state but the corresponding 'popstate'-event is not fired

Examples of setters that do fire a corresponding change Event:

  • 'hashchange' is fired even when set programatically with document.location.hash='#x=y'
  • 'resize' is fired even when the window size of a popup window is set programmatically via win.resizeTo(x, y)
  • <video> and <audio>-tags: video.currentTime = newValue; does fire a 'timeupdate'-Event
  • video.pause() (as a setter for property video.paused) does fire a 'paused'-event.

(I've tested these with Firefox 104)

I understand the logic of not firing an event (i.e. the first group) as follows: If we set a property ourselves programmatically, then we know that it has changed, so there is no need for an event. There are situations where it can be practical, but it is not necessary. It could even cause harm. Imagine, we have two <input> fields with a different stepattribute (e.g of type ruler) synchronized by listening to each other's change event. When the first changes by user interaction and fires a change event, we adapt the second with its setter. If that setter would fire a change event again, the first would listen to it. With different steps this can lead to an infinite ping-pong. According to this reasoning, the rule should be that an event should be fired only when there is some input from the exterior world. (Maybe in functional programming language one could pin it down to an I/O Monad)

For the second group I recognize that there are some setters that might not be successful or at least not immediately. E.g. window.resizeTo() succeds only, if the window was opened by us. Also the setter for video.currentTime might not be successful, e.g. if the video is not yet loaded. So in these cases an event could seem necessary, to tell us if and when the setting succeeded. But the setting of document.location.hash seems immediate to me (no Http-Request needed), so why an Event here? And also for video.pause() I don't understand the event, as it (in contrast to video.play()) does not return a Promise and is supposed to be immediate and always successful.

The information which setters induce change-events and which don't is not easily available (at least I didn't find e.g. the information about the fact that setters for input fields don't induce a change event on Mozilla, but was testing myself). Therefore I'm looking for a rule of thumb. So I'm curious to hear if there is more to it than the two reasonings I've sketched above. This would also be useful when creating own objects (e.g. custom-html elements) and having to decide, whether to throw an event in a setter or not.

Remark 1: As a reaction to comments, I would like to stress that I am of course aware that one can find the information for each event somewhere in the specs or on MDN. Just sometimes not as prominently as I would have expected. It was not my main concern to get this information in a particular case, but I was more looking for the logic behind the different behavior, in order to get a rule of thumb or alternatively an overview if it already existed. Nevertheless some provided links and comments were already very helpful and I'm going to write an own answer, which I would be happy to turn into a community wiki, if there is interest to turn it into such an overview.

Remark 2: My original title was asking for "native Javascript". My intention was to not ask about arbitrary libraries like Jquery, but to restrict to officially specified Javascript APIs. However, I was informed in the comments that "native" is not the proper word, in particular as all my examples apparently were about the DOM API while native Javascript has nothing like a DOM.


Solution

  • Edits to make this community-wiki answer more complete or more correct are explicitly encouraged. The answer so far is intentionally quite redundant to the question, because the question was giving part of the answer. This should enable future editors to modify the existing reasoning where necessary.

    The examples listed in the OP's question are exclusively events of the DOM API based on the Event interface. It was stressed in comments that one cannot generalize over arbitrary APIs nor even over different cases within one API. Nevertheless there are certainly common reasons shared by many different use cases for either triggering an event in a setter or not. This doesn't mean that there won't be many exceptions to a rule of thumb.

    Main reason(s) for NOT triggering an event in a setter The first example of the question for this group ('change'-events of <input>-tags, <select>-tags and <textarea>-tags) happens to have an explicit reasoning in the specs:

    These events are not fired in response to changes made to the values of form controls by scripts. (This is to make it easier to update the values of form controls in response to the user manipulating the controls, without having to then filter out the script's own changes to avoid an infinite loop.)

    This is similar to what was suspected in the OP's question. The same reasoning applies to all kind of synchronization between different objects. So therefore the other group should have more important reasons to overrule this one.

    Main reason(s) for triggering an event in a setter Maybe future edits will provide some official reference here. Clearly setters whose setting is not immediate and might even fail, do need to trigger an event, in order for the programmer to obtain the information if and when the requested change indeed happened. Not all of the examples for this group in the OP's question are of this kind. In particular for location.hash the event seems unnecessary.

    Note that for all the original examples of this group, even those where a setting seems immediately possible (like location.hash), the setters do not dispatch the event immediately in the same call stack (this was found only empirically with Firefox 104 and Chrome 105, therefore references for non-obvious cases like hash would be appreciated). Thus the program flow in

    object.property = newValue;
    doNextThing();
    

    will not be interrupted by the event handler. (It would, if the setter would directly, i.e. without timeout, call object.dispatchEvent(new Event('propertychange'))). The programmatic change in hash does however immediately dispatch a 'popstate' event (which otherwise was considered to belong to the other group).

    In spite of the above fact, even for maybe non-succeeding setter-calls like video.currentTime = newTime or video.play() the new value of the corresponding property is set immediately, meaning console.log(video.currentTime) and console.log(video.paused) directly after the setting will already give the new values. This can sometimes be long before the corresponding event (again found empirically). A notable exception (for Firefox, not for Chrome) seems to be window.resizeTo(), where for Firefox the new value is set only after succesfull resizing and shortly before the event.

    This would have allowed in principle also for this group a behavior where the setter itself doesn't trigger any event, and dispatching an event is reserved for the case were the setting turns out to be impossible and is turned back at a later time. This is however not the way it is. It is therefore hard to give a convincing reasoning why this second group has to behave differently than the first.

    Edit: in the html specs about navigating there is a statement about "web-developer sadness" related to the behavior of the popstate event in contrast to the hashchange event. This links to a new history-event proposal containing reasons of why emitting an event in the setter is useful.

    The following lists of examples contain the examples of the OP's question, but in addition refer to the official references and ideally will be growing over time to maybe provide a useful overview in the future.1

    Examples of setters that do NOT trigger a corresponding change event

    Events of this group are only fired with user-interaction. If no explicit setter is mentioned, the implicit setter of the property is meant.

    type of object property explicit setter event remark
    <input>-tags, <select>-tags and <textarea>-tags value 'change' or 'input'2 specs for the input-element are most specific about the reason
    history state pushState() 'popstate' "Fired at the Window when the user navigates the session history" (specs)

    Examples of setters that DO trigger a corresponding change event:

    Events of this group are fired even when the value of the property is set programmatically. If no explicit setter is mentioned, the implicit setter of the property is meant.

    type of object property explicit setter event remark
    Window instances outerWidth and outerHeight resizeTo(x,y) 'resize' --
    document.location hash 'hashchange' the specs of Location have an interesting warning
    <video> and <audio>-tags currentTime 'timeupdate' specs
    -"- volume and muted 'volumechange'
    -"- playbackRate 'ratechange'
    -"- paused pause() and play() 'pause' and 'play'
    <input>-tags and <textarea>-tags selectionStart and selectionEnd 'selectionchange' this is still an experimental feature, but notable, as it is on the same elements as the major examples for the non-triggering group

    [1] The idea is not to get an exhaustive list - probably almost every event can be seen as a change-event for some property - but to have a sufficiently large list to cover common use cases and see if they are explained by the reasoning given so far. One can search for events that are in a broad sense change events e.g. in this or this list of events. When there is only one reference for a particular reference given in the table, it is usually to MDN, because from there one easily gets to the specs, but not always the other way round.

    [2] The MDN page for input' events is a bit misleading, stating only

    The input event fires when the value of an <input>, <select>, or <textarea> element has been changed

    not making clear that this change has to be user-induced. In the specs for this event it is instead clear

    Fired when the user changes the contenteditable element's content, or the form control's value

    and in the specs for the <input> element, it is even stressed that it is not fired when set programmatically, as cited already above in the "reasons"-section.