Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The is="" attribute is confusing? Maybe we should encourage only ES6 class-based extension. #509

Closed
trusktr opened this issue May 31, 2016 · 533 comments

Comments

@trusktr
Copy link

trusktr commented May 31, 2016

The is="" API can be confusing, and awkward. For example (in current v1 API):

inheritance currently has to be specified three times:

class MyButton extends HTMLButtonElement {} // 1st time
customElements.define('my-button', MyButton, { extends: 'button' }) // 2nd time
<button is="my-button"></button> <!-- 3rd time -->

But, I believe it could be better:

inheritance specified once, easier non-awkward API:

class MyButton extends HTMLButtonElement {} // 1st and only time
customElements.define('my-button', MyButton)
<my-button></my-button>

But, there's problems that the is="" attribute solves, like making it easy to specify that a <tr> element is actually a my-row element, without tripping up legacy parser behavior (among other problems). So, after discussing in this issue, I've implemented an idea in #727:

mixin-like "element behaviors", without extending builtins

The idea uses a has="" attribute to apply more than one behavior/functionality to an element (is="" can only apply a single behavior/functionality to an element), using lifecycle methods similar to Custom Elements.

For example:

// define behaviors
elementBehaviors.define('foo', class { // no inheritance necessary
  connectedCallback(el) { ... }
  disconnectedCallback(el) { ... }
  static get observedAttributes() { return ['some-attribute'] }
  attributeChangedCallback(el, attr, oldVal, newVal) { ... }
})
elementBehaviors.define('bar', class { ... })
elementBehaviors.define('baz', class { ... })
<!-- apply any number of behaviors to any number of elements: -->
<div has="bar baz"></div>
<table>
  <tr has="foo baz" some-attribute="lorem"></tr> <!-- yay! -->
</table>
<button has="bar foo" some-attribute="ipsum"></button>
<input has="foo bar baz" some-attribute="dolor"></input>

In a variety of cases, this has advantages over Custom Elements (with and without is="" and):

  1. No problems with parsing (f.e. the table/tr example)
  2. No inheritance, just simple classes
  3. Works alongside Custom Elements (even if they use is=""!)
  4. "element behaviors" is similar to "entity components" (but the word "component" is already used in Web Components and many other web frameworks, so "behaviors" was a better fit).
  5. Lastly, "element behaviors" makes it easier to augment existing HTML applications without having to change the tags in an HTML document.

See #727 for more details.


Original Post:

The spec says:

Trying to use a customized built-in element as an autonomous custom element will not work; that is, <plastic-button>Click me?</plastic-button> will simply create an HTMLElement with no special behaviour.

But, in my mind, that's just what the spec says, but not that it has to be that way. Why has it been decided for it to be that way?

I believe that we can specify this type of information in the customElement.define() call rather than in the markup. For example, that very same document shows this example:

customElements.define("plastic-button", PlasticButton, { extends: "button" });

Obviously, plastic-button extends button as defined right there in that call to define(), so why do we need a redundant is="" attribute to be applied onto button? I think the following HTML and JavaScript is much nicer:

<!-- before: <button is="plastic-button">Click me</button> -->
<plastic-button>Click me</plastic-button>
// before: const plasticButton = document.createElement("button", { is: "plastic-button" });
const plasticButton = document.createElement("plastic-button");

The necessary info for the HTML engine to upgrade that element properly is right there, in { extends: "button" }. I do believe there's some way to make this work (as there always is with software, because we make it, it is ours, we make software do what we want, and this is absolutely true without me having to read a single line of browser source code to know it), and that is="" is not required and can be completely removed from the spec because it seems like a hack to some limitation that can be solved somehow else (I don't know what that "somehow" is specifically, but I do know that the "somehow" exists because there's always some way to modify software to make it behave how we want within the limitations of current hardware where current hardware is not imposing any limitation on this feature).

@domenic
Copy link
Collaborator

domenic commented May 31, 2016

This was previously discussed at length in the other bug tracker, and it's explained in the spec in the sentence above that:

Customized built-in elements require a distinct syntax from autonomous custom elements because user agents and other software key off an element's local name in order to identify the element's semantics and behaviour. That is, the concept of customized built-in elements building on top of existing behaviour depends crucially on the extended elements retaining their original local name.

@trusktr
Copy link
Author

trusktr commented May 31, 2016

@domenic That quote that you referred to makes me think exactly the same thing as I just wrote:

The necessary info for the HTML engine to upgrade that element properly is right there, in { extends: "button" }. I do believe there's some way to make this work (as there always is with software, because we make it, it is ours, we make software do what we want, and this is absolutely true without me having to read a single line of browser source code to know it), and that is="" is not required and can be completely removed from the spec because it seems like a hack to some limitation that can be solved somehow else (I don't know what that "somehow" is specifically, but I do know that the "somehow" exists because there's always some way to modify software to make it behave how we want within the limitations of current hardware where current hardware is not imposing any limitation on this feature).

You're pointing to something that exists in a spec, but I'm saying it doesn't have to be that way, specs can be modified. There is a solution (I don't know what that solution is specifically yet, but I know that the hardware on any computer that runs a browser nowadays isn't a limitation, and the only limitation is in the software we write, which can be modified). I simply can't understand why an HTML engine must need <button is="super-button"> (besides the fact that current algorithms require that) when clearly there's a JavaScript way to make the HTML engine understand that <super-button> means the same as <button is="super-button"> due to the fact we're defining that in the call to customElement.define.

