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

[idea] Ability to define Custom Element attributes (and optionally enable getters/setters for those attributes). #517

Closed
trusktr opened this issue Jun 7, 2016 · 15 comments

Comments

@trusktr
Copy link
Contributor

trusktr commented Jun 7, 2016

Skate.js, X-Tags, and Polymer all have a feature that allows a Web Component creator to specify attributes and associated setters/getters.

I think this is a really good idea because it makes things easy, but more importantly, it can enable integrations with other libraries more easily.

For example, suppose we can specify attributes somehow. Imagine a setter/getter on the HTMLElement prototype possibly:

class MyElement extends HTMLElement {
  constructor() {
    super()

    // optional use of the setter
    this.attributes = [
      "foo",
      "bar",
    ]

    // optional, but does nothing if no attributes are defined.
    // It's opt-in, otherwise setAttribute/getAttribute work as before, backwards
    // compatible with current spec.
    this.enableAttributeSettersGetters()
  }

  // if "foo" attribute was specified during creation (HTML engine would check
  // this after the constructor call), then it will use (if present) the
  // specified matching getter/setter on the instance to get/set values for the
  // attribute. This could be nice because it would mean that
  // this.getAttribute('foo') could return an object instead of a string, and
  // this.setAttribute('foo', ...) could accept anything, not just strings!
  set foo(value) { ... }
  get foo() { ... }
}

// ...

let el = document.querySelector('...')
console.log(el.attributes) // getter

This has two major benefits from a programming perspective:

  1. This is a nice shortcut for something that many people will want to do anyways (Skate.js, X-Tags, and Polymer all implement some form of this feature).
  2. Other libraries like jQuery would be able to reliably determine which attributes are explicitly defined on an element, and would therefore be able to automatically create getter/setter proxies. For example, this would allow the following jQuery code to be possible without string conversion (assuming jQuery takes advantage of a such a feature when/if it becomes reality):
    js // assign an attribute value to all selected elements! $('#awesome-element').attr('hello', new AnythingThatIsNotAString()) // or $('#awesome-element').hello = new AnythingThatIsNotAString()

I think that right there lends to some powerful new possibilities and could be one way to avoid having to convert everything to/from strings! Of course, things like Chrome's element inspector would then ultimately call toString on the things returned from the attribute setters/getters, so we might see things like the following when care is not taken:

<div foo="[object Object]">

But, this presents the opportunity for component developers to define toString methods! Furthermore, just like now, developers can continue to accept string input (that may match their toString output format), which would be really cool!! This even presents an opportunity for DevTool to possibly show objects instead of strings (f.e. right click > "Show value in console") and then the non-string value can be inspected!

There's performance benefits!

  1. When the an HTML object's properties are not being observed (for example by devtools) then there's an opportunity for values passed to/from attributes to never ever be converted to/from strings. The end user of a Custom Element can take advantage of this by simply never passing string values to setAttribute/getAttribute or the matching setters/getters.
  2. libraries like React will not have to convert values to strings any more, and can just pass the JS values directly.
@trusktr
Copy link
Contributor Author

trusktr commented Jun 7, 2016

The attribute property could possibly be a setter on the HTMLElement base class, which is what could trigger the HTML engine to take note of the specified attributes.

Alternatively, it could be a method:

class MyElement extends HTMLElement {
  constructor() {
    super()

    // optional
    this.defineAttributes([
      "foo",
      "bar",
    ])

    // optional, but does nothing if no attributes are defined.
    // It's opt-in, otherwise setAttribute/getAttribute work as before, backwards
    // compatible with current spec.
    this.enableAttributeSettersGetters()
  }

  // if "foo" attribute was specified during creation (HTML engine would check
  // this after the constructor call), then it will use (if present) the
  // specified matching getter/setter on the instance to get/set values for the
  // attribute. This could be nice because it would mean that
  // this.getAttribute('foo') could return an object instead of a string, and
  // this.setAttribute('foo', ...) could accept anything, not just strings!
  set foo(value) { ... }
  get foo() { ... }
}

// ...

let el = document.querySelector('...')
console.log(el.getDefinedAttributes())

But I think I like the attributes setter/getter better than a method, because then we require a second method like getDefinedAttributes or something (trying to avoid confusion with setAttribute/getAttribute).

@trusktr
Copy link
Contributor Author

trusktr commented Jun 7, 2016

The behavior of the style attributes of elements, for example, would remain completely explainable as per the Extensible Web Manifesto after an addition like this.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 7, 2016

The current limitation in Skate.js, X-Tag, and Polymer is that setAttribute/getAttribute only work with strings currently, so there's no actual way to pass JS literals by reference or value with those. It is possible to call the setters/getters directly though. This change would allow both setters/getters and setAttribute/getAttribute to be used interchangeably without worrying about that string problem.

@rniwa
Copy link
Collaborator

rniwa commented Jun 7, 2016

We specifically discourage people from adding attributes inside a constructor because no builtin elements add attributes during construction. As such, this is somewhat of an anti-pattern in custom elements land.

@treshugart
Copy link

@trusktr Skate already provides a way to do something similar to what you want. I don't think allowing non-string values to go to, or come back from setAttribute() / getAttribute() is the best idea considering eventually they'd have to be serialised anyways and it requires overriding them if it never gets spec'd.

Skate offers an API to serialize to and deserialize from attributes. Our react-integration will force setting props, so with the combination of Skate's property definitions and that, you'll get React components built from Web Components (not just Skate components).

Not sure exactly if this answers your questions, but if you have a question about Skate specifically, please feel free to raise it there.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 8, 2016

@rniwa

because no builtin elements add attributes during construction.

It may be true now, but the spec can be modified. That's not what the element's currently do, but that could be what they do in the future, as part of what explains how things work (following the Extensible Web Manifesto)...

@treshugart

...I don't think allowing non-string values to go to, or come back from setAttribute() / getAttribute() is the best idea considering eventually they'd have to be serialised anyways...
Skate offers an API to serialize to and deserialize from attributes.

That seems like a performance bottle neck when it comes to passing data from outer component to inner component, and so on. It'd be nice for anything that is passed via attributes to remain exactly as it is until it gets to where it needs to be (the thing being passed might flow into some inner, inner tree).

What I'm imagining is that component layers might go deep in a big application (layers of nested Shadow DOM trees, and possibly with encapsulated component registration as in #488). Imagine this horrible performance scenario:

  • An app has 3 layers of ShadowDOM trees, each layer is the ShadowDOM of a Custom Element. The elements are A, B, and C.
  • The app developer only uses element A directly, and passes a string into an attribute, where the attribute is called array (to be generic) and accepts an array of numbers:
    html <A array="1,2,3"></A>
  • Suppose that element B, used internally inside of A's shadow DOM, will also accept an array of numbers in some attribute after A has modified the values. Element A will need to deserialize the array, modify the values, then convert it back into a string to pass to element B.
  • Element C is used in element B's shadow tree and also needs to accept an array of numbers. Element B will deserialize the array, do some modifications, then finally serialize it to pass it to C.

Doesn't that sound bad?


In general, any framework or platform (in this case Chrome, Firefox, Edge, Safari) should aim for not using serialization/deserialization until the very last moment, or not at all. Nothing should need to be serialized until necessary (f.e. DevTools may need to show the values in the element inspector, but if DevTools are never used, serialization/deserialization may never even need to happen). Serialization/deserialization should only need to happen when some data needs to be displayed as text unless the data is text, or when saving/loading state from somewhere like a backend database. Serialization/deserialization would be great for server-side rendering, but should be completely avoided until the last minute when it is needed. The average web UI today doesn't not need serialization/deserialization except for when reading from HTML directly (i.e. the initial parsing of HTML coming from a server, using innerHTML, etc), but when the imperative DOM API is being used, serialization/deserialization is completely unnecessary.

In contrast to that Idea, serialization/deserialization is the current default behavior, no matter what the use case, and in my opinion that's awful because it requires unnecessary performance costs.

I'd like to see HTML interfaces being used to create immersive 3D experiences (games or other 3D applications). This is beginning to happen, and as an author of 3D content I would highly appreciate the performance improvement that this change would bring.

@rniwa
Copy link
Collaborator

rniwa commented Jun 9, 2016

because no builtin elements add attributes during construction.

It may be true now, but the spec can be modified. That's not what the element's currently do, but that could be what they do in the future, as part of what explains how things work (following the Extensible Web Manifesto)...

Explain what? That's just not how HTML works. When you create a HTML element, it doesn't have any attributes until a parser, DOM API, etc.. adds one.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 9, 2016

With this change, it'd make the use of things like JSX with native DOM API possible. Imagine JSX compiling to a native DOM API instead of a bunch of the React-like implementations that currently exist. Many of those libraries listed there give us the ability to pas around plain JavaScript data without serializing until the very end when the libraries have to generate real DOM. It would be absolutely great to remove that serialization step.

