-
Notifications
You must be signed in to change notification settings - Fork 376
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
A new attribute similar to is="", but would allow multiple behaviors to be attached to a given element. #662
Comments
If an approach like this were to be taken, it would be worth thinking up front about component encapsulation. For example, maybe component names can be defined on a per-root basis rather than globally: root.components.define('bar', class {
// ...
})
root.components.define('baz', class {
// ...
}) This would be similar to the idea for encapsulated per-shadow-root custom element names, but the idea here would be a brand new idea to the web so there wouldn't be naming conflicts with existing components (because none exist, unless the web wants to own components and release native ones, but I don't see why it should if it never has before). We can also let component names not have hyphens, as opposed to Custom Elements. |
@rniwa You said in the other thread
Properties would normally be defined on the component instances. F.e. an instance of the However, there could be a conflict if components assign properties onto the Element instance that is passed into the component's But in order to discourage developers blindly assigning properties onto an Element, there could be an API that makes it easy to get a component instance from the Element, f.e. // .components is readonly, perhaps frozen or sealed
const {foo} = someElement.components
foo.someMethod()
console.log(foo.someProperty) This way it would be possible for a component author to expose public methods and for them not to clash with methods of another component. <div is="audio-player" src="./foo.mp3"></div>
<script>
const audioDiv = document.querySelector('div[is="audio-player"]')
const player = audioDiv.components['audio-player']
// or maybe, const player = audioDiv.components.audioPlayer
player.pause()
// ...
player.resume()
// ...
const audioNode = player.getNode() // WebAudio API
// ... connect output to another node ...
</script> |
Another idea could be that the Element reference passed into a component's constructor could actually be a Or maybe there could be a way for a component to statically define which properties it will control, and the first component to be added which controls such properties takes precedence. f.e.
Then this component would control the value of
Would some sort of conflict-avoidance feature like that be too complicated of a to add to such a spec if it were spec'd? |
Couldn't you just use another attribute instead of <table is="very-generic" also="bar lorem"><table> const behaviors = {
bar: { connectedCallback(elm) { elm.classList.add('bar'); } }
lorem: { connectedCallback(elm) { elm.classList.add('lorem'); } }
};
const getbehaviors(elm) => {
return elm.getAttribute('also').split(' ').map(name => behaviors[name]);
}
CustomElements.define('very-generic', class {
connectedCallback() {
getbehaviors(this).forEach(b => b.connectedCallback(this));
}
}); – or solve it entirely in the JavaScript layer with some kind of mixin (or plugin) strategy? It seems like the high level frameworks could facilitate something like this in a number of ways without requiring a change to the low level spec, which I assume should be either based on inheritance with all the imagined problems it can cause or mixins with all the actual confusement it will cause; certainly not both via the same attribute? |
I really like the concept of such components, which are just adding functionality on top of existing elements, by plugging in into CEReactions, It would allow enhancing problematic native elements like However speaking of details in encapsulation and conflicts resolution, I'm thinking about simpler and maybe more naive approach. Then to answer @rniwa
What if those components/mixins have access directly to the element instance, but the order of names in If both mixins are defining a property setter with the same name, it will result in Even, if we limit ourselves to the case of only one component mixed by We would have "autonomous custom elements" that does The definition of such custom enhancement does not have to specify what it would extend, or it could be just an optional feature. Then consistently we could have "enhanced autonomous custom element". Like: <third-party-element is="my-enhancement"> customElements.define('third-party-element', class ThirdPartyElm extends HTMLElement{connectedCallback:/*...*/});
// ...
customElements.defineEnhancement('my-enhancement', {connectedCallback:/*...*/}); |
I don't want to come off as cynical, but you do realize that the current spec is based on decades worth proto-specification [1] which after an all out and still unfinished interstellar war between browser vendors has culminated in the [1] See https://www.w3.org/TR/sXBL/ and https://www.w3.org/TR/xbl/ and https://msdn.microsoft.com/en-us/library/ms531079(v=vs.85).aspx |
If this is intended for mixins consider adding a new, non-conflicting attribute as has= |
@wiredearp @jimmont I think you guys missed the part in which I mentioned an alternate name might be better considering
|
Not necessarily, because not all browsers even support If we were to change the spec, and all browsers wanted to implement the new spec, then it'd be official. That said, I'm open to the idea of using an alternate name other than |
I disagree. It should be very easy for people to use native web tech to easily build apps, without necessarily needing a framework. I think this is a good goal. Your example,
is much too verbose. This isn't relying on any new native behavior, it's just mapping some "behaviors" to CE callbacks manually, which is a good conceptual of making a polyfill for the idea here. And it's easy to make such a polyfill. I guess maybe you want to show that a feature like the one I proposed in the original post can easily be implemented in JavaScript. You're right, it probably can be, and this is due to the fact that behaviors are not required to extend from native classes like HTMLElement. This fact alone makes everything just easy to work with. The thing is, if it were spec'd and became standard, people could rely on it, and not have to choose between a gazillion different frameworks and libraries, and it'd work in every single web application without compatibility issues (or with the least amount of compatibility issues) because it would be API guaranteed to exist in every browser. |
As @jimmont suggested, maybe Here's an example based on what I like best so far from the three discussions (#509, #662, #663): class Foo {
connectedCallback(el) {
// do something with el
}
}
class Bar {
attributeChangedCallback(el, attr, oldValue, newValue) {
// do something with el's changed attribute
}
} behaviors.define('foo', Foo)
behaviors.define('bar', Bar) <!-- it "has" these behaviors -->
<any-element has="foo bar" /> But the defining part could also be components.define('foo', Foo)
components.define('bar', Bar) <!-- it "has" these *components* -->
<any-element has="foo bar" /> If elementBehaviors.define('foo', Foo)
elementBehaviors.define('bar', Bar) or elementComponents.define('foo', Foo)
elementComponents.define('bar', Bar) Going with "behaviors", here's what it looks like on a shadow root: root.elementBehaviors.define('foo', Foo)
root.elementBehaviors.define('bar', Bar) And here's what getting those components from an element and calling a method looks like: anyElement.behaviors.foo.someMethod() Here's an entity-component example. Imagine some game made with Custom Elements (rendering to WebGL for the sake of awesome): <ender-man has="player-aware holds-block" holds="dirt" position="30 30 30">
</ender-man>
<play-er position="40 40 30">
</play-er> then later the player gets away and the ender man behaves differently: <ender-man has="holds-block" holds="sand" position="30 30 30">
</ender-man>
<play-er has="diamond-armor horse-inventory" position="100 150 40">
</play-er> One might think, why not just use attributes, like the following? <ender-man player-aware="true" holds="dirt" position="30 30 30">
</ender-man> Well, then this means that there can only be one class associated with the The downside of this is to handle multiple piece of logic, they need to be encapsulated in a single class (f.e. So the approach where behaviors can be entirely separate classes (but with Custom Element callbacks) is a win because they can all be decoupled from each other more-so than mixing stuff into a single class. Behaviors can be added and removed. In the above example, when we removed the Or, maybe for behaviors, there could be something else like a It would be possible, for example, to have logic specifically for when the behavior is removed, and logic for specifically when the element is detached (but the element still have the behavior). |
@trusktr So are you proposing a) <web-map mixin="map mymapbehaviour"> or b) <map mixin="map mymapbehaviour"> where behaviour named "map" is the native behaviour that I was getting from inheriting from HTMLMapElement ? (Using @rniwa 's terminology here, since he's the one objecting to single inheritance). Use tr / my-tr if you have to run with that in your explanation. |
It might be worth looking at custom attributes. @matthewp has written something that might be a good basis for discussion: https://github.com/matthewp/custom-attributes. It would fit the custom element model quite well. |
@treshugart That's an interesting concept, but I think it may fulfill a different purpose. I believe that that concept would be usable in tandem with this concept, but it doesn't explicitly replace the concept here. That concept, for example, doesn't seem like a good fit to solve That one hooks into the life cycle for specific attributes, which might be useful in some ways, but this one uses the custom-element life cycle methods, which are a bit different, and in fact these "behaviors" can observe changes to attributes of an element, including custom global attributes. I believe these two concepts can live exclusively from each other, and it may be nice to have both. I didn't have much time write this response, I'd like to perhaps make a simple polyfill for this idea and then show the mix of the two ideas, later... |
Not quite. The behaviors can be applied to absolutely any element (unless there's a way to limit which elements a behavior can be applied to, but I'll skip that idea for now). So, for example, suppose we have behaviors class Foo { ... }
class Bar { ... }
elementBehaviors.define('foo', Foo)
elementBehaviors.define('bar', Bar) They can be used on any element: <div has="foo bar"></div>
<div has="foo"></div>
<map has="bar"></map>
<any-element has="foo bar"></any-element> It's merely a way to instantiate a specific class for any element, so that the instantiated class can react to the lifecycle of the target element. In <div has="foo bar"></div> There's at least three things happening:
How you use these behaviors and for what purpose is up to you (you choose which behaviors to apply to which elements). In your examples, In the <web-map mixin="map mymapbehaviour"> example, if
Finally, in my example, the class Foo {
constructor(el) {
// A behavior is constructed when it's name is added to an element's has=""
// attribute, or when the parser first encounters an element and creates it
// and that element already had the name it its "has" attribute.
console.log(el) // a reference to the element
console.log(this) // a reference to this class instance
console.log(el === this) // false
}
removedCallback(el) {
// This is called when the behavior's name is removed from the element it
// was instantiated for.
//
// For example, some other code might have called `el.setAttribute('has',
// 'bar')` which no longer contains the name "foo", so removedCallback() is
// called.
}
connectedCallback(el) {
// do something with el anytime that el is added into the DOM.
}
disconnectedCallback(el) {
// do something with el anytime that el is removed from the DOM.
}
attributeChangedCallback(el, attr, oldVal, newVal) {
// do something anytime that one of el's attributes are modified.
}
}
class Bar {
constructor(el) { /* same description as with Foo */ }
removedCallback(el) { /* same description as with Foo */ }
connectedCallback(el) { /* same description as with Foo */ }
disconnectedCallback(el) { /* same description as with Foo */ }
attributeChangedCallback() { /* same description as with Foo */ }
} Even more lastly, a new instance of a behavior is created for each element it is assigned to. If we have <div has="foo"></div>
<div has="foo"></div>
<div has="foo"></div> then just like there are three instances of It could be possible to add an additional feature, where a singleton class can be specified, so that only one instance of it is instantiated for all elements it is assigned to. Suppose the API was like this: class Baz {
constructor(el) { /* same description as with Foo */ }
removedCallback(el) { /* same description as with Foo */ }
connectedCallback(el) { /* same description as with Foo */ }
disconnectedCallback(el) { /* same description as with Foo */ }
attributeChangedCallback(el, attr, oldVal, newVal) { /* same description as with Foo */ }
}
// Let's use this behavior only inside a given ShadowDOM root (another feature).
shadowRoot.elementBehaviors.define('baz', Baz, {singleton: true}) and that we had this markup (inside the shadow root): <any-element has="baz"></any-element>
<other-element has="baz"></other-element>
<div has="baz"></div> In this case there'd be only one instance of The There's probably more considerations to be ironed out, like for example, what if an element is removed from DOM and never added back. This would obviously call a behavior's |
I mentioned
So here's that idea. The API might look like this: class Foo { ... }
elementBehaviors.define('foo', Foo, {limit: [HTMLMapElement, WebMap]}) Suppose we have this markup <web-map has="foo"></web-map>
<map has="foo"></map>
<div has="foo"></div> In this case, only two instances of the And here's another interesting idea. Suppose we have class SomeElement { ... }
customElements.define('some-element', SomeElement)
elementBehaviors.define('foo', Foo, {limit: [SomeElement]})
elementBehaviors.define('bar', Bar, {limit: [HTMLUnknownElement]}) This would apply the behavior only to any element that has no underlying class. For example, if there's no class defined for <some-element has="bar"></some-element> then the If at some point the element gets upgraded to a |
@trusktr re lifecycles being different, what if custom attributes also had hooks for element lifecycles? I think these ideas are so close that it's worth considering how they might be merged. The custom attribute model is much closer to custom elements. |
It's true, perhaps the same thing can be achieved with Global Custom Attributes, but semantically custom attributes seems better for attributes that are meant (and will likely) be used on every type of element. For example, the |
If we went that route, we'd see the following often, when only a behavior is needed and a value for the behavior might not be necessary: <ender-man is-visible player-aware holding="dirt"></ender-man> They both solve the same problem in a different way. I'm leaning towards a special attribute though, because it could be that using a particularly-named attribute (f.e. |
Plus, naming. Custom Attributes would require hyphens, while behaviors wouldn't. Woot woot! |
This effort could perhaps be pushed back to user land for prototyping if the issue was to be rebooted as some kind of "general mechanism for hooking into the lifecycle of custom elements". What if, for example, in addition to the current class constructor: CustomElements.define('my-element', class extends HTMLElement {}); – the element registry was rigged to also accept a singleton object: CustomElements.define('my-element', {
constructedCallback(elm) {}
connectedCallback(elm) {}
disconnectedCallback(elm) {}
attributeChangedCallback(elm, ...args) {}
adoptedCallback(elm, ...args) {}
}); – which triggers on lifecycle events for all instances of Or he can do both: Because you can register as many of these objects as you like, in addition to the single class constructor. The Custom Element will be associated with an infinite amount of behavior while the syntax in markup stays the same: <my-element>
<td is="my-element"> The [*] By message passing via non-bubbling CustomEvents from the Custom Element, one could perhaps suggest, so the the current class hierarchy doesn't just become a hierarchy of class-clusters made of hard dependencies. Just to illustrate how this might reduce the number of specifications spawned. |
@wiredearp With that idea it is still not possible to associate multiple behaviors. In your example, I think it'd be beneficial to have one simple clean new idea without mixing it with the confusing and limited existing functionality. |
Perhaps like this: import { CustomBehaviors, CustomBehavior } from 'myproposal';
CustomBehaviors.define('one', class extends CustomBehavior {});
CustomBehaviors.define('two', class extends CustomBehavior {});
// and so on ...
CustomElements.define('my-element', {
constructedCallback(elm) {
if(elm.hasAttribute('has')) {
CustomBehaviors.init(elm); // puts 5 new CustomBehavior() in WeakMap
}
}
}); The mechanism simply provides the necessary callbacks for you to experiment with the syntax: <div is="my-element" has="one two three four five"/> – until it becomes popular enough to be included in the standard. I just think this works better than the other way around. |
True, having an actual implementation definitely would help. This is a good place to determine what the features will be for that trial implementation. I still this that constructedCallback(elm) {
if(elm.hasAttribute('has')) {
CustomBehaviors.init(elm); // puts 5 new CustomBehavior() in WeakMap
}
} is too much work for authors writing custom elements. They shouldn't have to wire up this functionality. It should be baked in. Literally, to make things easy, all they should need to do is elementBehaviors.define('foo', Foo) then the rest is automatic. If an element has the <div has="foo"></div> And that's all. Nothing else should be required. ( Because, when you write if(elm.hasAttribute('has')) {
CustomBehaviors.init(elm); // puts 5 new CustomBehavior() in WeakMap
} then this automatically makes you wonder, what happens if they just write CustomBehaviors.init(elm); // puts 5 new CustomBehavior() in WeakMap without the conditional check, and the element doesn't have a ) |
Just to clarify that the only person who would write the potentially verbose and error prone code in question was you, the "polyfill" author, and everybody else could then write the JS and HTML exactly like you suggest. Before you release the library, you would of course move the part of the code into the module <table is="data-grid has="sortable clickable editable searchable filterable configurable"/> – is preferable to what the current spec has to offer: <table is="data-grid" sortable clickable editable searchable filterable configurable/> – or some combination of the two: <table is="data-grid" searchable filterable configurable has="sortable clickable editable"/> We are also free to explore the concept of shared state and define recovery guidelines for separate entities to act on the same DOM. We can determine the best APIs for behaviors to communicate and assert each others existence. Perhaps some component authors in the end decide that a mixin strategy in pure JS is preferred, because it is easier to release an NPM module than it is to edit one hundred HTML files in one thousand websites whenever they release a new behavior "screenreadable", or perhaps a majority of components will simply not use mixins at all. <table is="data-grid"> I can even imagine a movement against a "general mechanism for hooking into the lifecycle of custom elements" except for what the element decides to expose, because it violates some design pattern or justified concern. Or perhaps some other reimplementation of Custom Elements and/or Angular directives catches on, there seems to be no shortage of ideas. I think that however the specification is not the best place to sell an unopened can of worms, because people will be tempted to buy it just for that fact. A low level mechanism would allow them to try before you buy, and the example API I have suggested could by itself be used to extend the behavior of components even without adding new attributes. Perhaps in the end that turns out to be easier to maintain. <table is="data-grid"> |
The following feels very 🚂 🚋 🚋 🚋 wreck pattern IMHO. anyElement.behaviors.foo.someMethod() class Foo { ... }
elementBehaviors.define('foo', Foo, {limit: [HTMLMapElement, WebMap]}) This pattern feels very 🇨🇭 Army Knife™ to me. Usually API confusion leads to footguns. Best to keep
I concur 💯 however in the "simple and clean" category these examples leave much to be desired. I do like the concept of "enhanced" elements re: @wiredearp <table is=custom-element> And the ability to not have to extend from HTMLElement is a nice thought as well. But certainly feel this can be implemented at the library level with ease. Perhaps conventions for This even leaves room for Gut instinct says a couple of nice conventions could arise from this at the library level. However, we should be cautious of breaking Ockham's Razor at the spec level. My 2 Satoshi P.S. @trusktr, skating since late 90's #rad 🤘 #stillRockTracker & 🇨🇭 |
I'd be fine removing the current However, things at the library level are not "standard" or "spec". 100 different implementations will pop up, and then we'll have a bunch of different ways of doing the same thing that are incompatible with each other; fragmentation. If we had something standard, spec'd, and built into browser, we could rely on it in every single web app, not just specific web apps that use specific implementations. |
@prushforth Mind explaining your downvote? |
You are against "is", but it also seems like you are promoting what appears to me to be more complicated solutions (multiple inheritance / mixins). Now I won't say that your push back on is has singlehandedly delayed its implementation, but there are many people with operational systems based on is that are waiting on its delivery, so maybe we could just ease back on the ideas for a few months and let the standard as written get implemented. Finally, I'm not certain that github issues are the best place to float idea balloons: the WICG has a nice moderated forum for that where everyone can share ideas and get feedback on them from other experts where there is also a CLA in place for IP contributions. And I agree the little thumbs up/down is passive aggressive BS. There I said it. |
@prushforth Thanks for the reply! I think you misunderstood what I proposed.
On the contrary, the idea in this issue does not require inheritance at all; and they are not mixins. In the examples from above: class Baz {
// ...
}
elementBehaviors.define('baz', Baz) the The The examples in this issue are not mixins; they are multiple classes whose standalone instances can operate on the same object (a DOM element). As with any sort of code reading or setting properties on the same object, there's room for collision, but that doesn't make them mixins. Multiple jQuery plugins, for example, can operate on the same Element, but they aren't mixins. Nothing's perfect, however the idea in this issue is more powerful than the current |
For anyone interested, I've made an initial implementation of the I plan to open a new and more concise issue with a standalone implementation with examples. If you're curious to try it out, you can install <script>
class Foo {
constructor(element) {
// do something with element
}
connectedCallback(element) {
// do something with element
}
disconnectedCallback(element) {
// do something with element
}
attributeChangedCallback(element, attributeName, oldValue, newValue) {
// do something with element's attribute change
}
}
elementBehaviors.define('foo', Foo) // no hyphen required
class SomeThing { ... }
elementBehaviors.define('some-thing', SomeThing)
</script>
<div has="foo some-thing" ... ></div> I'm currently using this to implement the WebGL elements in my project. For example it looks like this: <i-scene experimental-webgl="true">
<i-node id="light" position="0 0 0">
<i-point-light position="4 4 4">
<i-mesh
position="0 0 0"
size="4"
has="sphere-geometry"
color="0.5 0.5 0.5 1">
</i-mesh>
</i-point-light>
</i-node>
<i-mesh
id="first"
size="2 2 2"
rotation="30 30 30"
has="sphere-geometry basic-material"
color="0.5 0.5 0.5 1">
<p> This is regular DOM content </p>
<p>TODO throw helpful error when component not used on i-mesh</p>
<i-mesh id="second"
size="1 1 1"
has="box-geometry basic-material"
rotation="30 30 30"
color="0.5 0.5 0.5 1"
position="1.5 1.5 0" >
</i-mesh>
</i-mesh>
</i-scene> |
…from Mesh and specify a default box-geometry or sphere-geometry, respectively. This shows how easy it is to extend existing elements that specify new behaviors. It is similar in some ways to A-Frame's entity-component model, but ours are called element-behaviors, so as not to confuse with the word 'components' used in Web Components or React Components. For comparison, 'Element' is to this library as 'Entity' is to A-Frame, and 'Behavior' is to this library as 'Component' is to A-Frame. This "element-behaviors" idea was discussed on the w3c GitHub: WICG/webcomponents#662.
I hadn't fully answered your question there. With these element behaviors, you'd do this: <table>
<tr has="selectable"><td></td></tr>
</table> where the If we tried to achieve this with Custom Elements, we'd try: <table>
<selectable-tr><td></td></selectable-tr>
</table> which doesn't work for the reasons in #590.
<tr has="selectable click-logger mouse-proximity-action" onproximity="..."><td></td></tr> where elementBehaviors.define('selectable', class { ... }) // simple class, no extending
elementBehaviors.define('click-logger', class { ... }) // simple class, no extending
elementBehaviors.define('mouse-proximity-action', class { ... }) // simple class, no extending |
Alright everyone, I finally put up a standalone implementation of the "Element Behaviors" idea: #727 Basic codepen example. |
Closing this, can continue in #727. |
Currently,
is=""
is spec'd to define ways in which a Custom Element can extend from a native element but without having to write the Custom Element's name inside the DOM because it would otherwise break ancient parsing rules and therefore cause unexpected breaking behavior which leads to confusion. See issue #509 detailing the problems with the currently-spec'd behavior.For example, one of the great uses for
is=""
is that it solves problems with tables and other elements, where using a custom element the normal way will not work:because in that example the engine specifically expects a
<tr>
element, and does not look at the composed tree as a source of truth (like it should).The current spec allows a solution using the
is=""
attribute,so that the table can be parsed and rendered correctly, but this currently requires awkward and confusing inheritance patterns as mentioned in #509.
Here's a very simple proposal that would repurpose
is=""
(or an attribute with a new name) for allowing developers to attach any number of behaviors ("components") to an Element (originally described in #509 (comment)).The main idea of that original comment is that we can add any number of behaviors to an element while not being required to extend from the element's original interface/class.
F.e.:
Note that these "components" do not directly or indirectly extend (and are required not to extend from)
Element
. (This limitation may need to be further fine-tuned; perhaps they are not allowed to extend fromNode
? This is just an example.) It wouldn't make sense, for example, for the<tr>
to be bothHTMLTableRowElement
andHTMLButtonElement
... or would it? This can be discussed later.In the example, the
foo
andlorem
components are simple classes that don't inherit from any other class, and using lifecycle hooks similarly to Custom Elements we can still do interesting things with the target element.We can also attach more than one component to a given element. And components don't need to be exclusively limited to a single type of element like with the current
is=""
attribute!As an idea to prevent possible confusion with the existing natively-implemented
is=""
in some browsers, this attribute might be named something else, f.e. maybecomponents=""
, which is nice, but more typing. Maybecomps
for short? Etc.is=""
is still nicest.is=""
is still the nicest because it is short and simple.IMHO, this new
is=""
attribute functionality is much more desirable than the currently confusingis=""
attribute, and follows the proven approach that well-known libraries (old or new) take today (jQuery, Vue, etc).The text was updated successfully, but these errors were encountered: