This is an archived snapshot of W3C's public bugzilla bug tracker, decommissioned in April 2019. Please see the home page for more details.

Bug 21969 - [Custom]: add attributeChangedCallback to the set of prototype callbacks
Summary: [Custom]: add attributeChangedCallback to the set of prototype callbacks
Status: RESOLVED FIXED
Alias: None
Product: WebAppsWG
Classification: Unclassified
Component: HISTORICAL - Component Model (show other bugs)
Version: unspecified
Hardware: PC Windows NT
: P2 normal
Target Milestone: ---
Assignee: Dimitri Glazkov
QA Contact: public-webapps-bugzilla
URL:
Whiteboard:
Keywords:
: 22288 (view as bug list)
Depends on:
Blocks: 14968
  Show dependency treegraph
 
Reported: 2013-05-08 16:53 UTC by Scott Miles
Modified: 2013-06-25 20:14 UTC (History)
5 users (show)

See Also:


Attachments

Description Scott Miles 2013-05-08 16:53:50 UTC
It's particularly difficult to write per-element attribute change observers with MutationObserver, so it is suggested that Custom Elements spec support an attributeChangedCallback directly on element prototype.

I believe an asynchronous, end-of-micro-task timing model would be an ideal solution.
Comment 1 Olli Pettay 2013-05-08 16:59:37 UTC
I don't object adding such callback, but how is it difficult to write
per-element attribute change observer?

(new MutationObserver(callback)).observe({attributes: true});
Comment 2 Scott Miles 2013-05-08 17:04:44 UTC
You are correct of course, I'm sorry for the bad info.

I mentally crossed wires wrt inserted/removedCallback and attributeCallback. It's the former I would claim are difficult to write with MutationObserver.
Comment 3 Scott Miles 2013-05-08 17:19:31 UTC
Ok, must think more and write less.

Unless I am messing up again, the issue is that people have insisted that attributeChangedCallback must in fact be *synchronous*, so that custom elements can behave like regular elements that can react synchronously to attribute changes.

For this reason, MutationObserver doesn't help and we require platform support.

Sorry for the noise.
Comment 4 Olli Pettay 2013-05-08 17:27:34 UTC
sync mutation observing has the risk of bringing back the problems
mutation events have.
Comment 5 Scott Miles 2013-05-08 17:31:41 UTC
We are only observing attributes, so isn't there less potential trouble wrt manipulating trees?

In any case, my original sense was that an asynchronous model was ok, but the objection was that people expect attribute changes to be synchronous on native objects, and having custom elements behave differently was a footgun.
Comment 6 Olli Pettay 2013-05-08 18:31:58 UTC
( I could remember wrong, but aren't DOMAttrModified events disabled in webkit/blink because of security reasons. )
Comment 7 Rafael Weinstein 2013-06-06 16:40:36 UTC
So I agree with Olli here,

For attributeChanged, I think that (new MutationObserver(callback)).observe({attributes: true}); is pretty straightforward.

Ideally, we'd re-use existing primitives where possible, unless they fall down.

WRT the desire for sync behavior, remember that a MutationObserver can takeRecords() and thus *can* present a sync API boundary.

I realize that this is more difficult, but I believe that the concerns with Mutation Events apply to this case (as they do in general).

Imagine that you end up with an app with lots of custom elements and someone changes an attribute. It's not hard to imagine that that custom element will react to that change by changing another attribute on itself or another element.

Thus you get right back into the quagmire of

-Sync events potentially lying about what happened (by the time you hear about something, someone else has changed it more -- or un-done it)
-No event listener gets to act in insolation (he can be prempted by another listener)
-Event listeners having to implement brittle, terrible hacks to *ignore* the effects of their *own* changes (which is trivially accomplished with MutationObservers).

The siren song of sync mutation events calls once again, and we should avoid it's pitfalls if at all possible.
Comment 8 Rafael Weinstein 2013-06-06 17:16:58 UTC
After talking with Adamk, I think I'm now convinced that there is a judgement question about whether custom element callbacks should be sync by default or async by default.

One can *basically* be structured in terms of the other. In particular, let's imagine that all custom elements end up exposing new API and all those getters wish to present synchronous abstractions, thus they all call *takeRecords* before returning a value. This is effectively re-creating the behavior of pseudo-sync events.

Alternatively, given the existing of MutationObservers, if the events are sync by default, the callback can also choose to delay processing till the end of the microtask via a mutation observer.

So the question is probably a judgement call about whether custom elements *should* be generally presenting synchronous API boundaries or not.

Thanks, Adamk, for the thoughtful point.
Comment 9 Dominic Cooney 2013-06-07 00:01:34 UTC
I was talking to myself on bug 22288 about this issue, but let me join the party.

Given takeRecords, the motivation for this is not expanding basic capability but improving developer ergonomics.

Here is my proposal:

We go through DOM spec and HTML spec and come up with a set of tree-mutating operations and attributes:

prepend, append, before, after, replace, remove, insertBefore, appendChild, replaceChild, removeChild, setAttribute, setAttributeNS, removeAttribute, removeAttributeNS, innerHTML, textContent and reflected attributes

(Did I miss some? What about DOMTokenList/DOMSettableTokenList?)

Custom Elements legislates an additional step at the end of these to invoke lifecycle callbacks (without the microtask recursion flag check. You want consistent behavior when your script is running in a top-level script or in a lifecycle callback--imagine populating one custom element's shadow DOM with other custom elements--that will be happening in the readyCallback.)

Implementers want to avoid recreating Mutation Events. I believe that the three critical differences between Mutation Events to what I'm proposing, which avoids the problems with Mutation Events, are:

1. The callbacks are queued while the user agent is affecting changes to the DOM. They are not invoked synchronously on various callstacks deep in the implementation.

2. The callbacks are invoked when the user agent is about to run script anyway. Hence if we believe takeRecords is safe (we do) then this is also OK.

3. We're particularly hands-off when it comes to modifications by the parsing the document. Those happen at microtask checkpoint just like a Mutation Observeration. I'm not proposing any change wrt lifecycle callbacks and document parsing.

Through 1+2 this proposal avoids significant reentrancy. This was the main root cause of many problems with Mutation Events from the implementer's point of view. Through 3 it avoids running scripts in new places, which was another concern I heard from implementers.

There is a crude implementation of this in Blink. We have an IDL extended attribute we stick on methods that we want to do this extra lifecycle callback processing step before returning.
Comment 10 Dominic Cooney 2013-06-07 00:02:11 UTC
*** Bug 22288 has been marked as a duplicate of this bug. ***
Comment 11 Dominic Cooney 2013-06-07 00:24:41 UTC
One aspect that I would love to get input from web developers on is whether attributeChanged is special and only *that* needs this synchronous behavior, or if inserted, removed and ready should be synchronous too.

readyCallback is already synchronous some places--createElement, for example. In Blink setting innerHTML will call it synchronously too, and chatting with dglazkov he said this was the spirit of the spec although I think parsing it closely that is speced to be asynchronous right now.
Comment 12 Olli Pettay 2013-06-07 09:31:40 UTC
In Gecko we'd use internal ScriptRunners for stuff that needs to right after
some script-unsafe C++ code.

(I could note that it was especially some googlers who didn't want
MutationObserver callback to be called right after the mutation happened ;)  similarly to what is proposed here.
Some of the performance concerns are valid also in this case.)
Comment 13 Rafael Weinstein 2013-06-07 21:43:27 UTC
Dominic, Olli is correct that what you are proposing is similar to an option we discussed for mutation observers.

The danger with it is the same as it was for mutation observers. Think about it this way: the abstract danger is inviting "foreign" code to run that may invalidate your assumptions.

By delaying delivery until the DOM is finished, you've removed the danger for the (C++) DOM, but you've left page script still vulnerable.

The design of mutation observers has at its core two invariants:

-When an observer is invoked, he can be sure that no other "actor" is in the middle of making modifications
-When an observer wants to do further work on the DOM, he won't invite other observers to pre-empt him.

You're proposal does not provide either of these invariants.

I'm willing to be persuaded that the use here is fundamentally different and therefore should be allowed to be morepowerful (and thus dangerous), but I'm not really seeing anyone make the case that specific uses require it.

In particular, having talked in person with Scott & Steve, it's not obvious to me that more synchronous semantics are actually neccessary. Steve pointed out that you misintepretted the meaning of his jsbin example. The point wasn't that it was important that the callback was synchronous. The point was that the callback wasn't firing at all.
Comment 14 Dimitri Glazkov 2013-06-08 04:18:29 UTC
(In reply to comment #13)
 
> I'm willing to be persuaded that the use here is fundamentally different and
> therefore should be allowed to be morepowerful (and thus dangerous), but I'm
> not really seeing anyone make the case that specific uses require it.

One specific example that I remember us talking about is the input type attribute.
Comment 15 Scott Miles 2013-06-08 07:45:10 UTC
IMO, 'readyCallback' really needs to be synchronous (relative to imperative javscript instantiation anyway), otherwise you cannot simply make an element and use it.

For the rest, I'm really torn. 

I would be happy to simply keep most everything asynchronous, but my concern is that users will complain and developers will start working around it.

The working around it will be messy and will bring back the synchrony-dragons anyway.
Comment 16 Dominic Cooney 2013-06-09 23:59:21 UTC
(In reply to comment #13)
> Dominic, Olli is correct that what you are proposing is similar to an option
> we discussed for mutation observers.
> 
> The danger with it is the same as it was for mutation observers. Think about
> it this way: the abstract danger is inviting "foreign" code to run that may
> invalidate your assumptions.
> 
> By delaying delivery until the DOM is finished, you've removed the danger
> for the (C++) DOM, but you've left page script still vulnerable.

*I'm* a huge fan of this argument. Here I'm really trying to close the loop between implementers and web devs so I have something to go implement. But I may have misjudged the appetite for synchronous callbacks from web devs.

> The design of mutation observers has at its core two invariants:
> 
> -When an observer is invoked, he can be sure that no other "actor" is in the
> middle of making modifications
> -When an observer wants to do further work on the DOM, he won't invite other
> observers to pre-empt him.
> 
> You're proposal does not provide either of these invariants.

Just out of curiosity if you've seen infinite chains of callbacks of battlin' mutations in the wild?

> I'm willing to be persuaded that the use here is fundamentally different and
> therefore should be allowed to be morepowerful (and thus dangerous), but I'm
> not really seeing anyone make the case that specific uses require it.
> 
> In particular, having talked in person with Scott & Steve, it's not obvious
> to me that more synchronous semantics are actually neccessary. Steve pointed
> out that you misintepretted the meaning of his jsbin example. The point
> wasn't that it was important that the callback was synchronous. The point
> was that the callback wasn't firing at all.

If that's the case, that asynchronous is good--woohoo.

Dimitri has mentioned the "input type" use case. I think use cases like this boil down to two things:

1. If you just need to *appear* to have responded synchronously, how onerous is it to wrap methods and properties in things that check for changes to respond to? At one level that is just a pain (one more thing to do.) At another level it is tricky if you try to split up your element's reaction to state changes (eg try to work out which API depend on the "type" attribute versus which depend on the "disabled" attribute and process the minimal set of things you depend on.)

2. Are there times when you really need to respond synchronously, you will kick off some observable side-effect, it can't wait? I can't think of any use cases like this. If there was such a case it would justify the synchronous callback. Maybe a creative person can come up with one.

(In reply to comment #15)
> IMO, 'readyCallback' really needs to be synchronous (relative to imperative
> javscript instantiation anyway), otherwise you cannot simply make an element
> and use it.

You can. The prototype will be fixed up immediately, regardless of when the readyCallback is fired. So just like you could takeRecords to see if your attributes have changed, you could check some property on "this" to see if readyCallback had run and run it.

> For the rest, I'm really torn. 
> 
> I would be happy to simply keep most everything asynchronous, but my concern
> is that users will complain and developers will start working around it.
> 
> The working around it will be messy and will bring back the
> synchrony-dragons anyway.

I didn't realize there was apathy towards making this synchronous from web devs. In that case, let's implement the asynchronous one and get feedback from web devs. Possible outcomes:

1. Everyone ends up using MutationObservers (so that they can do takeRecords) and is happy => we kill the callbacks.

2. Everyone ends up using MutationObservers and is unhappy => we think about synchronous callbacks.

3. Everyone uses the callbacks and is happy => hammocks and beverages for everyone.
Comment 17 Scott Miles 2013-06-10 02:38:01 UTC
I think there has been a fairly dramatic miscommunication. :/

> 1. If you just need to *appear* to have responded synchronously

While I believe this is possible, it's completely onerous for the developer.

> 2. Are there times when you really need to respond synchronously

Not in terms of achieving correctness, but yes in terms of user ergonomics.

> I didn't realize there was apathy towards making this synchronous from web devs.

The is the opposite of what I tried to say. I believe it would *theoretically* be possible to have a world where all these effects are asynchronous, and force web developers to adapt, but the expect the reality of the situation is that there would be a tremendous hew and cry and effigy-burning. 

> You can. The prototype will be fixed up immediately, regardless of when the readyCallback is fired. So just like you could takeRecords to see if your attributes have changed, you could check some property on "this" to see if readyCallback had run and run it.

That's the opposite of 'you can'. A user is not going to do those steps, and the only way to make the component do it is to gate all the accessors and have an 'ready before ready' fixup step, which is a non-starter. To require something like this is to completely gum-up the component lifecycle.

> 1. Everyone ends up using MutationObservers

I don't understand this bit. Unless you simply do not provide callbacks, why would anybody use MutationObservers to do the same work?
Comment 18 Dominic Cooney 2013-06-11 01:45:46 UTC
(In reply to comment #17) 
> > I didn't realize there was apathy towards making this synchronous from web devs.
> 
> The is the opposite of what I tried to say. I believe it would
> *theoretically* be possible to have a world where all these effects are
> asynchronous, and force web developers to adapt, but the expect the reality
> of the situation is that there would be a tremendous hew and cry and
> effigy-burning.

OK. I misinterpreted the "really torn" part. It seems like there's confusion, see Comment 13, "In particular, having talked in person with Scott & Steve, it's not obvious to me that more synchronous semantics are actually neccessary. (sic)"

Raf, Olli, are you convinced?

> > You can. The prototype will be fixed up immediately, regardless of when the readyCallback is fired. So just like you could takeRecords to see if your attributes have changed, you could check some property on "this" to see if readyCallback had run and run it.
> 
> That's the opposite of 'you can'. A user is not going to do those steps, and
> the only way to make the component do it is to gate all the accessors and
> have an 'ready before ready' fixup step, which is a non-starter. To require
> something like this is to completely gum-up the component lifecycle.

I have a sinking fear that if you want to be really robust you will need to do that anyway. If you have a dozen custom elements in the tree, we're going to fire that callback one at a time. Therein lies opportunity for misadventure.

> > 1. Everyone ends up using MutationObservers
> 
> I don't understand this bit. Unless you simply do not provide callbacks, why
> would anybody use MutationObservers to do the same work?

Because if the callbacks are asynchronous, but what web devs really need is to appear synchronous, they are going to switch to MutationObservers so that they can call takeRecords to eagerly grab and process the mutations they need to respond to.

This is down in the weeds, but do you think readyCallback and attributeChanged need to be synchronous, but insertedCallback and removedCallback could be asynchronous?
Comment 19 Scott Miles 2013-06-11 02:01:17 UTC
re: "[talking to Scott] it's not obvious ... that more synchronous semantics are actually necessary"

Yes, I agree that my stance was confusing. Unfortunately, I've been trying to thread the needle by agreeing that fully asynchronous semantics are sane while suggesting nobody will like it.

I'm more inclined now to take a harder line and just say that `readyCallback` and `attributeCallbackChanged` must be synchronous (to JavaScript, not to any internal process).

I'm way less concerned about `inserted|removedCallback`, which IMO can simply be asynchronous.

Sorry for the confusion about MutationObservers; since I think of that API as the vanguard for asynchrony, the notion that it would be the answer for synchrony eluded my consciousness. :P

Thank you as always for your patience.
Comment 20 Rafael Weinstein 2013-06-12 00:04:04 UTC
I still want to get my head around the inserted/removed callback use cases, but yes: I am now convinced that ready & attributeChanged should be "as synchronous as possible".

Here are the arguments which convinced me:

-readyCallback is really a stand in for a constructor. We accepted the constraint that the parser can't synchronously call custom element constructors, but the requirement is an implementation constraint not a design constraint. It's simply unreasonable to take away the invariant that the constructor is the first method invoked on an instance of a class.

-While it's theoretically possible that readyCallback and attributeChanged can be fully asynchronous and all accessors start by ensuring the element is initialized and any dependent attribute changes are flushed -- This model of custom elements is already (almost) entirely implementable using Mutation Observers. In other words, if we thought this programming model was reasonable, there would be no need for a custom element mechanism. The very fact that we are discussing a mechanism which allows page script to implement DOM elements implies that those elements implementation model should be *roughly* similar to that of the native elements (IOW, largely synchronous).
Comment 21 Dimitri Glazkov 2013-06-20 22:30:44 UTC
https://dvcs.w3.org/hg/webcomponents/rev/3cd85d5fcf0c

I probably did it wrong, but it's a start :)

WDYT?
Comment 22 Dominic Cooney 2013-06-21 08:17:36 UTC
(In reply to comment #21)
> https://dvcs.w3.org/hg/webcomponents/rev/3cd85d5fcf0c
> 
> I probably did it wrong, but it's a start :)
> 
> WDYT?

LGTM. I like the part about calling it synchronously when set in certain ways.