Imagine if the document.createElement function (and the shadowRoot.createElement from #488) were given a new second parameter that would let us write something like this:

let element = document.createElement('div', {
  attributes: {
    id: "someDiv",
    array: [1,2,3]
  },
  children: [
    document.createElement('span', {children: [
      document.createTextNode('hello')
    ]})
  ]
})

This would open the doors to JSX-like libraries used with native DOM API, so the above could be written as:

let element = (
  <div id="someDiv" array={[1,2,3]}>
    <span>hello</span>
  </div>
)

Notice how the array literal would get passed without performance loss from serialization.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 9, 2016

@rniwa

That's just not how HTML works. When you create a HTML element, it doesn't have any attributes until a parser, DOM API, etc.. adds one.

I know, which is why I've made the overall suggestion of this issue. I've mentioned above that

In general, any framework or platform (in this case Chrome, Firefox, Edge, Safari) should aim for not using serialization/deserialization until the very last moment, or not at all. Nothing should need to be serialized until necessary (f.e. DevTools may need to show the values in the element inspector, but if DevTools are never used, serialization/deserialization may never even need to happen). Serialization/deserialization should only need to happen when some data needs to be displayed as text unless the data is text, or when saving/loading state from somewhere like a backend database. Serialization/deserialization would be great for server-side rendering, but should be completely avoided until the last minute when it is needed. The average web UI today doesn't not need serialization/deserialization except for when reading from HTML directly (i.e. the initial parsing of HTML coming from a server, using innerHTML, etc), but when the imperative DOM API is being used, serialization/deserialization is completely unnecessary.

Currently, the DOM API around HTML attributes expects everything to be serialized/deserialized to/from strings 100% of the time if HTML attributes are to be used for passing data (which will happen betweenWeb Components). What I'm suggesting is that serialization/deserialization does not need to happen 100% of the time, and in many cases it can be entirely avoided depending on the needs of the application.

For example, I imagine I'll want to serialize things when saving to a server, and would want to deserialize things when receiving the initial HTML payload from a server. But, as far as dynamic application go ("single-page apps", "ajax", client-side routing, etc), serialization and deserialization during runtime are completely unnecessary.

I believe that the current state of strings-only for attributes stem from the old days when everything was server-side rendering of HTML, so strings were fine.

But, we're in a new era where single-page apps allow for dynamic experiences, and to make this change will help improve those experiences.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 9, 2016

Opened #519 specifically about attributes accepting anything besides strings.

@trusktr trusktr changed the title idea: easy way to specify attributes (and optionally enable getters/setters). [idea] Easy way to specify attributes (and optionally enable getters/setters). Jun 9, 2016
@trusktr trusktr changed the title [idea] Easy way to specify attributes (and optionally enable getters/setters). [idea] Ability to define Custom Element attributes (and optionally enable getters/setters for those attributes). Jun 9, 2016
@treshugart
Copy link

Sure, performance could degrade but what you want isn't possible without overriding set/getAttribute which would create another performance bottleneck, unless it were implemented natively, which I don't see happening anytime soon, if ever. Your performance issue is also hypothetical. For your use case, I would suggest you use properties for everything and don't have linked attributes. Properties are faster to set by a factor of about 3 on any element, anyways.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 12, 2016

@treshugart HTML elements are the standard that everyone uses. You've proposed a workaround, but it's not really a solution because libraries (React, Angular, virtual-dom, Riot.js, etc) can't guess what properties a Custom Element uses. f.e., React won't know which property to use for some-attribute in <my-button some-attribute={foo} />). HTML attributes are the explicit interface of communication (attributes are element properties) to/from DOM (including from the server-side, noting that JS properties exist only on the client-side). More details in #519.

Now that we'll be writing ES6 classes, there's no reason why setAttribute calls should need to leave the JavaScript environment for Custom Elements, so the factor of 3 that you mentioned can probably be eliminated for Custom Elements if the browser implementation is designed to take advantage of this fact.

@treshugart
Copy link

@trusktr What I was saying was more akin to what Seb was saying in #519: prefer properties, in general. IMO a better solution would be for HTML, when parsed, to take the attributes and apply them as properties. You may have a dash-case / camelCase problem then, but that's easily solvable by applying the same property descriptor to the dash-case version and the camelCase version.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 29, 2016

IMO a better solution would be for HTML, when parsed, to take the attributes and apply them as properties.

I think I like that too, but is that backwards compatible? Current native elements don't all do that, so then we'd be adding a rule that doesn't explain existing technology properly (web manifesto) because existing elements don't follow it, and we can't change those elements to follow it because backwards compatibility.

So, what can be the backwards-compatible solution? Ability to specify certain attributes as properties/getters/setters agrees with web manifesto because then for existing native elements that don't have the properties we can just say that those elements just didn't define them as per this new API.

@domenic
Copy link
Collaborator

domenic commented Jul 21, 2016

Let's close this. It appears to be something that is better done with a library.

@domenic domenic closed this as completed Jul 21, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants