-
Notifications
You must be signed in to change notification settings - Fork 378
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
Proposal: Custom attributes for all elements, enhancements for more complex use cases #1029
Comments
That was a fast turnaround.... There are a couple of things I like about this right off the bat:
I agree on the need to hyphenate names, again matching the CE rules and the strict blocking of certain names altogether. I need to think more about registration. That's the biggest open question for me. I think having registries on the individual types is interesting. I hadn't thought about doing it that way. I wonder how that will work with custom element registries, particularly WRT scoped registries. So, I wonder whether attribute registration should just be another API on the existing custom elements registry, so we can inherit all the work on scoping for attributes as well. My personal preference...I don't particularly like using a "has" attribute vs. just using attribute names. It doesn't feel as "HTML-y" to me. That's not a deal breaker for me though. I do think there should be a programmatic way to add and remove behaviors (and as a result, possibly callbacks for when that happens). I'm inspired a bit by Unity 3d, which has a generic scene graph with a behaviors system that is the mechanism for all node specialization. Behaviors are part of a collection and can be added, removed, searched, etc. This comes in handy if you want to build more complex systems of behaviors where one behavior looks for another and then if found, collaborates with it through its public API. You could imagine a custom drag/drop system working this way with custom attributes for draggables and drop targets, where the attributes locate one another and through their APIs coordinate the drag/drop. Ok, that's just a few quick thoughts. I'm excited to see if we can make this happen! |
Thanks for the implicit feedback on my proposal. There are some nice ideas here I may steal if that's okay, with "attributions" (sorry for the bad pun) (and apologies for overlooking your original 2017 proposal, will add a link to that for sure). I remain unconvinced of this direction (but I will see if my reasons hold merit on reflection). It seems I am in a minority of one, but I will continue slogging along at my proposal, incorporating ideas such as these, in case we find a happy compromise. For starters, it seems that the feedback is that my proposal is too complicated, so I will look for a better "on ramping" experience. One concern:
This means that in order to register the attribute, then we need to wait for the MyInput class to be downloaded. But if we use whatever we call this thing-of-a-jiggy for doing cross cutting things (like binding during template instantiation, for example), I think that could be an issue. I'm curious if you could provide insights on this question: When I look at this documentation, it seems that by default the "hooks" can apply to all elements by default. Likewise when I look at Vue and others (knockout.js) etc. My proposal was heavily influenced by what I see in industry (as that seems like a proven model?) It seems that by default your proposal is deeply curtailing the developer's "reach" and I'm puzzled as to why? Is it parsing constraints? I agree we want the developer to be able to curtail their own reach, but I'm a bit surprised that that is the direction this is taking? |
Thanks for writing this up so quickly! Some initial thoughts on this proposal only, still need to digest some of the referenced alternatives.
|
Hm. I need to understand the use cases more where scoping comes into play with attributes. Attribute registries are scoped to element classes already. Is the use case having different attributes for the same element class if registered differently, in different registries? Is the use case adding an attribute to a built-in only when used within a WC's Shadow DOM, without affecting the built-in outside that? FWIW making scoping part of the custom elements registry is also problematic: First because registration is often decoupled from the custom element definition, so that consumers can do the registration. If now consumers also have to register all the element's attributes, that is too much work on the consumer side, for little benefit, plus it would break DOM methods to read/write attributes.
How so? It's designed with that as a primary use case.
If there is, even better! Though I suspect use cases for
It …already does? In fact, it entirely automates it 🙂
Not necessarily, that's what
I …don't understand what you're asking here at all. 🤷🏽♀️ What's the relevance of Mavo hooks and Vue plugins? What is curtailing what developer reach? 🤔 |
I agree very much with two things you have said:
and I try to abide by the principal you've brought up -- about making simple or common things, easy to do, while not making complex things impossible. So, thinking about it last night, here's my simple use case, and I will add to it my proposed solution to that simple case, that I think typifies the "common" use cases in my mind: Say all you need to do is to create an isolated behavior/enhancement/hook/whatever associated with an attribute, say "log-to-console" anytime the user clicks on elements adorned with that attribute, where we can specify the message. Here's how that would be done with the custom enhancements proposal: customEnhancements.define('log-to-console', class extends ElementEnhancement{
attachedCallback(enhancedElement: Element){
const msg = enhancedElement.getAttribute('log-to-console');
enhancedElement.addEventListener('click', e => {
console.log(msg);
});
}
}); <svg log-to-console="clicked on an svg"></svg>
...
<div log-to-console="clicked on a div"></div>
...
<some-custom-element enh-log-to-console="clicked on some custom element"></some-custom-element> Done! If your proposed alternative is as simple, then my question to you regarding vue and mavo was truly out of left field and irrevelant. (I really do want to peg my hopes on someone else's proposal, not my own, so hoping you can address this concern I have). How would your proposal solve this? How many lines of code would be required to cover all the DOM elements? @sorvell , you had me in complete agreement until you proposed the has option -- it seems to me solving the problem above And can we go to teams like HTMX and Alpine and Wordpress and say "sorry, to get the platform's blessings, what you've been doing is wrong -- you need two attributes to solve this problem, which I know is more complicated, few if anyone has or is doing that, and increases the chances of naming conflicts, but the benefits are..." And that's where I'm drawing a blank, without attempting to mind read, and probably getting the benefits wrong. Could you explain what I'm missing? |
Sure, here you go: class LogAttribute extends Attribute {
connectedCallback() {
this.ownerElement.addEventListener("click", e => console.log(this.value));
}
};
HTMLElement.attributeRegistry.define("log-to-console", LogAttribute); Roughly the same amount of code I believe. Note that because SVG elements do not inherit from
Btw your code will log the old message even if the attribute changes (this is easily fixable by moving the When does
+1 to that. |
You are correct, in your observations, all good points. I literally wanted to look at the simplest requirement to compare, which you've done in spades. Thanks! You've easily convinced me that I was wrong, I misread what you are proposing, (I guess I was thrown by the HTMLInput.attributeRegistry.define, but on rereading what you wrote, I see what I missed). I hope you will address the more "complex" things I'm after (the ability for all these solutions to work well together) as the proposal progresses, and I feel a burden lifting from my shoulders as we speak! |
Hi @LeaVerou , I for one vote overwhelmingly to support svg (I was proposing that by extending "ElementEnhancement"), and I think it is up there in the top complaints / limitations for custom elements. So unless there's a solid reason not to, why wouldn't we? (mis)reading your proposal further (apologies in advance):
I'm getting a little lost here. Is your proposal heading in the direction of allowing each custom attribute to "opt-in" to be a behavior? Is that why you are considering the "has" attribute? Is basing access to the name of the behavior based either literally on the custom attribute, or the camelCase string problematic? Either way, I'm not following some of the discussion (apologies, again), but have you yet settled on how third party vendors can access the behavior, what that looks like (and how to make that seamless), or is that a work in progress? Are attributes required to enhance an element? If so, why? Feel free not to answer, just wanted to convey what I'm looking for to be 100% satisfied, and what I'm finding difficult to follow. |
I'm with you on that one!
You're right that as currently described, each custom attribute would be "opting-in", but also note that you could e.g. activate the behavior for the entire subtree the attribute is on (e.g. imagine a declarative version of Vue where the selector would be
I'm listing two potential association schemes. I don't think we should go with
Not 100% sure I fully understand what you're saying here, but perhaps the default I'm proposing above satisfies that?
Not 100% sure I understand what you're asking here either, but since these are regular objects (most likely classes, but I could see plain literals working too) you can just export them in the usual way. If that doesn't answer your question, I think some more specific use cases could help me understand it better.
No, they are not.
This is great feedback on what I may not have explained sufficiently well, so please keep it coming! |
Unless I'm missing something, the basic scoping problem is the same for both custom elements and custom attributes. The issue with custom elements is basically this: The same issue could occur with
The bit that isn't quite connecting with me is perhaps just not filled in yet. Having to configure an element's attributes' behavior via something other than the element class feels unnatural, but this part of the proposal may just need to be worked out more.
There's 3 cases that I think are actually fairly common: (1) 2 way reflection: attribute <-> property, (2) 1 way reflection: attribute -> property, (3) property only.
|
Couple other points related to the basic design of the class:
|
@sorvell , so I think you are advocating allowing one behavior to "own" multiple (non-data?) attributes. Benefits I see:
Dangers:
Maybe that approach should be optional? |
What does that mean? What is this Attribute interface? And who would add listeners to it? |
I wonder if in practice registering custom attributes like this ends up in a situation where it becomes impossible to DCE or move this code to late modules because it might end up being used somewhere. Any way of avoiding this ends up putting explicit imports somewhere, which may be unintuitive. This problem probably happens even with this is="" approach too, so I guess this isn't really making anything worse |
Hello you all, interesting ideas and conversation! I would love to get involved with F2Fs but I didn't have the chance this time around. It'd be great to meet you all in person if not in a call, hopefully next time. Please bare with me, this reply is long, covering lots of topics, hopefully separately and easy to follow (just long). @LeaVerou I found the scoping of attributes on element classes interesting.
@sorvell I think there's merit for both global and scoped attributes. F.e. a library might want to define certain attributes for functionality specific to the library and the elements those attributes are placed on (f.e. a material on a mesh), but another library might like to define attributes that work on any element (f.e. a click tracker on any element). Perhaps there's a need for scoping per class and per shadow root? F.e.: this.shadowRoot.attributes.define({
"foo-attribute": {
element: HTMLButtonElement,
attribute: FooAttribute,
}
}) I think at the very least we need ShadowRoot scoping even if not element class scoping. For global attributes, it could be
This got me thinking that an another way to scope attributes to a class could be via the observedAttributes array (but this won't work for built-ins), although maybe it isn't possible without breakage? For example: class MyAttr extends Attr {
static name = "my-attr"
connectedCallback() {...}
valueChangedCallback(oldVal, newVal) {...}
// ...
}
class MyEl extends HTMLElement {
static observedAttributes = ['some-attr', MyAttr]
} where
I pondered this too. If a custom element receives a value via JS property, it must remember to reflect that back to the attribute instance or reactivity in that attribute instance will not happen (today custom elements systems tend to make reflection optional while still triggering reactivity, which is in contrast to this). One way I've dealt with this is having a custom attribute (or behavior) use
I made <div id="div" has="foo"></div> const div = document.getElementById('div')
div.fooDataSet.bar = 123 // type error, unknown property (TypeScript has no idea a "foo" behavior was attached to the div) If behaviors are defined globally, then it is possible that const div = document.getElementById('div')
div.behaviors.get('foo').bar = 123 // ok, we know the type of behavior object, we know it has a bar property. and similar for const div = document.getElementById('div')
div.attributes.getNamedItem('my-attr').bar = 123 // ok, we know the type of attribute object, we know it has a bar property. (association of custom attributes into When I've previously defined a specific set of behaviors that should be used on specific types of elements, I had to do a type augmentation of the element class to add all the properties from the behaviors onto them. For example, if an element export class MyEl extends HTMLElement {...}
customElements.define('my-el', MyEl)
class Foo {...}
elementBehaviors.define('foo', Foo)
class Bar {...}
elementBehaviors.define('bar', Bar)
// Augment the class with all the possible properties it could gain from a known set of behaviors.
export interface MyEl extends Partial<Pick<Foo, 'a' | 'b'> & Pick<Bar, 'c' | 'd'>> {} where But the issue with this is:
Doing this augmentation is important however, because without it we get type awareness and intellisense on the elements in JSX, etc. I believe it is impossible to dynamically augment the type of a JSX element based on a value of one of its props. If we write the following JSX assuming that a behavior "foo" observes attributes "a" and "b" on a host element, return <my-el has="foo" a={1} b={"2"} c={something} d={otherthing} /> there's no way for TypeScript to know that the How do we do this in a way that will be the simplest to add type definitions for?Right now, type definitions in frameworks typically read properties from an object type for a given element name. For example, JSX type defs come from declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
"my-el": Pick<MyEl,
// pick some props, but not all (f.e. not methods).
'prop1' | 'prop2'
// pick properties augmented from behaviors
| 'a' | 'b' | 'c' | 'd'
>;
}
}
} It can be simplified with helpers (but doesn't changed the end result). Most frameworks today simply work on a bag of properties for a given element. For attributes or behaviors that need to observe separate attributes on the host element, I've been contemplating moving to this pattern, which would be both easy to define types for without bags of possible properties per element, and supported in all frameworks: <my-el a-known-property="123">
<foo-behavior another-known-property="456"></foo-behavior>
</my-el> where for every custom element that is ever defined, using today's mechanisms we always know the set of properties per element in a clean way without performing type augmentation. The user of <my-el a-known-property="123">
<third-party-behavior some-new-property="456"></third-party-behavior>
</my-el> In this pattern, the "behaviors" would use So a main thing I'm wondering is, if we were to introduce attributes/behaviors/enhancements that observe other attributes/properties on the host element, what sort of path can we enable for frameworks syntax wise and type wise? For example, if adding a "foo" behavior means we now have (I'm not a fan of the data- and dataSet verbosity, I've never used those APIs, just plain attributes and properties. Am I missing out?) Apple's new 3D Alternative to customized built-ins.I think the proposal, in whichever form, needs to support an alternative to this: <button is="cool-button" cool-foo="foo" cool-bar="bar"> and in behavior format or custom attribute format that looks like this, for sake of ensuring it is in our minds: <button has="cool-button" cool-foo="foo" cool-bar="bar"> or <button cool-button cool-foo="foo" cool-bar="bar"> In either case, to really be an close alternative to custom elements that extend built-ins, the behavior or the custom attribute needs to have a feature like
@sorvell in both of Lume's element-behaviors and custom-attributes,
This is a simple model: there's a creation hook, and a destruction hook, and that's really all that the end author of an attribute/behavior should worry about. Otherwise, things will get more complicated if we have
@sorvell this is covered with the Here's an example impl with hypothetical custom attributes (imagine similar with behaviors): class WithMoods extends Attr {
static observedAttributes = ['onhappy', 'onsad', 'onbored']
}
customAttributes.define('with-moods', WithMoods) <input with-moods onhappy... onsad... onbored...> Also, on that note, why does it really matter if custom attribute names have dashes or not, considering that this is not a requirement for Suppose I want to implement a custom constructor() {
this.shadowRoot.attributes.define('onclick', MyOnclickAttributeThatMapsStringNamesToMethodsOfMyElement)
}
foo() {...}
connectedCallback() {
this.shadowRoot.innerHTML = `
<div onclick="foo"></div>
`
} It seems that the worse thing that a custom attribute could do, without dashes in the name, is provide an different value than the browser expectes. This is fundamentally not different than the end result of someone setting an invalid value on an attribute from outside of the element. Maybe I want to define custom attributes, for use on certain elements ( Why would it be a problem to define I don't see the issue this has compared to custom elements. With custom elements, we aren't just overriding a single string value that the browser reads, but we're replacing the whole implementation of a class that the browser uses (if we were to allow non-hyphenated custom elements), which could very much affect how the browser works. But in case of custom attributes, unless I missed something, it seems that the browser simply needs to read their values.
@bahrus why is namespacing required? Why not just leave that to userland, linters, etc? For example, in JavaScript, you can shadow an outer scope variable with a same-name variable in inner function scope, and the language doesn't care. Should HTML care? What problem will be solved? Can you show code examples of what namespacing solves?
@smaug---- We already have a We've also discovered simple patterns like class-factory mixins in modern JS using classes + functions. Why not introduce utility mixins natively? For example, leave class MyAttr extends EventTargetMixin(Attr) {
connectedCallback() { this.dispatchEvent(new Event('foo')) }
} but note that this means that now the attribute emits an event that does not capture/bubble/compose with element events. Also note that attributes can easily use their host element as an event target. Maybe having an EventTarget custom attribute instance would prevent the need for naming of events on elements being more unique? Why hasn't the platform released any new classes in the form of mixins? Is there a major downside I am missing? Internally, for example, a browser could re-use implementation details for both |
Hi @trusktr , I need to read through your ideas, but having just skimmed it, I'm kind of excited to see we might be converging in views -- I've added support for observedAttributes to my proposal, based on the "lightning bolt" that emanated from @sorvell 's comments. However, I was just sitting down to correct my approach -- I think we want the list of observable attributes to be provided in the registration function, rather than a static property. I think the registration is the key place that should control the property name off of the enhancements property as well as the attributes, so we can concentrate all our scoped registry trickery in one spot. I also agree with you I was probably overthinking the namespacing a bit, I think we can probably trust developers to make such judgments for themselves (as long as we insist that attributes have dashes, and I still think we need a prefix when applying to third party custom elements). |
@trusktr, I haven't yet wrapped my brain around the scoped registry solution, and consequently can't foresee what impact it will have on namespacing. I'm waiting for the dust to settle. But my instinct tells me it isn't going to solve every problem under the sun. For example, I still think that we will want to reduce the need for complex and confusing mappings, so that developers will still want to strive to "namespace" their attributes based somewhat on their package name, document their libraries names based on this "canonical" naming, and that we will want to utilize scoped registries to rename things off of their canonical defaults, only when absolutely necessary, due to inevitable name collisions due to the limitations of relying on public npm packages exclusively for claiming "ownership" of a string. If anyone's understanding is different, I would love to get a response if anything I said above is inaccurate, because I'm only guessing, and it is something I feel like I should understand better to be attempting to add my voice to this proposal space. |
I'm so sorry, but I think we need to start thinking of our custom attributes as a tuple of observable strings (rather than the more traditional name/value pair mental model), so that the names can be modified by the party registering the attributes, as well as within scoped registries. |
I use |
There's absolutely nothing to be sorry for. Also I'm not sure what you mean yet. |
@rniwa I'm wondering about particular feedback you have specifically for WebKit's needs. Based on my thoughts regarding
with the final use case being Just wondering what your thought here is since making an alternative to customized built-ins was a primary reason for the conversation. |
So we are all on board with supporting multiple attributes, which is great, excellent idea. In my excitement, I didn't think to check in with you as far as your thoughts with observedAttributes vs has? I think @trusktr and I reached the same conclusion from different vantage points, but wanted to get a show of hands I guess. I'm concerned we may have a split. Syntactically, is there a reason to prefer: <input has="moods" onhappy... onsad... onbored...> over something like: <input ismoody onhappy... onsad... onbored...> or my preference (am I the only one)? <input is-moody on-happy... on-sad... on-bored...> ? Or is there something else I failed to see in addition to wanting to support multiple attributes? Feel free to enjoy your weekend, no rush, I just felt a bit bad for jumping to conclusions I shouldn't have. I've expressed my concerns before, perhaps too harshly, but I would like to understand how to sell something if I am to support it. Equally important to me is whether the restriction I assumed would still hold, holds still -- no dashes. I think it's great if we don't need them. But couldn't the syntax @sorvell provide start triggering events unexpectedly with a new browser version? It seems @sorvell and @trusktr don't believe it matters. The way I look at it, yes, it does mean every attribute needs some sort of prefix. That's the worst aspect. For two words it adds only a slight amount of inconvenience, frankly I find it more readable, easier to type (on the keyboard) than camel case, but most importantly, it increases risk if we stop using them. It seems most globals and element specific attributes these days are getting long multiple syllable at least, so dashes can't hurt. As far as attributes on built-in elements, ff the industry had long ago abandoned such concerns about prefixing, that would be one thing. I know we have for custom elements, which I have mixed feelings about, but I came to peace with it long ago, especially for short words (especially if a little thought is put into it). But the industry generally has not for higher level elements, where the argument grows stronger, in my mind (maybe not so much now, but I believe it will with time). Why rock the boat on that question? If this is somehow magically resolved by scoped registry, that's all I need to know, and that would be a great surprise. I will note that others have honestly reported encountering this issue with custom elements, quite recently, so it's a real issue in my mind. @LeaVerou, I'm totally with you as far as wanting to link up attributes with properties, parsing, etc. I'm okay with requiring that be fixed as part of this proposal. I think your idea to filter on Element types is a great one, and I've added it to my proposal with attributions. But I just can't wrap my brain around this, and honestly, I doubt I will be alone in this: class MyAttribute extends Attribute {
connectedCallback () {
this.ownerElement.behaviors.add(Htmx);
}
disconnectedCallback () {
let hasOthers = this.ownerElement.getAttributeNames().some(n => n.startsWith("hx-");
if (!hasOthers) this.ownerElement.behaviors.delete(Htmx);
}
} I think it is much easier to group attributes together, like developers are used to, like @trusktr and I agree, via a finite list of observed attributes, bundled together in a class that I think would make much more sense calling "ElementEnhancement", as I've done in my proposal. We need to be precise in which attributes are mixed together to form one enhancement. I just get brain fog when I look at that code. Is it mutual? Do you get the same brain fog looking at my proposed alternative: I am purposely not hiding any of the complexity, to showcase how consumers of the enhancement can rename all the attribute within a scoped registry: //canonical name of our "custom prop", accessible via oElement.enhancements[canonicalEnhancementName],
//which is where we will find an instance of the class defined below.
export const canonicalEnhancementName = 'logger';
//canonical name(s) of our custom attribute(s)
export const canonicalObservedAttributes = ['log-to-console'];
customEnhancements.define(canonicalEnhancementName, class extends ElementEnhancement {
attachedCallback(enhancedElement: Element, enhancementInfo: EnhancementInfo){
const {observedAttributes, enhancement} = enhancementInfo;
const [msgAttr] = observedAttributes;
// in this example, msgAttr will simply equal 'log-to-console',
// but this code is demonstrating how to code defensively, so that
// the party (or parties) responsible for registering the enhancement
// could choose to modify the name, either globally, or inside a scoped registry
// in a different file.
enhancedElement.addEventListener('click', e => {
console.log(enhancedElement.getAttribute(msgAttr));
});
}
}, {
observedAttributes: canonicalObservedAttributes
}); I think @trusktr and I are generally on the same page now on this. BTW, I'm softening my stance on enh- if anyone cares. |
The problem with attributes/behaviors/enhancements and type safetyOne thing I haven't gotten to write my thoughts on yet is type safety. I first created First, let me show problems in TypeScript with the the First, here is a sample with class CoolButton extends HTMLButtonElement {
foo = "bar" // suppose we add a new property "foo", and values from a "foo" attribute get assigned to it.
}
customElements.define('cool-button', CoolButton, {extends: 'button'})
const button = document.createElement('button') // TypeScript infers `button` to be `HTMLButtonElement`.
console.log(button instanceof HTMLButtonElement) // true
console.log(button instanceof CoolButton) // false (!)
// Here TypeScript does not change the type of the `button` variable from `HTMLButtonElement` to `CoolButton`.
// And, umm, is the element supposed to be upgraded when this attribute gets set?
button.setAttribute('is', 'cool-button')
console.log(button instanceof HTMLButtonElement) // still true
console.log(button instanceof CoolButton) // still false (!), and large ding against customized-builtins
const newVariable: CoolButton = button // Type error! (good in this case)
console.log(button.foo) // Type error! (good, because right now it is undefined) Paste that into your browser console and hit enter after removing the type annotations. Then check out the type errors in TS playground. Output:
(This shows a significant problem with Now, let's suppose we do things differently so that class CoolButton extends HTMLButtonElement {
foo = "bar" // suppose we add a new property "foo", and values from a "foo" attribute get assigned to it.
}
customElements.define('cool-button', CoolButton, {extends: 'button'})
document.body.insertAdjacentHTML('beforeend', '<button is="cool-button">btn</button>')
const button = document.querySelector('button')! // TypeScript infers `button` to be `HTMLButtonElement`.
console.log(button instanceof HTMLButtonElement) // true
console.log(button instanceof CoolButton) // true
const newVariable: CoolButton = button // Type error! (bad, it actually is a CoolButton!)
console.log(button.foo) // Type error! (bad, the property exists!) Try it in console after stripping types, and here's the TS playground showing type errors. A similar problem can manifest itself with custom attributes, behaviors, and enhancements, when they both listen to additional attributes on an element and they observe those attribute changes via JavaScript property values (very common in custom element libraries, and libraries similar to custom element libraries (customattributes/behaviors/enhancements) where similar patterns are replicated). For sake of example, suppose we define a behavior (but similar applies with custom attributes and enhancements, just varying syntax) that listens to a "foo" attribute on its host element, and the way that it sets this up is via a decorator that does two things: observes the "foo" attribute and observes the "foo" property on the host element (taking into consideration that maby frameworks today bypass attributes and set properties directly on custom elements and therefore the behavior needs to observe properties because frameworks are setting JS properties): <body>
<div has="coolness" foo="bar"></div>
</body> @behavior('coolness')
class Coolness extends ElementBehavior {
@attribute @receiver foo = "initial"
connectedCallback() {
// Log this.foo anytime it changes (which will have been due to either the host's "foo" attribute changing, or the host's "foo" property changing).
createEffect(() => console.log(this.foo))
}
} where We start to experience some of the same problems with type safety with the following added code: const div = document.querySelector('div') // TypeScript infers this to be HTMLDivElement
div.foo = "bar" // Type Error, but it works! Logs "bar" to console. It is important for a behavior to be able to observe properties, not just attributes, due to today's frameworks allowing both attributes and properties to be set via delarative templating systems, with the preference being on JS properties. In plain HTML, the following will cause the behavior to map the host element attribute value to the behavior's JS property: <body>
<div has="coolness" foo="bar"></div>
</body> In Lit, the following template set an attribute, and so the behavior with "foo" in return html`
<div has="coolness" foo="bar"></div>
` Lit has syntax for setting properties on an element, bypassing the attributes, so we need to write robust implementations that can handle this: return html`
<div has="coolness" .foo="bar"></div>
<some-custom-element has="coolness" .foo="bar"></some-custom-element>
` where This iogui doc page states:
Solid's const elements = html`
<div has="coolness" foo="bar"></div> <!-- sets an attribute by default -->
<some-custom-element has="coolness" foo="bar"></some-custom-element> <!-- sets a property by default -->
<div has="coolness" prop:foo="bar"></div> <!-- explicitly set property -->
<some-custom-element has="coolness" attr:foo="bar"></some-custom-element> <!-- explicitly set attribute -->
` In today's landscape custom attributes, behaviors, enhancements, or any similar concept, need to be robust and handle both attributes and properties. In the above examples, the You could, in practice, define a certain custom element that can have a certain set of behaviors on it, and you can augment the type definition so that the element class will have all possible properties of all possible behaviors as optional properties, which is very ugly (real-world example). This is fairly bad because it means a library (f.e. Lume) has to define up front what possible behaviors are known to be placeable onto a In Lume's current state, when you write this code in VS Code: const mesh = document.querySelector('lume-mesh')
mesh. then VS Code will begin to show possible auto-completions, which will include a list of all possible properties even if they are properties from behaviors that are not currently added to the element: What if the But this example of pre-defined behaviors in Lume is not even a usable concept with behaviors that are generic to be applied onto any element. Imagine if, for example, a behavior/attribute/enhancement author goes and augments the An alternative pattern, but not a replacement for is=""I am contemplating to add a new pattern to Lume, where instead of behaviors being added via the This HTML, <lume-mesh has="phong-material" color="red"></lume-mesh>
<lume-mesh has="physical-material" clearcoat="0.7"></lume-mesh> will change to this: <lume-mesh>
<phong-material color="red"></phong-material>
</lume-mesh>
<lume-mesh>
<physical-material clearcoat="0.7"></physical-material>
</lume-mesh> This is a lot cleaner because:
This "behavior element" pattern be a lot easier to work with (easy to define type defintions simply as properties on a class, and without a bad intellisense story), and a lot easier to compose in React/Vue/Svelte/Solid/Angular/etc. TLDRThe concept of element behaviors, custom attributes, and element enhancements, still have a place. They will be more useful in cases that do not listen to arbitrary host element JS properties (as far as type checking goes), or cases that listen only to attributes (but then The "behavior element" pattern does not solve the problems that <table>
<tr>
<behavior-for-the-tr></behavior-for-the-tr>
<td></td>
</tr>
</table> The parser will move the <behavior-for-the-tr></behavior-for-the-tr>
<table>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table> which means the An upside of custom attributes/behaviors/enhancements is they can still be useful for solving cases like with But generally speaking, for other cases, I would like to migrate to the children-as-behaviors format for better type safety and composability. Perhaps ironically, as compared to styling an element with certain behaviors/attributes/enhancements by using attribute selectors, styling elements with certain "behavior elements" would be done using the In Lume we have a goal to be able to move our Lume-specific features out of HTML and into CSS, taking place of behaviors, f.e.converting either of these <lume-mesh has="phong-material" color="red"></lume-mesh>
<lume-mesh has="phong-material" color="red"></lume-mesh>
<lume-mesh has="physical-material" clearcoat="0.7"></lume-mesh>
<lume-mesh has="physical-material" clearcoat="0.7"></lume-mesh> <lume-mesh><phong-material color="red"></phong-material></lume-mesh>
<lume-mesh><phong-material color="red"></phong-material></lume-mesh>
<lume-mesh><physical-material clearcoat="0.7"></physical-material></lume-mesh>
<lume-mesh><physical-material clearcoat="0.7"></physical-material></lume-mesh> into something like this: <lume-mesh class="one"></lume-mesh>
<lume-mesh class="one"></lume-mesh>
<lume-mesh class="two"></lume-mesh>
<lume-mesh class="two"></lume-mesh>
<style>
.one {
--material: phong;
--material-color: red;
}
.two {
--material: physical;
--material-clearcoat: 0.6;
}
</style> I think that we'll have some sort of system that applies "behaviors" from CSS, then overrides with the attribute values from "element behaviors" or "behavior elements". I'm not sure which pattern is better for it. Maybe if the concept of a behavior is totally abstracted so that |
|
@trusktr, I feel like maybe there is a question of whether Typescript/JSX is the cart or the horse. To my mind, the most relevant question is not "what does Typescript/JSX support today"? but rather "could JSX/Typescript be trained to support whatever we come up with?", and we should come up with the best solution, considering that additional question as a secondary consideration. I was extremely excited when Typescript was announced, I spent all night playing around with it the day it came out, but, perhaps this is an old-fashioned view, one of the things that really excited me about it is that it took an extremely subservient view to the standards at the time ("we will adopt to the standards (which we may influence a bit) even if it breaks backwards compatibility when they come out, and not the other way around"). I've not seen the same humbleness from many JSX advocates, but that's another story for another day. Anyway, do you see any reason Typescript/JSX couldn't be trained to support autocomplete / compiling support for something like: <table>
<tr [behaviorForTheTr
myFirstProp:={myListItem}
mySecondProp:={isCollapsed}
my-first-attribute?={isOdd}] >
<td></td>
</tr>
</table> ??? |
I feel I've already tackled most of the complexities with CEs but am willing to open that can of worms again. The current approach I've implemented is That said, it's a pain to scaffold, but it works. And I know it works because I run over 1000 W3C spec tests to confirm. (Even fixing out-of-spec issues in Safari for But no beginner creating Some attributes aren't bidirectional with what's in the DOM. For example:
If there's no parity with I prefer overloading the native attributes names and properties names, but being forced to use custom is fine. I prefer being able to swap Attributes emitting events sounds good. Performance concerns versus Typing is always a mess. I do have some automatic, no transcompile needed, support for Typescript but the parser is rather subpar when working with pure JS files. I do feel once something become native in the browser, TS team will do what's needed to be parsed right. There is very limited support for any of our adhoc Attribute/Property solutions. I've tried and resigned to not bother expecting JS structures to get first-class support. I'm not interested in writing once in JS and then again in a I'll have to deepdive later into what other attributes can prove problematic, but this proposal just hit my radar, and wanted to add my concerns from experience dealing with CE/FACE. |
I think it is unlikely for JSX to go in that direction. But with that said, it seems from your snippet, that the attributes inside the brackets are for the behavior right? In that case, maybe this is better: <table>
<tr>
<behavior-For-The-Tr myFirstProp="..." mySecondProp="..." my-first-attribute="..."></behavior-For-The-Tr>
<td></td>
</tr>
</table> (that's HTML, but interpolation would work in JSX, So that's another question for some use cases: can we fix parsing? Or is that impossible? |
Here's another element behaviors lib from 12 years ago by @robb1e! https://github.com/elementaljs/elementaljs It's a bit simpler, no life cycle methods (presumably in that time you'd use DOM Mutation Events for that). |
Thanks for the mention. I'm happy to share background on ElementalJS if useful. It came out of work developed by @nertzy on projects we were working on many years ago. |
Hi @trusktr I'm fine if you prefer to use separate custom element "element behaviors", essentially abandoning/undercutting the whole premise for our proposals (including yours), the moment we seemed to reach a consensus between us, annoying as that is 😄. Go for it! The good news is the platform has everything that you need already, other than perhaps a little more flexibility with some HTML tags, like the table tag, which I'm fine if the platform feels the need to address. What I'm not so keen on is that the premise you seem to have for why the rest of us should all abandon wanting to enhance elements themselves, is that it doesn't play nice with current JSX, and a vague "that's not a direction JSX would go in". I view JSX as a tool that is supposed to help us, not hinder us. I just don't find that argument compelling. And it seems to ignore the fact that countless frameworks/libraries, which I've linked to in my proposal, to which I may add elementjs, find it useful to want to enhance elements directly, including React, which adds "react fiber" objects to the elements themselves. My proposal is attempting to "formalize" what the king of all JSX frameworks is doing already, in an interoperable way. I feel like I'm on some infinite loop, spanning decades, of bringing up points already raised with the same cast of characters ( 😄 ), but anyway, the other reason I don't find that argument compelling, is that some JSX based libraries like SolidJS and Astro, have in fact started using attributes in the JSX to institute behavior like functionality: <For each={state.list} fallback={<div>Loading...</div>}>
{(item) => <div>{item}</div>}
</For> Why not carry over that concept to adorning built-in or custom elements? If you don't like the bracket idea for grouping related properties and attributes together, maybe you prefer a common prefix approach? In the example above, focusing on the fallback attribute, solidjS is using curly braces to group things, rather than square brackets, as I suggested. I didn't put much thought into which would be better, I couldn't care less between the two, maybe using curly braces is a direction JSX could go with? In fact, for quite a while, I was following the path you appear to be on now, for example with this approach. I did struggle with the question, especially as I liked the way separate elements could allow the attributes to form "complete sentences" via boolean, prefix-less attributes. And also, hoping that what the platform had already provided was sufficient, no need for requesting more. But with time, I have evolved to think it is much better to group related things closer / more tightly together. I believe there are performance benefits. Also, it is easier for copy/cut and paste, and also lends to a better api (especially when we want to combine multiple behaviors together for the same element): myTRElement.enhancements.expander.isExpanded = true; vs. let myExpanderBehaviorIHope = myTRElement.firstChild;
while(!(myExpanderBehaviorIHope instanceOf MyExpanderBehavior)){
myExpanderBehaviorIHope = myExpanderBehaviorIHope.nextElementSibling;
}
myExpanderBehaviorIHope.isExpanded = true; Not to mention the ambiguity non experts using the library are likely to face:
Not all such element behaviors could be child elements. For example, what if you want to enhance the input element, which would require more platform parsing adjustments, I think. Just to placate one vision of where JSX ought to go? PS, to answer one of your questions: <table>
<tr [behaviorForTheTr
myFirstProp:={myListItem}
mySecondProp:={isCollapsed}
my-first-attribute?={isOdd}] >
<td></td>
</tr>
</table> This was my passive-aggressive way of continuing to suggest that we are overemphasizing the attribute aspect of what I want, at least. The only part of the example above that would be an attribute is the one that has an attribute in the name, used only for styling purposes -- my-first-attribute (yes, modern css doesn't require that, it was just trying to illustrate a point with a simple example). All the rest would be properties, hence my use of props in the names. The tr element would have a property that is accessible via oTR.enhancements.behaviorForTheTr, to which JSX could pass a non-JSON serializiable listItem via (behind the scenes): oTR.enhancements.behaviorForTheTr.myFirstProp = myListItem;
oTR.enhancements.behaviorForTheTr.mySecondProp = isCollapsed;
if(isOdd){
oTR.setAttribute('my-first-attribute');
}else{
oTR.removeAttribute('my-first-attribute');
} The latter statement would pass through the behavior/enhancement's attributeChangedCallback (but I've been investigating a better api for that over time). |
@trusktr we can't remove Foster Parenting generally for web compat reasons. The |
The API is nice, but HTML engines do have an attribute node and Attr type interface Attr extends Node { Based on this concept, in <custom-element tag="dce-link" hidden="">
<attribute name="p1">default_P1 </attribute>
<attribute name="p2" select="'always_p2'"></attribute>
<attribute name="p3" select="//p3 ?? 'def_P3' "></attribute>
p1:{$p1} <br> p2: {$p2} <br> p3: {$p3}
</custom-element>
<dce-link></dce-link>
<dce-link p1="123" p3="qwe"></dce-link> More live samles As for imperative declaration, the
|
@bahrus I don't want anyone to abandon the behaviors-or-similar idea. If the parser could be changed, then the behaviors-as-children idea would be an alternative for the main issue: lack of support for the As for the JSX stuff @bahrus,Solid.js is using existing JSX syntax, it did not change any syntax (the stuff it does with curly braces is valid JSX (inside the curlies is just JavaScript)), and remains compatible with tooling like TypeScript, etc. JSX itself does not have any runtime specification, it is only a syntax specification, and frameworks like React, Solid, etc, can create any runtime output they want when they compile JSX to JS. Maybe if we want to have the best effect, we can propose something for HTML instead of JSX, and then every markup language including JSX would likely want to make space for it if possible. Currently, the easiest way to get support with existing tools (f.e. TypeScript+JSX) is with elements, not currently with behaviors-or-similar. I agree that behaviors-or-similar can be totally useful, especially when type checking is not in play. Is it easier to introduce behaviors-or-similar, or to update parsing? (seems like the former is) Behaviors via syntax?Is it possible to propose new (or moreso, modified) syntax behavior for HTML? <table>
<tr
#my-behavior(foo="asdf" bar="blah")
class="row"
#other-behavior-without-attributes
></tr>
</table> This is currently valid HTML, so not necessarily new, but people don't generally write attributes like that, so a subset of HTML could potentially be reserved for a new feature? For non-parser-blocked scenarios (f.e. "foster parenting"), syntax could allow it as a child-parent association in the markup: <div>
<p class="row">
<!-- these behaviors apply to the <p> -->
<#my-behavior foo="asdf" bar="blah" />
<#other-behavior-without-attributes />
</p>
</div> In that last example, although the markup looks as though behaviors are children, the instances would not appear in On this question on if a syntax feature like this is possible, if it is possible, then maybe we can support primitive types too (maybe similar to JSON)? For example: <p id="myPara">
<#my-behavior foo="0" bar="'blah'" baz="true" lorem="[0, true, 'foo']" />
</p>
<script>
const myBehavior = myPara.behaviors.myBehavior
console.log(typeof myBehavior.foo) // "number"
console.log(typeof myBehavior.bar) // "string"
console.log(typeof myBehavior.baz) // "boolean"
console.log(Array.isArray(myBehavior.lorem)) // true
</script> Improved HTML?And if we could do this, we'd probably want this ability on regular elements somehow (namely custom elements): <p id="myPara" #foo="0" #bar="'blah'" #baz="true" #lorem="[0, true, 'foo']"></p>
<script>
console.log(typeof myPara.foo) // "number"
console.log(typeof myPara.bar) // "string"
console.log(typeof myPara.baz) // "boolean"
console.log(Array.isArray(myPara.lorem)) // true
</script> (There'd be no lexical scope, like JSON). The only thing missing so far would be boolean attributes ( <p id="myPara" ?foo="true" ?bar="false"></p>
console.log(myPara.hasAttribute('foo')) // true
console.log(myPara.hasAttribute('bar')) // false (that's based on Lit's If we could adopt <p id="myPara" .lorem="[0, true, 'foo']"></p>
<script>
console.log(Array.isArray(myPara.lorem)) // true
</script> If we had the above syntax features, then it would encourage the following problem to be solved in all frameworks: Right now frameworks are just confused, no one knows if I think a parser update could really help us a lot. Engines already have Sorry, that was on a slight tangent, but maybe its a space that is related (better syntax for elements with room for behaviors-or-similar). |
I don't know how well this will fit in with this proposal, but for what its worth I am throwing my hat into the ring: I have just publicly released my own version of Lume's Element Behaviors (@trusktr) that I have been using privately for my business/projects. I think it may be worth considering some of the differences as part of this proposal. The interactive manual (documentation) includes a Limitations and Modified Behavior section that I imagine specification authors would like to read, as well as live examples. |
I have a use case (which might resemble many other use cases). I want to create a “current nav link” element. Today, I would use the following markup: <a href=/>Home</a>
<a href=/about aria-current=page>About</a> Instead, I’d like to use a custom attribute called <a href=/ nav-link>Home</a>
<a href=/about nav-link>About</a> Instead of sprouting additional attributes, I’d like the a[nav-link]:state(current) {
text-decoration-thickness: 3px;
} Both require
|
@knowler your comment reminded me to import the list of custom attribute use cases I have been maintaining, I added it to the end of the first post. |
I haven't consumed this whole thread, but I'm pretty sure these points haven't been raised: I'm assuming that the dashes requirement is to avoid clashes with 'official' attributes. This doesn't work, because with reflection, a When automatically mapping attribute and property names, it's usually better to start from the property name. I know this isn't a real example, but If reflection is supported, the attribute should be the source of truth. That's how it works in HTML. |
I agree and I'm not sure custom attributes should be adding properties to elements. Custom Elements should be free to add the property which can look up the custom attribute node, and the custom attribute should be able to internalise its own reflection rules and expose them (perhaps via a
However I would like to point out that we could easily specify a deny list that clashes with existing names. We already have that for custom elements; you cannot for example define |
@jakearchibald Great point wrt prop namespacing. Some ideas off the top of my head:
@keithamus I think custom attributes would be quite crippled without props. A big part of this proposal is making attribute-property reflection easier for both custom elements and built-ins. The idea is that many things that people today make components about, are actually traits that would be more suited as a modifier (i.e. an attribute) on other elements, rather than a separate component. See the list of use cases in the OP. |
Right, but the real problem is when the platform name tries to arrive later. |
To me it seems quite straightforward for a component to integrate with a custom attribute by wiring up a getter/setter, and avoids a whole lot of issues that we'd have in proposing an automated way to do this. For example: customAttributes.define('my-long', class extends Attr {
get value() {
return Number(super.value)
}
set value(value) {
super.value = value;
}
});
customElements.define('my-consumer', class extends HTMLElement {
get myLong() {
return this.getAttributeNode('my-long').value
}
set myLong(value) {
this.setAttribute('my-long', value)
}
}); ^ That's not that much code and userland libraries can reduce the boilerplate (as they do today), but it saves us having to specify some incredibly messy things which may stall or block such a proposal from existence due to the complexities. Such as the cases @jakearchibald pointed out. There are however more troublesome cases; platform conflicts are one issue but so are userland conflicts... what do you do about attribute definitions clobbering properties on existing custom elements? Either the custom element wins in which case the automagic consistent application of the attribute is no longer consistent, or the attribute wins which breaks the prescribed contract of the custom element. I'd also just like to point out that reflection rules are quite specific but they're not coupled to the property name. In fact HTML is quite inconsistent mapping content attributes to IDL; I'm sure people in this issue are aware of these but for example |
Btw in case it's useful, I just did some research on existing native props, and I'm now less optimistic that we can carve out a path for prefixes that still allows reflection into native-like props: https://codepen.io/leaverou/pen/mdNjGod?editors=0110 I’m on board with leaving the ergonomics up to userland to help unblock this. As long as we don't paint ourselves into a corner and are able to improve ergonomics later. Basically, the MVP is:
Something like: HTMLElement.customAttributes.define("foo-tooltip", class FooTooltip extends Attribute { ... });
HTMLVideoElement.customAttributes.define("start-at", class StartAtAttribute extends Attribute { ... }); With a custom attribute being defined on a superclass being available in all subclasses (so defining it on Though do public properties and methods on these classes become element properties and methods? If so, that could increase concerns about clashes. I.e. do these classes basically become HTMLElement mixins? If so, TC39 folks could push back as this could be seen as encroaching on existing TC39 work on class mixins. |
Can reflection be changed to work one way with no |
This proposal introduces an API for defining custom attributes for both built-ins and custom elements, and design discussion for an API that can be used to define more complex enhancements that involve multiple attributes, methods, JS-only properties etc.
This came out of the TPAC extending built-ins breakout, and some follow-up discussions with @keithamus.
Defining attributes: The
Attribute
classUse cases
Prior art
Needs
API sketch
HTMLElement.attributeRegistry
property which is an AttributeRegistry object. Attributes can be registered generically onHTMLElement
to be available everywhere, or on specific classes (built-in or custom element classes).aria-*
, SVG attributes etc). Or maybe this should be a validation restriction, and not actually enforced by the API?Attribute
should extendEventTarget
so that authors can dispatch events on it.Definition:
Usage on built-ins or existing custom elements:
An optional options dictionary allows customizing the registration, such as:
propertyName
to override the automatic camelCase conversionUsage on new custom elements:
We could also add a new static
attributes
property as syntactic sugar to simplify the code needed and keep the definition within the class:Types
In v0 types could only be predefined
AttributeType
objects, in the future these should be constructible (all they need is aparse()
andstringify()
function, and maybe an optionaldefaultValue
to act as a base default value).Open Questions
Complex Enhancements
Complex enhancements include:
Attribute
objects)This can be fleshed out at a later time, since
Attribute
already deals with a lot of use cases. That could give us time to get more data about what is needed.Prior art
Needs & Design discussion
Referencing: How to associate enhancements with elements?
Element behaviors use a
has
attribute that takes a list of identifiers, and that is totally a viable path. The downside of this is that it introduces noise, and potential for error. E.g. imagine implementing a language like VueJS in this way, one would need to usehas
in addition to anyv-*
attribute, and would inevitably forget.Associating enhancements with a selector would probably afford maximum flexibility and allows the association to happen implicitly (e.g. for an Enhancement implementing htmx, the "activation selector" could be
[hx-get], [hx-post], [hx-swap], ...
, without an additionalhas="htmx"
being required eveyrwhere that these attributes are used.Imperative association could allow us to explore functionality without committing to a particular declarative scheme. That way, individual attributes can still automatically activate behaviors, though it's a bit clunky:
Flexibility
Should enhancements allow the same separation of names and functionality as custom elements and attributes? Given that they include multiple attributes, how would the association happen? I'm leaning towards that they'd also name the attributes they are including (since they are naming properties, methods etc already anyway).
List of use cases
I have been maintaining a list of use cases that I periodically add to here, I will edit this periodically to import it:
Use cases for custom attributes
format
attribute on<time>
,<data>
for presenting data with custom formatsname
<button>
<button href>
prefix="icon-name"
/suffix="icon-name"
)<p loading-placeholder="3 sentences">
)highlight="foobar"
)removable
(adds X button that removes the element and fires a suitable event)<table sortable>
<td value>
to be used for filtering and sorting<pre>
<pre src>
<pre editable>
for code editors (or any other element)<pre normalize-whitespace>
<audio>
and<video>
)start-at="0:05"
The text was updated successfully, but these errors were encountered: