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

Element Behaviors, and the has="" attribute. A useful alternative to Custom Elements in many cases! #727

Closed
trusktr opened this issue Feb 1, 2018 · 17 comments

Comments

@trusktr
Copy link

trusktr commented Feb 1, 2018

Based on #509, #662, and #663, I've released a standalone implementation of "Element Behaviors", published to NPM as element-behaviors.

I currently use Element Behaviors in Lume to implement various rendering behaviors for custom 3D elements, and I plan to use element behaviors to define mixable functionalities useful in interactive apps like 3D games or decorated web sites.

Additionally, I've implemented a "default behavior" system in Lume, on top of element-behaviors (not published as standalone yet) that makes it easy to define Custom Element classes that ship with default sets of behaviors, as well as makes it easy to add/remove behaviors in the same way that it is easy to add/remove classes with el.classList.add/remove.

What is it?

For all intents and purposes, this is an "entity-component" system, but in this case called "element behaviors" which avoids conflict with widely-used term "component" in the web these days.

Element behaviors allow us to add any number of functionalities ("behaviors") to any number of elements, using interfaces similar to Custom Elements, and a new has="" attribute. See the README for more details, examples, and API.

This is an alternative to the is="" attribute, where instead of using an is-a design pattern we're using a has-a design pattern which is less complicated and more flexible.

Basic Example

The first example from the README is on codepen:
https://codepen.io/trusktr/pen/3abb3ab634d1171954fb00fbc188b9e7.

Not shown in that demo, also apply any number of behaviors to any number of elements:

<div has="click-logger other-behavior" attribute-for-other="foo">one</div>
<p has="click-logger some-action">two</p>
<button has="click-logger other-behavior some-action" attribute-for-other="foo">three</button>

mixin-like "element behaviors", without extending builtins

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

For example:

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

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

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

augment existing HTML applications

Suppose we have an HTML app:

<div>
  <h1>my app</h1>
  <input ...></input>
  <button></button>
</div>

With Custom Elements, we could enhance it like the following, but it requires changing the names of the elements, which can cause conflicts with CSS and JS code:

<awesome-layout>
  <h1>my app</h1>
  <awesome-input ...></awesome-input>
  <awesome-button></awesome-button>
</awesome-layout>

With Element Behaviors, we can more easily add the functionality without modifying existing markup, CSS, or JS:

<div has="awesome-layout other-behavior">
  <h1>my app</h1>
  <input has="awesome-input input-validator input-logger" ...></input>
  <button has="awesome-button particle-click-effect"></button>
</div>

Of course, the implementation of a behavior that doesn't extend from the target element might require a different sort of implementation than a Custom Element that extends from the target element. Nothings perfect, and one way may be a little more work than the other way depending on scenario. But overall, the utility is improved (f.e. mix behaviors together, and some behaviors might even team up to do certain things when combined together on a single element), avoid strange parser problems, stay out of the way of existing CSS/JS, and more.

@trusktr
Copy link
Author

trusktr commented Feb 10, 2018

(Updated the description in the previous comment)

@trusktr trusktr changed the title [idea] Element Behaviors, and the has="" attribute Element Behaviors, and the has="" attribute. A useful alternative to Custom Elements in many cases! Feb 10, 2018
@joshbruce
Copy link

joshbruce commented Feb 11, 2018

@trusktr: Thanks for the invite! This is interesting.

I think it's really starting to try and move us to looking at individual HTML elements as actual class definitions from the OOP world. As such, this response might turn into something a bit longer than expected or requested. :)

@joshbruce Seems like you like is="" more than has="". I'm curious to know more on that. Mind commenting on that over at #727, where I've published an implementation of element-behaviors and has=""?

For me it's not a question of "more than" - it's a question of language (communication not programming) and intent.

is in my mind is analogous to class inheritance from class-based languages like PHP, Swift, and so on; therefore, the following would be equivalent representations of the same concept (please forget JS for the purposes of this reply):

<div is="my-custom-block-element"></div>
class Div {
  // block element definitions and properties
}

class MyCustomBlockElement extends Div {
  // my overrides and extensions
}

The has proposal seems more like traits (from PHP) or multiple inheritance, which has its own issues and has been, and I believe continues to be, debated. There's a difference between something being an extension of something else and something having the qualities defined somewhere else. (@colinalford - might find this interesting.)

So, the following would also be analogous - using PHP traits - not JS.

<div has="bar baz"></div>
trait Bar {}

trait Baz {}

class Div {
  has Bar, Baz;
}

I like to write code that is explicit in its intent; I can figure out what's going on by reading it...that's why I still mark things as public despite not needing to in languages that have those keywords and concepts.

It seems like we are trying to do for script what class did for style. However, CSS doesn't have the crazy inheritance that JS can provide.

So, why not both?

<div is="my-cool-contact-card" has="customHover customClick" class="rounded blue"></div>

This is much easier to read and doesn't assume the existence of custom elements support on the client.

<my-cool-contact-card has="customHover customClick" class="rounded blue"></div>

And, it removes the clutter of literal JS and CSS in the element.

In all honesty, I like is from the perspective that it's a valid selector and allows me to have custom named HTML elements today - while the debate and conversation of how that looks continues on. Therefore, I can style and make my intent explicit without using class as a junk-drawer for selection...since we don't seem to like taking advantage of the data-* thing.

I like has from the perspective that it could, in JS, grab the prototypes and implementations without having to run the selectors and finds...if I'm understanding correctly. (I'm finding myself using traits more and more in PHP as well; however, it's usually only when I discover moments when I just want the method - aka banana - and not the gorilla that comes with it...move method out of superclass and into a trait that is then used by the superclass.)

Let's pretend id, class, and style only serve to inform the browser of our aesthetic intent - id there is only one in this document, class it has n number of stylistic traits, and style this one specifically has these styles as well (most likely generated using a template engine or JS).

It's possible that is, has, and script could do the same thing for interactivity (behavior) - is I have all the interactive characteristics of this other thing (button), has I have these additional interactive characteristics (keyboard shortcuts and so on), and script this instance in particular also does these things.

Could be crazy talk, but that's how my mind is mapping these things - taking on the voice of the element itself for a second:

  1. Structure and layout: Am I block or inline?
  2. Style and aesthetics: What do I look like??
  3. Interactivity and behavior: How can people play with me??

I've been toying with the notion of HTML as a language being less about the web and more about giving authors the ability to describe their intent - otherwise, we would only need div and span, because everything else could be accomplished in CSS + JS (or other scripting languages)...p is just a div with expressed intent kinda thing.

@annevk
Copy link
Collaborator

annevk commented Mar 5, 2018

There's no interest in this, but custom attributes (with callbacks) might solve this. JavaScript mixins might make this easier going forward too.

@annevk annevk closed this as completed Mar 5, 2018
@joshbruce
Copy link

@trusktr: Sorry. I think you and I might be on the same page though at a higher level...could be wrong. Shameless plug: https://m.8fold.pub/on-constraints-internet-bandwidth-eab05c20e218 - see highlight midway down the page.

@stefsullrew
Copy link

@annevk What is the current alternative to the ideas proposed here. These would assist us with some of the issues we're encountering. But yet, is keeps getting shot down — along with similar ideas.

@annevk
Copy link
Collaborator

annevk commented Apr 27, 2018

@stefsullrew custom attributes are discussed in whatwg/html#2271. Not sure if there's a concrete API proposal yet, don't think so. Not sure what you mean about is. Apart from Apple everyone is on board there...

@Nashorn
Copy link

Nashorn commented Dec 18, 2018

the "has" attribute idea is clever but not necessary when ES7 lands. In ES7 we would be able to easily define traits on a class using a custom @traits class-level decorator. Problem solved at the class level.

C'mon, keeps the html clean and lean. I think you've seen the abuse that can occur in Angular just because of an ng-if....Oh gosh!

@trusktr
Copy link
Author

trusktr commented Jan 7, 2019

@annevk @Nashorn Hey guys, you both mentioned mixins and @traits at the JS level, but besides the fact that class-factory mixins have existed for a long time, there's issues with them being defined on a class in JS:

  • They aren't declarative, they don't give an HTML author the opportunity to define which features are used in a graphical scene. f.e., this is good:
    <lume-node
      has="obj-model phong-material transform"
      color="pink"
      shininess="30"
      obj="path/to/model.obj"
      position="10 20 30"
      scale="10 10 10"
    ></lume-node>
    Where the obj-model feature/trait/behavior is an instance that reacts to the obj attribute value, phong-material is a feature/trait/behavior that observes the color and shininess attributes, and transform is a feature/trait/behavior that observes position and rotation, etc. Maybe the user doesn't want to load a 3D obj model, but rather use a geometry primitive:
    <lume-node
      has="box-geometry phong-material transform"
      color="pink"
      shininess="30"
      size="20 20 20"
      position="10 20 30"
      scale="10 10 10"
    ></lume-node>
  • They can not be added or removed at runtime.
    • We can update an element from has="foo bar" to has="foo" in order to remove the trait/behavior/feature. F.e., add a special-looking material when the object is selected:
    <lume-node
      has="box-geometry phong-material transform selection-material"
      color="pink"
      selection-style="zebra"
      selection-color="teal"
      shininess="30"
      size="20 20 20"
      position="10 20 30"
      scale="10 10 10"
    ></lume-node>

In other words, the declarative power of being able to add/remove JS features to/from elements, declaratively, is really nice.

@annevk

There's no interest in this

How do you gauge that?


(@joshbruce I liked your ideas in your above reply 👍)

@annevk
Copy link
Collaborator

annevk commented Jan 7, 2019

Please use inclusive language: https://notapattern.net/2014/10/14/ways-men-in-tech-are-unintentionally-sexist/.

As for determining interest, I wrote that comment during a face-to-face meeting, minutes available at #713 (comment).

@trusktr
Copy link
Author

trusktr commented Jan 7, 2019

Please use inclusive language: https://notapattern.net/2014/10/14/ways-men-in-tech-are-unintentionally-sexist/.

Sorry. Based on @Nashorn's GitHub profile, I can not tell whether Nashorn is a guy or gal. I should've used "guys" only if I'd known you both were under the "he/him" pronouns.

As for determining interest, I wrote that comment during a face-to-face meeting, minutes available at #713 (comment).

Can you quickly summarize that part?


Now that you have examples of how I'm using element-behaviors in the above non-imaginary examples, what do you think about it?

Maybe there is not any interest because it simply doesn't exist and people aren't aware of it?

As far as I know, A-Frame is the only big library out there that has anything similar to this, they call it "entity-components" instead of "element-behaviors", but they are the same things. Theirs is only slightly different from behaviors: theirs is more like custom-attributes, and their custom attribute values accept style-like key-value pairs as input into the components (it maps to a POJO inside the component).

As far as I can tell, people who use A-Frame love it.

In my case, element-behaviors is not tied to a specific library (f.e. A-Frame's entity-components are specific to A-Frame, not usable on any elements), and behaviors can be used on any elements.


SIDENOTE I was debating renaming "element-behaviors" to "element-traits" or "element-features" or "element-characteristics", because "behaviors" implies that each feature is associated with an action or verb: a behavior is a manner in which something acts by means of verbs. In contrast a feature or trait or characteristic can be merely the existence adjectives or nouns (f.e. a data object from which to read from and that doesn't execute any logic). Thoughts?

@trusktr
Copy link
Author

trusktr commented Jan 7, 2019

I am already forseeing that I will mostly use my element-behaviors instead of Custom Elements in most cases, as it is simply just more flexible and easier to work with, and doesn't come with a load of compiler/transpiler/backwards-compatibility nightmares.

@trusktr
Copy link
Author

trusktr commented Jan 7, 2019

This is what it looks like to define a new Custom Element with a set of default behaviors in Lume:

import Node from './Node.js'

export default
class ObjModel extends Node {
    static elementName = 'lume-obj-model'
    static defaultBehaviors = [ 'obj-model' ]
}

That defines a <lume-obj-model> Custom Element that has a default obj-model behavior, which is short for <lume-node has="obj-model">.

Here's how we'd use the obj-model behavior without the Custom Element:

            <!-- a lume-node element with an obj-model behavior. The obj-model
            behavior observes the obj and mtl attributes. -->
            <lume-node id="model"
                rotation="0 40 0"
                align="0.5 0.5 0"
                size="0 0 0"
                scale="200 200 200"
                has="obj-model"
                obj="./models/spaceship/ship.obj"
                mtl="./models/spaceship/ship.mtl"
            >
            </lume-node>

And with the simple Custom Element definition above, here's how we'd use the lume-obj-model which includes the behavior:

            <!-- alternatively, the lume-obj-model is a node element that
            implicitly has an obj-model behavior. We've omitted the mtl, so the
            model will have a solid color material: -->
            <lume-obj-model id="model2"
                rotation="0 20 0"
                align="0.5 0.5 0"
                size="0 0 0"
                scale="200 200 200"
                obj="./models/spaceship/ship.obj"
            >
            </lume-obj-model>

This opens up a door for composability of functionalities on an element; ability to compose JS features dynamically (add or remove them any time).

Imagine this,

<red-monster ...>

then a user gets close enough to the monster, so we make it have rage:

<red-monster has="rage-mode" ...>

Then the user gets far enough away, so we remove the rage:

<red-monster ...>

@trusktr
Copy link
Author

trusktr commented Jan 7, 2019

That monster example is more behavioral, but you can see in my previous examples how materials and obj-models can be more traits/characteristics and not implying verbs.

@joshbruce
Copy link

(SIDENOTE I want to rename "element-behaviors" to "element-traits" or "element-features" or "element-characteristic", because "behaviors" implies that each feature

Agreed.

Also, thanks for the compliment on the article @trusktr. Gonna leave an updated link as I begin to retool my digital world, which is including changing the 8fold domains and figure out where the writing should live for real.

Of course, you coulda just been talking about the comment not the long-winded article. In either case, I'm tired and needed to update the link anyway. :)

Your last couple of comments reminds me of what we used to do in Flash-based games. What is the defined line between action and state?

@Nashorn
Copy link

Nashorn commented Jan 10, 2019

@trusktr - the females are cows and young ones are calfs

@trusktr
Copy link
Author

trusktr commented Jan 23, 2019

What is the defined line between action and state?

Exactly. My thought is that something like "traits/characteristics/features" must include at least state, while "behavior" seems to want to include more.

@AndyOGo
Copy link

AndyOGo commented Mar 10, 2019

This is a really great idea and a very constructive feedback.

@annevk
Please reopen it and do not ignore it.

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

6 participants