I wish I had the time and resources to read the Chromium source in order to point to an actual solution (someone else can write the spec, as I'm not even good at reading those), but in the meantime, I'm 100% sure a backwards-compatible programmatic solution exists.

@domenic
Copy link
Collaborator

domenic commented May 31, 2016

Yes, everything is Turing machines and everything can be implemented. However, there are real-world constraints---both in implementation complexity and in ecosystem impact---that make it not desirable.

@trusktr
Copy link
Author

trusktr commented May 31, 2016

Some things are complex to implement, but this particular change doesn't seem to be. :]

Would you mind kindly pointing to the complexity and impact that make it non-desirable?

Is it because if the element isn't registered (i.e. with JS turned off) that the browser won't know to at least render a fallback <button>? I think that's perfectly fine, because without JS the app probably won't work anyways, and I'm willing to bet that most people who will write JavaScript-based Custom Elements won't care if the app doesn't work with JS turned off. The Nike website doesn't work with JS turned off, and they care. Most people won't care. Developers who do care can just as easily use <noscript> and place a <button> in there, which makes much more semantic sense anyway.

What other reason might there be?

@trusktr
Copy link
Author

trusktr commented May 31, 2016

When someone who comes from React tries to learn Custom Elements for the first time (which is more likely to happen than someone learning Custom Elements first due to the mere fact that specs are hard to read compared to things like React docs, unfortunately, because specs are meant for implementers, and the actual usage docs if any are sometimes limited or hard to find), they'll face these weird things like the redundant is="" form of extension and globally-registered custom elements that are not registered on a per-component basis.

This is what classes are for, and in the case of the HTML engine where multiple types of elements may share the same interface (which is probably a partial cause to any current problems) the third argument to customElements.define is there to clarify things. The is="" attribute is simply redundant and out of place from an outter API perspective (which may matter more than whatever extra implementation complexity there may be) because we already specify the extension in the define() call.

I'd take this further and propose that every single built in element should have a single unique associated class, and therefore ES2015 class extension would be the only required form of extension definition.

Just in case, let me point out we must currently specify an extension three whole times. For example, to extend a button:

class MyButton extends HTMLButtonElement {} // 1
customElements.define('my-button', MyButton, { extends: 'button' }) // 2
<button is="my-button"></button> <!-- 3 -->

That's three times we've needed to specify that we're extending something, just to extend one thing. Doesn't this strike you as a bad idea from an end-user perspective?

It should ideally just be this, assuming that there is a one-to-one relationship between built-in elements and JS classes:

class MyButton extends HTMLButtonElement {} // 1
customElements.define('my-button', MyButton)
<my-button></my-button>

I get what you're saying about complexities and real-world world constraints, but I think "breaking the web" might actually fix the web in the long run. There can be an API cleansing, and websites may have to specify that the new breaking-API is to be used by supplying an HTTP header. Previous behavior can be supported for a matter of years and finally dropped, the new API becoming default at that time. Sites that don't update within a matter of years probably aren't cared for anyways.

I'm just arguing for a better web overall, and breaking changes are sometimes necessary. It seems I may be arguing for this forever though. Unless I am an employee at a browser vendor, I'm not sure my end-user viewpoints matter much. The least I can do is share them.

@rniwa
Copy link
Collaborator

rniwa commented Jun 1, 2016

The problem here is that UA internally checks local name e.g. element.localName == aTag to do various things, and all of that code needs to be updated to account for the fact there could be an intense of HTMLAnchorElement whose local name is not a.

Now I'm going to re-iterate that Apple objects to extending subclasses of HTMLElement using is= as currently spec'ed for various reasons we've stated in the past and this feature won't be supported in WebKit.

@trusktr
Copy link
Author

trusktr commented Jun 7, 2016

The problem here is that UA internally checks local name e.g. element.localName == aTag to do various things, and all of that code needs to be updated to account for the fact there could be an intense of HTMLAnchorElement whose local name is not a.

I think that relying on element tag names might be a bad idea, especially if we give users ability to scope elements on a per-shadow-root basis (now that's a good idea!). If we can allow end users of a custom element to assign any name they wish to that custom element used within the user's component (within the user's component's inner shadow tree), then we can avoid the global mess that window.customElements will be when an app gets large and when people widely adopt Web Components (wide adoption is one of the goals here).

In React (by contrast) the creator of a component never decides what the name of that component will be in the end user's inner tree. The end user has full control over the name of an imported component thanks to ES6 Modules. We can encourage a similar concept by introducing a per-shadow-root custom element registration feature.

If we can map names to classes without collision and without two names assigned to the same class (i.e. without elements like <q> and <blockquote> sharing the same class), then we should be able to use instanceof instead of checking tag names, and element inheritance can be based solely on ES6 class extension, as it should be (rather than having the extra is="" feature). The change that would be needed here would be to ensure that the native elements map to separate leaf classes so it's possible to tell a <q> element apart from a <blockquote> element with instanceof and therefore encouraging that sort of checking over tag names.

This API is still v0 in practice, so I think there's room for modification even if v1 is on the verge of coming out.

Apple objects to extending subclasses of HTMLElement using is=

I think Webkit not implementing is="" may be a great thing.

If we continue with globally registered elements, it will be inevitable that some two components in an app will have a name collision, and that the developer of that app will attempt to fix the problem by modifying the source of one custom element (possibly requiring forking of an NPM module, pushing to a new git repo, publishing a new module, etc, time consuming things) just to change the name of that element; it'd be a pain and can introduce unexpected errors if parts of the modified code are relying on the old tag names.

TLDR, if we put more care into the JavaScript aspects of an element (f.e. choosing which class represents an element, which is already happening in v1) then we'll have a more solid Web Component design, and we can drop is="". It will also require fixing things like the <q>/<blockquote> problem.

@trusktr
Copy link
Author

trusktr commented Jun 7, 2016

@domenic Can you describe the "ecosystem impact" that you might know of?

@domenic
Copy link
Collaborator

domenic commented Jun 7, 2016

I am referring to the various tools which key on local name, including JavaScript frameworks, HTML preprocessors and postprocessors, HTML conformance checkers, and so on.

@trusktr
Copy link
Author

trusktr commented Jun 7, 2016

I think I know what you mean. For example, search engines read the tag names from the markup in order to detect what type of content and how to render it in the search results. This point may be moot because today it completely possible to use a root <app> component with a closed shadow root. In this case a search engine might only ever see

<html>
    <head>
        <title>foo</title>
    </head>
    <body>
        <app></app>
    </body>
</html>

That's functionally equivalent to an app with no shadow roots and made entirely of randomly named elements:

<html>
    <head>
        <title>foo</title>
    </head>
    <body>
        <asf>
            <oiur>
                ...
            </oiur>
            <urvcc>
                ...
            </urvcc>
        </asf>
    </body>
</html>

Based on that, I don't think it's necessary to keep element names.

I imagine a couple remedies:

  1. Allowing both forms and let users decide which to use: <button is="awesome-button"> or <awesome-button> and let users decide which to use as they so wish.
  2. Allowing overriding of native elements (which pairs well with the idea of per-shadow-root element registrations as in "hey, let me define what a <p> element is within my component that has no paragraphs."). Then <button> could behave like <awesome-button>, and search engines or other tools would be able to receive the same semantic meaning. This idea only works if the elements are in Light DOM.
@domenic
Copy link
Collaborator

domenic commented Jun 7, 2016

Remedy 1 is what we have consensus on.

@rniwa
Copy link
Collaborator

rniwa commented Jun 7, 2016

I'll note that we've vocally and repeatedly objected to having is=, and in fact, stated publicly that we won't implement this feature, but somehow the WG kept it in the spec. I wouldn't call that a consensus. We extremely reluctantly agreed to keep it in the spec until it gets removed at risk later.

@zamfofex
Copy link

It’s important to note that a lot of elements are implemented partially (or completely) with CSS. So, in order to support this and allow user agents to continue to implement elements like they do today, a new CSS pseudo-class would be needed: :subtypeof(button)

@chaals
Copy link
Contributor

chaals commented Jun 11, 2016

a new CSS pseudo-class would be needed: :subtypeof(button)

Not really. The point of the is= approach is that since you have a button, that already matches CSS selectors for button. If you want to match a specific subtype you can do button[is=broken-button]

@zamfofex
Copy link

@chaals right. You would need the pseudo-class if using a custom element that extends a native button could be done like <awesome-button> instead of <button is="awesome-button">.

@zamfofex
Copy link

Also, like @trusktr said, if this and whatwg/html#896 become a thing, I think it’ll be possible to remove altogether the extends property from the options argument. We would then write customElements.define("awesome-button", class extends HTMLButtonElement{}, {}), which I really like.

Offering this as an alternative to the current way of defining extending native elements — allowing user agent developers and authors to choose which they want to use — is the best solution in my opinion. As @trusktr said, “I think ‘breaking the web’ might actually fix the web in the long run”.

@chaals
Copy link
Contributor

chaals commented Jun 11, 2016

(@Zambonifofex it's generally not clear to me what you mean when you say "this", which makes it hard to engage)

As @trusktr said, “I think ‘breaking the web’ might actually fix the web in the long run”.

I've tried breaking the Web, at the turn of the century when it was smaller, to do something that would have been really useful. It failed. For pretty much the reasons given by people who say "you can't break the web".

Although user agent developers are less reluctant to break things than they were a decade ago, they're still very reluctant, and I believe with very good reason.

I think a strategy reliant on breaking the Web to fix it is generally a non-starter, even if the alternatives are far more painful.

@chaals
Copy link
Contributor

chaals commented Jun 11, 2016

For what it's worth, I agree with @rniwa's characterisation - we have an agreement that we won't write is= out right now, but I don't see a strong consensus behind it. (Although as an individual I'm one of those who think it's pretty much indispensable to making this stuff work).

@zamfofex
Copy link

@chaals

(@Zambonifofex it's generally not clear to me what you mean when you say "this", which makes it hard to engage)

When I say “this” I mean “this issue”; the suggestion made by @trusktr in the first comment of this thread.

But, honestly, — not that I’d be able to know, as I’ve never touched any browser’s code — I think that this would be a much easier change to make for user agent developers than you guys are making it out to be.

@zamfofex
Copy link

zamfofex commented Jun 12, 2016

I mean, sure, it could be slightly annoying to go and change all of the checks for the tag name to something else, but would it be actually hard?

@trusktr
Copy link
Author

trusktr commented Jul 2, 2016

The is="" API can be confusing. For example (in current v0 API):

<my-el></my-el>
class MyEl extends HTMLElement { /*... */ }
MyEl.extends = 'div'
document.registerElement('my-el', MyEl)

// ...

document.querySelector('my-el') instanceof MyEl // true or false?
@domenic
Copy link
Collaborator

domenic commented Jul 2, 2016

That API does not reflect the current custom elements API.

@trusktr
Copy link
Author

trusktr commented Jul 2, 2016

We should make the result of instanceof be obvious. I think this could be better in v1 (without is="" and without options.extends):

<my-el></my-el>
class MyEl extends HTMLDivElement { /*... */ }
customElements.define('my-el', MyEl)

// ...

document.querySelector('my-el') instanceof MyEl // true!
@trusktr
Copy link
Author

trusktr commented Jul 2, 2016

I forgot this line too, in the previous comment:

document.querySelector('my-el') instanceof HTMLDivElement // also true!
@trusktr
Copy link
Author

trusktr commented Jul 2, 2016

(@domenic I'm still using v0 in Chrome)

@trusktr
Copy link
Author

trusktr commented Jul 3, 2016

@rniwa

there could be an intense of HTMLAnchorElement whose local name is not a.

I think you meant "instance" instead of "intense"? This seems to be the reason why is="" exists, making it possible to create new elements that extend from elements that inherit from the same class, but that possibly have new behavior added to them in their definitions along with differing nodeNames. This is also the reason for the {extends: "element-name"} (I am calling it options.extends), which effectively accomplishes the same thing. I feel like this may be an anti-pattern considering that we now have ES6 classes for describing extension.

I am against both of those methods of defining extension, as I believe encouraging ES6 Classes as the the tool for defining extension will reduce room for confusion.

@trusktr trusktr changed the title Why must the is="" attribute exist? Jul 3, 2016
@trusktr
Copy link
Author

trusktr commented Jul 3, 2016

Perhaps existing native elements should be re-worked so that any differences they have should rather be described (explained) by using class extension, and not using those other two methods (is="" and options.extends). An example of native elements that are defined with the same interface/class but with different nodeNames are q and blockquote which share the HTMLQuoteElement interface. We should attempt to describe the difference (if any) between these elements with class extension rather than is="" combined with options.extends. If there is no difference except for nodeName, then if they are both instanceof the same class that is fine too as long as the behavior for both elements is entirely defined in the shared class, and not by other means.

@trusktr
Copy link
Author

trusktr commented Mar 22, 2021

I've been using element-behaviors for quite a while now. It works great as a replacement of is="". It works perfectly fine on tables and their content.

<table has="sortable-table filterable-table">
  <tr has="something-special">...</tr>
</table>
@WebReflection
Copy link

WebReflection commented Mar 23, 2021

@trusktr the idea is not terrible but it raised a few concerns:

  • the fact the API mimics CE but needs a suffix parameter as element is a bit yack
  • accessors (get/set behavior properties) can't access the element (target), unless such target is assigned in the constructor once
  • because of the previous 2 points, I'd rather vote for this.target as getter so that it can be accessed at any time in the behavior
  • while I initially thought it'd be confusing for developers to distinguish a behavior from a CE, the fact the definition doesn't extend HTMLElement, or any other, helps disambiguating that, but as soon as one behavior extends a base behavior, it's easy to think that class definition is for a CE, not a behavior
  • as this is based on MutationObserver, I guess everything is inevitably asynchronous, which leads to unexpected sequence of events (the CE polyfill with MO has the same gotcha more than a developer felt with)
  • because of the previous point, I wonder if mimic the CE API is, actually, a good idea at all, as in projects such as wickedElements or hookedElements these kind of expectations are less common ...

My last points regard the fact that connectedCallback might be called when disconnectedCallback also happened (quick add node and remove it before MO triggers, connected is called when the element is not live) ... so that expectations, compared to Custom Elements that have synchronous invokes, might need to be well explained.

However, this last point is more about a polyfill limitation, not exactly how behaviors should be defined by specs.

P.S. this is exactly what wickedElements has been for years, except the definition is via CSS selector, not specific attribute

P.S.2 ... imho, customBehaviors looks like a better name, if it wants to be similar to customElements in terms of API

@WebReflection
Copy link

WebReflection commented Mar 23, 2021

@trusktr more concerns while quickly implementing an alternative/proposal to experiment with:

  • the disconnectedCallback called when the has attribute changes and the behavior is removed is misleading, as it's not about the element being disconnected but about the behavior being disconnected. Unless the intention is indeed to simply have connected and disconnected run only when the behavior is added or removed, I find this part hard to reason about.
  • similarly, the connectedCallback. It's not clear if it should be called when the element attribute changes (i.e. via element.behaviors.add('behavior')) even if the element is offline.
  • if these callbacks are bound to the element lifecycle, the disconnectedCallback for cleanup sounds footgun proof, because when a behavior is removed (i.e. via behaviors.remove('behavior')), but the element is still live, it looks like we're simulating a destructor instead, not a disconnect.
  • it's not clear if offline elements will work as expected (at least with a MO that observes only live nodes)
  • it's not clear if trashing layout would invoke disconnected (but it'd expect so)
  • it's not clear if injecting in templates via html would connect behaviors, or only once document.importNode(template.content, true) happens
  • it's not clear if adding and removing the same behavior would use the same instance or create a new one

Thanks for clarifications.


edit

This is how I see this API proposal more useful, all optional except the target:

class CustomBehavior {
  get target() {
    // the element associated with this behavior: read-only
    // returns `null` after `detachedCallback` happens
  }
  // same as CE ... all optional
  static get observedAttributes() { return []; }
  attachedCallback() {
    // as opposite of constructor, unless there's a destructor
    // invoked once like a constructor would be
    // `this.target` is already available (even if it was a constructor)
  }
  attributeChangedCallback(name, oldValue, newValue) {
    // invoked surely after attachedCallback/constructor
    // if the attribute is one of the observed one
  }
  connectedCallback() {
    // invoked surely after attributeChangedCallback the very first time if live
    // invoked every time the element goes live again
  }
  disconnectedCallback() {
    // invoked only if the element goes offline
    // NOT INVOKED if the behavior is removed
  }
  detachedCallback() {
    // or either we have destructor, in case we have a constructor
    // invoked once and `this.target` will returns `null` right after
    // the behavior is "dead" at the end of this callback, and if
    // its name will be added back, a new instance will be used
  }
}

With such base class all behaviors can extend it and inherits this.target accessor which is an implementation detail.

This allows behaviors to initialize their listeners when attached, and cleanup once detached, but these operations are not necessarily bound with the element live state, simplifying setup/teardown per each behavior, less unexpected when disconnectedCallback is called, but the element is still live in the DOM (which is the most common case here, when behaviors are dropped).

All names are subject to changes, but I think this proposal would be much cleaner than your one, and simply because, differently from Custom Elements, behaviors can be added and removed at runtime, while custom elements are forever, and strictly bound with the element these represent.

I hope this sounds better to you too.

P.S. alternatively, disconnectedCallback might use a detached = false parameter ... but this distinction between being removed, as behavior, or signaling the element was disconnected, looks kinda important to me, example:

class CustomBehavior {
  get target() {}
}

customBehaviors.define('foo', class extends CustomBehavior {
  static get observedAttributes() { return ['any']; }
  constructor() {
    setup(this.target);
  }
  attributeChangedCallback(name, oldValue, newValue) {}
  connectedCallback() {}
  disconnectedCallback(detached = false) {
    if (detached)
      teardown(this.target);
  }
});
@trusktr
Copy link
Author

trusktr commented Mar 24, 2021

the fact the API mimics CE but needs a suffix parameter as element is a bit yack

Now sure what you mean by that. Are you talking about constructor receiving the element arg? If so, more on that below regarding the this.target idea. Passing it in via constructor does mean users have to assign it to their own (possibly #private) property to use it in other methods.

There's only one issue with the this.target idea: how does the engine provide the value if the classes don't extend from a base class? The engine would have either

  • construct the behavior instance, then assign the value, which means it can't be used in the constructor (that's originally how I had it, but then I wanted to use the elements in the behavior constructors).
  • or modify the class prototype, which means that after passing the class to elementBehaviors.define, the class shape would be different than what the class definition shows (so not good for static analysis).
  • or provide a base Behavior class, then users would be required to extend from it, and the base class would have target as part of its shape.
    • EDIT: Oh, is this what you meant about class CustomBehavior {? That this base class in built in and all users are required to extend from it? If so, then yeah that would work to prevent the constructor arg.

as soon as one behavior extends a base behavior, it's easy to think that class definition is for a CE, not a behavior

True. I see the ambiguity there. A developer should be responsible about knowing what their class does though...

as this is based on MutationObserver, I guess everything is inevitably asynchronous, which leads to unexpected sequence of events (the CE polyfill with MO has the same gotcha more than a developer felt with)

True. If it were to become a spec'd feature, that could be changed to be sync during parse like with CEs.

My last points regard the fact that connectedCallback might be called when disconnectedCallback also happened

That can't happen. If an element is disconnected already, connectedCallback will not be called. The sequence can only ever be connectedCallback -> disconnectedCallback -> connectedCallback -> disconnectedCallback -> etc. If that's not the case, it would be a bug.

this is exactly what wickedElements has been for years, except the definition is via CSS selector, not specific attribute

That's also an interesting approach. In some ways it is like jQuery (instantiating plugins via selectors).

customBehaviors looks like a better name, if it wants to be similar to customElements in terms of API

I thought about that, but there's no already-existing built-in behaviors, so they aren't really "custom".

the disconnectedCallback called when the has attribute changes and the behavior is removed is misleading, as it's not about the element being disconnected but about the behavior being disconnected. Unless the intention is indeed to simply have connected and disconnected run only when the behavior is added or removed, I find this part hard to reason about.

I thought about this too. Basically it's more like a "when do I clean up?". Rather than having, for example, two separate methods, I just think of it this way: if an element is removed from the live DOM, or if the behavior is removed from an element, then effectively the behavior is disconnected from live DOM.

The behaviors (currently) can not be moved from one element to another. So even after being disconnected from DOM, this.element could still reference the owner element.

Also, not sure we'd want the behavior to still be "connected", because if it isn't in the DOM, it really shouldn't do anything (it should be cleaned up, just like the element was.

similarly, the connectedCallback. It's not clear if it should be called when the element attribute changes (i.e. via element.behaviors.add('behavior')) even if the element is offline.

Same thought here: is the behavior connected into the DOM? If so, then it is guaranteed to have been connected to an element, and it should initialize things at that time. The main thoughts are "when to initialize" and "when to cleanup".

Could there be a different name for those two methods?

if these callbacks are bound to the element lifecycle, the disconnectedCallback for cleanup sounds footgun proof, because when a behavior is removed (i.e. via behaviors.remove('behavior')), but the element is still live, it looks like we're simulating a destructor instead, not a disconnect.

The "destructor" is basically what I was going for. The "cleanup" is not necessarily tied to the element's life cycle exactly. F.e. remove behavior -> behavior cleanup, or remove element -> behavior cleanup.

Suppose instead elementDisconnectedCallback is only for when the element is disconnected, and then we have a new method like behaviorDisconnectedCallback for when a behavior is removed from an element (and similar with elementConnectedCallback and behaviorConnectedCallback). During parse, both connected methods would fire (so somewhat redundant). What if an element is removed, but the behavior is not, and it didn't clean up on element disconnect?

Would this be better? Maybe people would be likely to do things wrong compared to just "initialize" and "cleanup".

My inclination is that I will most likely just have both of the disconnect call a third custom cleanup method.

What about initializeCallback and cleanupCallback? Even with those names, the documentation would still need to describe how it works (initialization happens on element or behavior connect, and clean up happens on element or behavior disconnect).

it's not clear if offline elements will work as expected (at least with a MO that observes only live nodes)

Currently if the element is disconnected, the behavior was cleaned up (disconnectedCallback) and so nothing should run (ideally). Not sure if attributeChangedCallback should continue to run. I haven't relied on it running after a behavior is cleaned up (disconnected), so I don't even know if it does or not currently.

it's not clear if trashing layout would invoke disconnected (but it'd expect so)

What do you mean? Does trashing layout mean getting rid of a subtree? If so, then all behaviors would have disconnectedCallback ran.

it's not clear if injecting in templates via html would connect behaviors, or only once document.importNode(template.content, true) happens

Do you mean with innerHTML, for example? If so, then it should connected them. Any time an element exists with a has="" attribute present for whatever reason, the behaviors needs to be instantiated if they have been defined. If that doesn't happen, there's a bug.

Or do you mean behaviors on elements that are inside a <template>? I've never tried that, so not sure what will happen currently. But it should be the same as CEs: behaviors should not be instantiated (just like elements are not upgraded) until they are in a real DOM.

EDIT: I just did a test, and it appears if elements are in a <template> nothing happens (good).

it's not clear if adding and removing the same behavior would use the same instance or create a new one

It just creates a new one currently. Actually exposing behaviors on the element is not good at the moment, because modifying that Set will not do anything (I simply placed it there so I could easily get an instance if I wanted to (f.e. in devtools debugging).

But perhaps that should be read-only, and perhaps the only way to add/remove them imperatively would be something like element.behaviors.add('behavior-name') and element.behaviors.remove('behavior-name') similar to element.classList.

Otherwise, if a user instantiates them manually, we couldn't guarantee that they pass in the element instance to the constructor (relating to the this.target ideas above).

I'll circle back to modifying element-behaviors to fix that part regarding behaviors. I think it is can be useful to get the instances, but perhaps they should not be instantiable.

Another idea regarding this.target is, maybe connectedCallback would receive the element, so connectedCallback(element) {}. If it was like that, then the user could instantiate the behaviors, and add them by reference, element.behaviors.add(theBehavior).

This allows behaviors to initialize their listeners when attached, and cleanup once detached, but these operations are not necessarily bound with the element live state, simplifying setup/teardown per each behavior, less unexpected when disconnectedCallback is called, but the element is still live in the DOM (which is the most common case here, when behaviors are dropped).

All names are subject to changes, but I think this proposal would be much cleaner than your one, and simply because, differently from Custom Elements, behaviors can be added and removed at runtime, while custom elements are forever, and strictly bound with the element these represent.

I hope this sounds better to you too.

I'm not sure. There's something nice about the simple initialize vs cleanup. The user need not worry about if the element still is in the DOM or not. The behavior just does what it does when it needs to, otherwise it is cleaned up and gone.

Also is "attached" vs "connected" naming clear? Both terms have been used to describe when elements are connected to the DOM.

I do agree the new suggestion would allow more flexibility of some sorts (partial clean up rather than full cleanups). Is it worth the extra methods? What may we want to do with the extra methods (that would be worth it) that we can't do with only two methods regardless of name?

With four methods, I would imagine some people just doing this:

class MyBehavior {
  connectedCallback() {
    this.initialize()
  }
  attachedCallback() {
    this.initialize()
  }
  initialized = false
  initialize() {
    if (this.initialized) return
    this.initialized = true
    // initialize stuff.
  }
  disconnectedCallback() {
    this.cleanup()
  }
  detachedCallback() {
    this.cleanup()
  }
  cleanup() {
    if (!this.initialized) return
    this.initialized = false
    // cleanup stuff.
  }
}

I think most of the time I would just want to initialize things, it will run for a while, then finally cleanup.

I think that I like the two-methods better still. But I'm not clear on if I'd actually want to do something different with the four method that merely forwarding to initialize/cleanup. I think I like the idea of "the behavior is connected or disconnected from the DOM", regardless of whether it happened with the element or not.

In this form, the user does have a guarantee: constructor or connectedCallback always reveals a connected element available to the behavior author. Or basically, if the behavior is connected, the element must be too. If the behavior is disconnected, the element may or may not be connected, but this should be of no concern to the behavior which should clean itself up.

disconnectedCallback might use a detached = false

That might be more confusing. I think four methods would be cleaner if it went that direction.

this distinction between being removed, as behavior, or signaling the element was disconnected, looks kinda important to me

Wondering what are some compelling use cases for it.

@WebReflection
Copy link

Passing it in via constructor does mean users have to assign it to their own (possibly #private) property to use it in other methods.

My concern is about trapping the argument/leaking it, while a read-only property that returns null once the behavior is detached looks less error prone.

or provide a base Behavior class

I think a base class to extend is more elegant and future proof (in case we want to add behavior features, all extenders will benefit).

That can't happen. If an element is disconnected already, connectedCallback will not be called.

The MO schedules records, so the sequence of events will be guaranteed, but being asynchronous, if you quick append and remove an element before the MO callback triggers, you have a connectedCallback event while the element is actually disconnected.

Nothing we can do though, let's not focus much on this, but it'll be a caveat of the polyfill.

That's also an interesting approach.

Yeah, I've realized I can simulate most of this via define('[has~="behavior"]', {...}) too in wickedElements.

I thought about that, but there's no already-existing built-in behaviors, so they aren't really "custom".

Fair enough.

So even after being disconnected from DOM, this.element could still reference the owner element.

Not in my implementation. this.target is null, once the behavior is detached, and the behavior virtually dead.

Would this be better?

It's veeeeeeery verbose ... let's think about it.

I haven't relied on it running after a behavior is cleaned up (disconnected), so I don't even know if it does or not currently.

My implementation drops the MO once detached, nothing works anymore, behavior dead.

It just creates a new one currently.

Cool, I like that.

But perhaps that should be read-only, and perhaps the only way to add/remove them imperatively would be something like ...

Yeah, I think leaking behaviors around is not great indeed.

Another idea regarding this.target is ...

My implementation sets it as getter and uses a WeakMap pre-populated per behavior <-> node. It works out of the box in constructor too.

I think most of the time I would just want to initialize things, it will run for a while, then finally cleanup.

For behaviors: yes, which is why connected/disconnected are not a good place to do so.

Wondering what are some compelling use cases for it.

DOM diffing. You have a list of things and you filter things as you type, as example, and nodes get off/on all the time, without changing state, except their presence on the document.

Doing listeners dance, or any other, due diffing, looks like extremely undesired overhead.

Behaviors are per element, not per element lifecycle, so coupling these strictly with element lifecycle might easily backfire, imho.

@WebReflection
Copy link

WebReflection commented Mar 25, 2021

@trusktr after thinking about this for a little while, I think the API would be much simpler if it had only attachedCallback and detachedCallback plus the target getter, as long as we propose a new API that would actually cut the mustard and offer what most developers want from CustomElements: their lifecycle on the DOM.

ElementObserver

This API proposal consist in being able to use connectedCallback and disconnectedCallback to any DOM element, exactly in the same way it works with Custom Elements, through a MutationObserver friendly API.

const eo = new ElementObserver((records, eo) => {
  for (const {target, type} of records) {
    if (type === 'connected') {
      // target has been connected
      doConnectedThings(target);
    }
    else {
      // target has been disconnected
      doDisconnectedThings(target);
    }
  }
});

const target = document.querySelector('any-dom-element');

eo.observe(target);

if (target.isConnected)
  doConnectedThings(target);

If the asynchronous nature (easier to polyfill and aligned with MO) is not cool or ideal, the API could instead be like:

const eo = new ElementObserver({
  connectedCallback(element) {
    // do things with element
  },
  disconnectedCallback(element) {
    // do things with element
  }
});

eo.observe(anyDOMElement);

// maybe the sync version could trigger
// connectedCallback if the element is already live?

With this new mechanism a lot of the arguments around builtin-extends might vanish, because at that point we'll have MutationObserver to mimic attributeChangedCallback on any element, and these two new mechanisms to understand when the element gets in or out the DOM.

This API makes it possible, out of the box, to define behaviors however developers like, but it'll also make it possible to have a simplified API for behaviors, in case we'd like to abuse the has attribute.

// base class
class Behavior {
  get target() { /* return the element/target */ }
  // a constructor like place to setup once
  attachedCallback() {
    this.eo = new ElementObserver(callback || literalObserver);
    this.eo.observe(this.target);
  }
  // a destructor like place to teardown once
  detachedCallback() {
    this.eo.disconnect();
    // this.target is null after this call
  }
}

Doing this way the Behavior doesn't mislead with elements associated callbacks through its prototype, the separation of concerns/responsibilities is clear, there's no confusion, and observing elements lifecycle becomes optional, so that behaviors can be much simpler in intent, scope, and also code.

What do you, or anyone else, think about these two possible APIs? Is it worth opening a new issue and discuss any of these in there?

Thank you.

@WebReflection
Copy link

P.S. I really like the literal API proposal, because it allows the Behavior class to eventually have those methods and be the argument passed along to new ElementObserver(behavior) ... heck, expanding the API to accept attributes and a filter, would basically replace Custom Elements and/or the need of a globally shared registry.

const eo = new ElementObserver({
  attributeChangedCallback(element, name, oldValue, newValue) {
  },
  connectedCallback(element) {
    // do things with element
  },
  disconnectedCallback(element) {
    // do things with element
  }
});

eo.observe(element, {
  attributes: true,
  attributesFilter: ['only', 'these', 'attributes']
});

I think I'll play around a bit with this, also making connectedCallback and disconnectedCallback optional through the configuration object (hints for the property name welcome).

@WebReflection
Copy link

@trusktr if interested, ElementObserver is live and working well.

@andregs
Copy link

andregs commented Jul 27, 2021

Hey, @trusktr, did you had a chance to look at ElementObserver?

@WebReflection , the possibility of augmenting standard elements with custom behaviors without a globally shared registry sounds really great, but I still don't see how to bind it to has attribute like in element behaviors (has is really convenient).

@WebReflection
Copy link

WebReflection commented Jul 28, 2021

@andregs has requires inevitably a registry, so it'll mine the flexibility of the ElementObserver, but it's still possible.

customBehaviors.define(
  'my-thing',
  {
    observedAttributes: ['some', 'attribute'],
    attributeChangedCallback(element, name, oldValue, newValue) {},
    connectedCallback(element) {},
    disconnectedCallback(element) {}
  }
);

The customBehaviors will verify that my-thing wasn't previously defined, and then it'll do something like this:

const registry = new Map;
customBehaviors.define = (name, behavior) => {
  if (registry.has(name))
    throw new Error(`duplicated ${name}`);
  const instance = new ElementObserver(behavior);
  const options = {
    attributes: true,
    attributeFilter: behavior.observedAttributes || [],
    attributeOldValue: true
  };
  registry.set(name, {instance, options});
  for (const target of document.querySelectorAll(`[has="${name}"]`))
    instance.observe(target, options);
};

// a simulation of what should happen at the global level
const mo = new MutationObserver(records => {
  for (const {target, attributeName, oldValue} of records) {
    if (attributeName === 'has') {
      const newBehaviors = (target.getAttribute('has') || '').split(/\s+/);
      const oldBhaviors = (oldValue || '').split(/\s+/);
      for (const behavior of oldBhaviors) {
        if (registry.has(behavior) && !newBehaviors.includes(behavior))
          registry.get(behavior).instance.disconnect(target);
      }
      for (const behavior of newBehaviors) {
        if (registry.has(behavior) && !oldBhaviors.includes(behavior)) {
          const {instance, options} = registry.get(behavior);
          instance.observe(target, options);
        }
      }
    }
  }
});
mo.observe(document, {
  attributeFilter: ['has'],
  subtree: true
});

The only possible improvement I can see needed is an eo.isObserving(target) to avoid missing new defined behaviors already set as attribute.

@andregs
Copy link

andregs commented Jul 29, 2021

I did some experiments with ElementObserver (https://stackblitz.com/edit/element-observer?devtoolsheight=44&file=index.js) in the spirit of this component and these are my findings:

  • it's confusing that eo.observe(el) triggers connectedCallback but eo.disconnect(el) doesn't trigger disconnectedCallback.
    • disconnectedCallback is triggered only if EO is connected and you remove the observed element from DOM.
    • I'd expect when I click the remove button the disconnect callback would be called and my div would revert to its original html content.
  • eo.disconnect(el) only means EO will no longer call your callbacks on DOM changes.
  • attr changed callback is called before connected callback when the element is added to dom with an observed attribute.
    • I think it makes sense to call both methods, but I'd expect connectedCallback first.
    • If connectedCallback comes first, I could put my initialization there (like the initial rendering of innerHTML), and use attr changed callback to re-render only the new values.
  • due to MutationObserver, if you make many changes to the elem at once (the mess button), your callbacks will be called multiple times and you'll have to be extra careful on how to react (difficult to determine what's the current state).
    • The main issue here is that the callbacks are called out of order and previous/current values become actually prev/final value.
    • If we can't guarantee the callback order and intermediate values, then we'd better swallow intermediate events and be notified only once with a summary of what happened (see https://github.com/mmacfadden/mutation-summary). Otherwise, you can imagine how confusing it is when you call increment fn but your callback says you went from 2 to 1 (•_•)
function messAround() {
  console.info('messing with the new one');
  const el = document.getElementById('another-counter');
  
  console.info('removing it from dom', el);
  el.remove();
  
  console.info('changing its value', el);
  counter.inc(el);
  counter.dec(el);
  counter.dec(el);
  
  console.info('adding it back to dom', el);
  document.getElementById('app').append(el);
}

image

@WebReflection
Copy link

I am not sure this thread is the right venue for these kind of conversations ... but here my answers:

it's confusing that eo.observe(el) triggers connectedCallback but eo.disconnect(el) doesn't trigger disconnectedCallback.

it's based on Custom Elements behavior, where connectedCallback is called even if the element was already live.

eo.disconnect(el) is based on current MutationObserver behavior. If you call disconnect you'll stop having notifications from that observer. While I agree these might be confusing, as similarly named, the element is not technically disconnected from the DOM, so I am not sure that should be called.

If we use connected/disconnectedCallback as seatup.teardown down, then maybe both callbacks should be renamed, like I've done with builtin-elements library. In this case, I see observedCallback and unobservedCallback decent candidates, but I'd be happy to change/re-think these too.

eo.disconnect(el) only means EO will no longer call your callbacks on DOM changes.

exactly like MutationObserver works ... you stop observing the element, nothing triggers anymore.

attr changed callback is called before connected callback when the element is added to dom with an observed attribute.

exactly how Custom Elements work. The attributeChangedCallback is always triggered before connectedCallback if an element is upgraded and one of its attributes is observed.

Moving away from the current standard lifecycle doesn't look like a good idea, imho.

If connectedCallback comes first, I could put my initialization there

this is abusing the meaning of connected/disconnected which is, in Custom Elements world, about these elements being upgraded and live on the DOM, or removed. The right place to setup listeners once is the constructors, but here we don't have one, so that observedCallback or upgradedCallback and downgradedCallback or unobservedCallback would be a better place for that. This is how previously mentioned builtin-elements library works. Agreed it's better ergonomic, not there by accident indeed.

if you make many changes to the elem at once (the mess button), your callbacks will be called multiple times

This is how Custom Elements work. The only difference here is that Mutation Observer callbacks are asynchronous and nobody can know upfront if other callbacks were queued/scheduled, without further delaying the reaction.

The order is guaranteed to be the correct one though, the sequence is always exactly the one that happened live.

However, element.isConnected is the only source of truth, and removedElements list might contain nodes that are live again, but the code will know only after parsing the next record.

This is how MutationObserver works, nothing I can do, and a well know tiny caveat of all Custom Elements polyfills to date that are based on the asynchronous nature of the Mutation Observer.

Synchroonus DOM mutation events are deprecated for good, and I don't want to over-complicate in my library this dance, as there is no effective way to avoid multiple calls, all I can guarantee is that the sequence is correct (i.e. by triggering removedNodes before addedNodes*).

I hope this answered all your findings/questions.

@andregs
Copy link

andregs commented Jul 30, 2021

Yes, that clarifies a lot. And I'm sorry if I went off topic, I'm just trying to collaborate since you asked what we think of the APIs.

After your explanations I can see I was misusing connected/disconnected callbacks as observed/unobserved callbacks. And, honestly, setup/teardown could be done alongside eo.observe(el) and eo.disconnect(el), like in my updated example, so I don't think you need to rename the existing callbacks or creating new ones. It's good enough the way they are.

The beauty of this ElementObserver (or element-behaviors) is that they make it easy to define reusable behaviors that you can apply to standard or custom elements. It's also good that the relationship between elements and behaviors is many-to-many, unlike in is="" (in my updated example I created a behavior to add a counter to an element, and another one to enforce a threshold on the element's value).

That said, I'd only rename eo.disconnect(el) to eo.unobserve(el) in ElementObserver API to avoid the confusion with disconnectedCallback. Also, it's valid to note that the callback order is correct as long as we don't mix connect/disconnect with attr changed events in a single run like I did, otherwise attr changed will always be called after connect/disconnect events. It doesn't matter much, though, since the callbacks have el.isConnected and the final attr value to easily determine the element's current state. So, if it's too much trouble to avoid multiple calls, don't worry with that. It's expected due to MutationObserver, as you said.

Thank you for your attention and hard-work. I'm looking forward to see @trusktr 's opinions too.

@WebReflection
Copy link

@andregs @trusktr after feedback and a post, I've experimented a new approach where mixins/behaviors are handled directly through the classList (element's class), solving name clashing, and the callbacks order. It's called p-cool and it uses Custom Elements primitives to work, so it's less MutationObserver error prone. Let me know even via twitter DM what you think, so we don't keep piling up posts to this thread, thanks.

@inopinatus
Copy link

inopinatus commented Mar 15, 2022

Adding to the pile of reasons to support extending built-in elements, whether it's is="" or some other mechanism.

The Hotwired framework depends on the turbo-frame custom element to progressively enhance a webpage. However, using it inside a table fails, for the obvious reason (content model). I figured it could work as a <tbody is="...">. And it does! After abstracting the constructor into a constructor factory, able to extend any built-in as a customized built-in, we found it works great.

Except in Safari of course where it doesn't work at all.

You can read the PR at hotwired/turbo#131.

@clshortfuse
Copy link

I'm realizing how hard it is to write Web Components with ARIA without being able to extend the native elements. For example, I would like to use HTMLButtonElement or HTMLDialogElement, but am forced to use the basic HTMLElement. The alternative is to wrap the intended element inside the WebComponent. But, now that element is now hidden from the regular DOM (HTML writer) meaning it's impossible to set ARIA attributes.

For example, if a user wants to add aria-label to the button, it's not proper to be on the parent HTMLElement. One solution is put a mutation observer for each and every ARIA attribute and then copy them down to the intended native element. This is essentially how the Ionic team has wrangled this issue, and seems somewhat messy, requiring constantly updating frameworks as ARIA attributes gets updated. You'll also have to hope that that framework is still maintained in the future, and doesn't fall out-of-spec.

https://github.com/ionic-team/ionic-framework/pull/25156/files

Is there really no clean solution to using native elements as fallback, as suggested by WAI-ARIA, and using Web Components?

Does Apple have another solution to this problem?

For the sake of accessibility, we should A) watch for all ARIA attributes, B) don't use Web Components, or C) ignore the recommendation of using native elements?

@rniwa
Copy link
Collaborator

rniwa commented Jul 26, 2022

ARIA attributes have been added to ElementInternals so that the default values can be configured there for each custom element without having to modify outer DOM tree. We (WebKit) haven't gotten around to implement that yet but we will.

@clshortfuse
Copy link

clshortfuse commented Jul 26, 2022

@rniwa Thanks for the quick reply. I saw your comment earlier (long thread) about HTMLInputElement and ARIA tags. HTMLInputElement should be self-closing anyway, meaning it's an "impossibility" inherent in custom elements. Replicating attrs is probably the way to go with a feature detection for ElementInternals. And after it is fully implemented, the ARIA attribute list would be ignored anyway (no falling out of spec).

@kellengreen
Copy link

kellengreen commented Jul 26, 2022

Thank you @rniwa. While we have you, is it still the stance of WebKit to not support this feature?

While I too am not a huge fan of the is= syntax, and do think ElementInternals is a step in the right direction. I'm still hoping (perhaps foolishly) that someday we will see this feature supported across all browsers.

@rniwa
Copy link
Collaborator

rniwa commented Jul 26, 2022

While we have you, is it still the stance of WebKit to not support this feature?

Nothing has changed as far as I'm aware.

@trusktr
Copy link
Author

trusktr commented Jul 13, 2023

@rniwa aria attributes are not the only feature difficult to work with due to lack of customized built-ins (is="").

Although I don't like customized built-ins as currently spec'd, I believe that because Safari (you as far as I can tell, because it seemed to have been your choice in particular) led the charge to not implementing customized built-ins, it would be great for Safari (probably you, not because I'm demanding, but because it was seemingly your sole responsibility to choose this path, although I would be happy to hear if I am incorrect) to lead the charge in providing a better and wholesome alternative that allows solving all the the same problems in whole and in a simple (if different) ways (ElementInternals is not that).

@mpaperno
Copy link

So this isn't being implemented for ideological reasons, is that right? After 8 years seems safe to say that is="" has been adopted by everyone else. Safari is the new IE here.

is may be awkward, but it is of no matter at all when elements are being created dynamically in code anyway (new CustomElement()). OTOH re-creating the full functionality of input elements just for a small tweak is absurd. Or having to fall back to the old ways of shadowing actual input elements, proxying events and attributes/properties/etc... "mutation observers" really? Bleh. All just workarounds that extending built-ins gets rid of. Most (if not all) of the basic "custom input element" examples I've seen to date end up wrapping an actual input in the shadow DOM. Great improvement... yea!

Sorry, but seems to me that anyone seriously suggesting there are "better" methods than simple inheritance hasn't tried implementing any of this stuff in real-world apps of any real complexity. IMHO of course.

Regards,
-Max

@EisenbergEffect
Copy link

EisenbergEffect commented Jan 14, 2024

At TPAC 2023, the attendees (browser vendors including WebKit, CG/WG members, community, etc.) agreed to begin working on an alternative that was capable of handling the original use cases plus additional use cases that have been discovered since. There were several proposals discussed during the meeting for which there were already WIP specs and even polyfills. Coming out of that meeting, a new proposal attempted to capture some of the ideas and discussion. Please give it a read and help determine if it will enable your use cases:

#1029

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment