npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

element-behaviors

v5.0.3

Published

An entity-component system for HTML elements.

Downloads

474

Readme

element-behaviors

Element behaviors are re-usable bits and pieces of logic that we can mix onto any HTML element. We can apply any number of functionalities ("behaviors") to an HTML element.

The next Sparkles demo on CodePen shows how to apply a behavior to multiple elements:

https://codepen.io/trusktr/pen/MWzzNdV?editors=1000

https://github.com/lume/element-behaviors/assets/297678/0289f294-e5ff-4b7c-b30a-61e29aabea51

Apply one or more behaviors onto HTML elements

Element behaviors are useful for assigning features onto HTML elements. They are similar to Custom Elements, but multiple behaviors can be associated with an element.

Element behaviors have lifecycle methods that are named the same as with Custom Elements. This let's us react to the lifecycle events of an element just like a custom element can.

To help spark your imagination, this is what you might do with Element Behaviors. Suppose we are making a Minecraft-like game:

<ender-man has="player-aware holds-block" holds="dirt" position="30 30 30"></ender-man>

<!-- uh oh! The enderman is aware of the player, run!...  -->
<play-er position="40 40 30"></play-er>
<!-- ...the player got away from the enderman, and found diamond armor and a horse -->
<ender-man has="holds-block" holds="sand" position="-20 38 40"></ender-man>
<play-er has="diamond-helmet diamond-footwear horse" position="100 150 40"></play-er>

How

To illustrate with a small example, let's suppose we want to add a behavior to a wide variety of elements in an application, and that the behavior will simply log to the console whenever the element is clicked.

Unlike Custom Elements that need to extend from HTMLElement, Element Behaviors do not need to extend from any class. Similar to but unlike Custom Element lifecycle methods, Element Behavior lifecycle methods all accept a first argument element which is the element onto which the instance of the behavior is applied.

Let's define a ClickLogger behavior:

<script>
	// First define an element behavior class.
	class ClickLogger {
		// The constructor accepts the `element` in its first parameter.
		constructor(element) {
			this.handler = () => {
				console.log('Clicked an element: ', element)
			}
		}

		// This is called when the `element` is added to the DOM, passed in the `element`.
		connectedCallback(element) {
			// Here we create a click handler.
			element.addEventListener('click', this.handler)
		}

		// This is called when the `element` is removed from the DOM, passed in the `element`.
		disconnectedCallback(element) {
			// Don't forget to clean up!
			element.removeEventListener('click', this.handler)
		}
	}

	// Define the behavior with our class.
	elementBehaviors.define('click-logger', ClickLogger)
</script>

Now we can use the has="" attribute to specify which behaviors an element has, and in this case we'll give multiple elements the click-logger behavior:

<div has="click-logger">one</div>
<p has="click-logger">two</p>
<button has="click-logger">three</button>

That's all that we need to do! For each DOM element created that has the specified behavior, an instance of the behavior will be constructed, and will log to console any time the elements are clicked.

An example of that is in examples/clicks/.

Examples

For a basic example, see this live pen: https://codepen.io/trusktr/pen/ymPXNb

To run local examples like the previous ClickLogger after cloning this repository, run

npm install
npm run examples

This opens a tab in your browser. Then, for example, click on the clicks/ folder to see the examples/clicks/index.html file in action.

Alternative to Custom Elements for special cases

Element Behaviors can be used as an alternative to Custom Elements, especially in cases where Custom Elements cannot be used at all.

For example, Custom Elements do not work with SVG because Custom Elements cannot extend from SVGElement, and special HTMLElements like <table> and <tr> can not be extended by Custom Elements in all browsers (Safari does not support the is="" attribute, i.e. "customized built-ins").

This is where Element Behaviors are advantageous: they do not need to extend from any base class, and one or more behaviors can be used on any type of elements, whether they are SVG, table elements, etc:

<table has="click-logger">
	<tr has="coolness awesomeness">
		...
	</tr>
</table>
<svg has="some-behavior">
	<rect has="other-behavior"></rect>
</svg>

This works great for progressive enhancement where <svg> and <table> elements will work fine without JavaScript (or prior to JavaScript being loaded), and Element Behaviors can augment the elements when JavaScript is available.

API

The API is simple. If you know Custom Elements, then you basically know Element Behaviors.

Behavior classes

The following is a class showing the APIs that a behavior class can have, in a fashion similar to Custom Elements, with an additional static awaitElementDefined property. The first argument received by each lifecycle method is the element that has the behavior on it:

class SomeBehavior {
	// This is called only once, given the `element` that the behavior is attached to.
	constructor(element) {}

	// This is called any time the associated `element` is appended into the
	// DOM, passed in the `element`
	connectedCallback(element) {}

	// This is called any time the associated `element` is removed from the DOM,
	// passed in the `element`.
	disconnectedCallback(element) {}

	// As with custom elements, define which attributes of the associated
	// element that the behavior should react to.
	static observedAttributes = ['some-attribute', 'other-attribute']

	// This is called any time any of the `observedAttributes` of the associated
	// element have been changed, just like with Custom Elements but with the
	// additional passed in `element`.
	attributeChangedCallback(attributeName, oldValue, newValue, element) {}

	// There is one additional API, unlike with Custom Elements. If `static
	// awaitElementDefined` is `true`, then the behavior will not be
	// instantiated and connected until its host element is defined and upgraded
	// (that is, if the host element is possibly a custom element, having a
	// hyphen in its name). If the host element has no hyphen in its name, then
	// this does not apply, and the behavior will be created and connected
	// immediately without waiting. If a possibly-custom element is removed
	// before it is defined, then a behavior will not be created and connected
	// at all (waiting will have been canceled).
	static awaitElementDefined = true // Default is false.
}

elementBehaviors.define()

Similar to customElements, elementBehaviors is a global with a define() method.

The first parameter accepts the name of the behavior (a string) that will be defined, and the second parameter accepts the class (an instance of Function) that defines the functionality of the behavior.

Define a behavior, by associating a behavior name with a class:

class SomeBehavior {
	/* ... */
}

elementBehaviors.define('some-behavior', SomeBehavior)

And now the behavior can be used.

The has="" attribute

To use behaviors, the special has="" attribute is used on desired elements to specify which behaviors they should have.

Apply a behavior to an element:

<div has="some-behavior">one</div>

Any number of behaviors can be applied to an element. If we define three behaviors, "foo", "bar", and "baz" using elementBehaviors.define(), we can apply all of them to an element as a space-separated list in the element's has attribute:

<script>
	class Foo {
		/* ... */
	}
	elementBehaviors.define('foo', Foo)

	class Bar {
		/* ... */
	}
	elementBehaviors.define('bar', Bar)

	class Baz {
		/* ... */
	}
	elementBehaviors.define('baz', Baz)
</script>

<div has="foo bar baz">one</div>

Behaviors can be added and removed from elements at any time. For example, suppose we want to remove the "baz" behavior from the previous div, and add "click-logger". We can do so by changing the value of the has="" attribute:

const div = document.querySelector('div')

div.setAttribute('has', 'foo bar click-logger')

The new value of the has attribute no longer has "baz" and now has "click-logger". The Baz behavior will have its disconnectedCallback() method called for cleanup, while a new ClickLogger instance will be constructed and have its connectedCallback() method called.

Note If you were to call div.setAttribute('has', 'click-logger') thinking that you were adding the click-logger behavior, you will have removed all three foo, bar, and baz behaviors and the element will have only a click-logger behavior because the new has attribute is has="click-logger".

Element.prototype.behaviors

All elements have a new .behaviors property that returns a map of strings to behavior instances. This makes it easy to get a behavior instance from an element to interact with its APIs as needed. For example:

<div has="some-behavior"></div>

<script>
	// Get the element
	const el = document.querySelector('[has=some-behavior]')

	// Get the behavior from the element
	const behavior = el.behaviors.get('some-behavior')

	// do something with `behavior`

	// Map.forEach
	el.behaviors.forEach((behavior, behaviorName) => {
		console.log('behavior:', behaviorName, behavior)
	})

	// It is iterable.
	for (const [behaviorName, behavior] of el.behaviors) {
		console.log('behavior:', behaviorName, behavior)
	}
</script>

Notes

  • See this long issue on w3c's webcomponents repo, which led to the issue where the idea for element-behaviors was born, with some ideas from this other issue (thanks to all who helped to discuss the idea!).
  • Uses custom-attributes (originally by @matthewp, forked in LUME) to implement the has="" attribute.

Extras (spec and proposal authors can stop reading here)

The rest of the document adds features that wouldn't be implementable in a real "element behaviors" (or similar) spec because the web platform does not support the following extras:

TypeScript

If you are using Solid JSX (f.e. with @lume/element or solid-js packages) you will want to import the has="" attribute type for use in your JSX templates:

import type {} from 'element-behaviors/src/attribute-types.solid'

export function SomeComponent() {
  return <div has="foo bar" ...></div> // no error
}

export function OtherComponent() {
  return <div has={123} ...></div> // error, value should be a string
}

Note Other types for React JSX, Preact JSX, Svelte templates, Vue templates, etc, are not yet supported but easy to add. Open an issue or PR as needed.

Solid.js Reactivity

The el.behaviors property is reactive using Solid.js APIs, meaning we can react to changes in behaviors.

This can be taken advantage of by first installing solid-js,

npm install solid-js

Then in your app you can use el.behaviors APIs in a reactive context such as a JSX template, or in an effect:

import {createEffect} from 'solid-js'

// This effect will re-run any time the values of
// `el.behaviors.get('some-behavior')` or `behavior.count` change.
createEffect(() => {
	const behavior = el.behaviors.get('some-behavior') // reactive

	if (!behavior) return

	// Log the count any time it changes:

	// Assume in this example that behavior.count is a reactive (signal) property made with Solid.js:
	console.log(behavior.count) // reactive
})

DefaultBehaviors (in LUME)

LUME (a 3D HTML toolkit) uses Element Behaviors, and provides an additional DefaultBehaviors mixin class that gives Custom Element classes the ability to define which behaviors they ship with by default, which is super handy!

Note Thinking to move DefaultBehaviors to here instead of lume.

To use it first install lume:

npm install lume

To define a Custom Element with default behaviors, it is done similarly to with observedAttributes:

import {DefaultBehaviors} from 'lume/dist/behaviors/DefaultBehaviors.js'

class SomeElement extends DefaultBehaviors(HTMLElement) {
	// Define observed attributes
	static get observedAttributes() {
		return ['some-attribute', 'other-attribute']
	}

	// Define default behaviors that the element will have
	static get defaultBehaviors() {
		return ['some-behavior', 'click-logger']
	}
}

Additionally, the static defaultBehaviors property can return an object whose key names are behavior names, and whose values are functions that return true or false to determine if a default behavior should be initially added to an element or not. The function will receive the element, as well as intial behaviors that the element already has defined by the has="" attribute when the element is created.

For example, suppose we have the following HTML:

<my-el has="another-behavior"></my-el>
<my-el has="some-behavior"></my-el>

We define a Custom Element like:

class MyEl extends DefaultBehaviors(HTMLElement) {
	static get defaultBehaviors() {
		return {
			'click-logger': (element, initialBehaviors) => {
				// For sake of example, suppose that if the element has
				// `another-behavior`, then we do not want it to have the `click-logger`
				// behavior:
				if (initialBehaviors.includes('another-behavior')) {
					return false
				}
				return true
			},
		}
	}
}

customElements.define('my-el', MyEl)

When the my-el elements are created, only the one without the another-behavior will have click-logger added to it, so the resulting DOM will be as follows:

<my-el has="another-behavior"></my-el>
<my-el has="some-behavior click-logger"></my-el>

Contributing

First install dependencies:

npm install

Code

Source files are written in TypeScript, ending in .ts.

Please make sure your editor obeys the format rules in .editorconfig. There are Editorconfig plugins for just about every text editor out there. Also install a Prettier plugin for your editor, and have it auto format on save. Tests will fail if the formatting check does not pass.

Development build mode

Run the package in dev mode (it will rebuild when files change):

npm run dev

This watches files and automatically incrementally rebuilds the project when any files in src/ have changed.

Production build

To build the package for production, run

npm run build

Testing

Any files ending with .test.ts anywhere in the tests/ or src/ folders are test files that will be ran by Karma, the test runner.

To run tests (which will both check code format and run unit tests):

npm test

To debug tests, we can open a visible Electron window in which Karma is running tests, and use Chrome's devtools for debugging (f.e. stepping through the test code). To do so, run:

npm run test-debug

Publishing a new version

When ready to publish a new version, run one of the following depending on which part of the version number you want to increment (see SemVer for conventions around version numbers).

npm run realease:patch
npm run realease:minor
npm run realease:major

Any of the three release:* scripts will:

  • clean the project of any previous build output
  • stash any changes in the repo
  • build the project in production mode
  • run the project's tests
  • increment the version number (according to SemVer rules depending on if you choose patch, minor, or major)
  • create a new commit containing the version number in the form "v1.2.3" as the message
  • tag that commit with a git tag of the same name as the commit message
  • publish the new version to NPM
  • push the commit and the tag to GitHub
  • and finally unstash any changes if there were any

Note If something goes wrong (f.e. an error during the build or test process), fear not, the package will not be published. Fix the failing tests, and try again.

Note After a failure, changes that were stashed will remain stashed.

TODO

  • TypeScript example with solid-js
  • TypeScript example with @lume/element
  • TypeScript example with react
  • TypeScript example with preact
  • TypeScript example with svelte
  • TypeScript example with vue