{"id":19346739,"url":"https://github.com/lume/element-behaviors","last_synced_at":"2025-10-20T09:41:47.757Z","repository":{"id":49716846,"uuid":"108196997","full_name":"lume/element-behaviors","owner":"lume","description":"An entity-component system for HTML elements.","archived":false,"fork":false,"pushed_at":"2024-10-02T21:10:46.000Z","size":262,"stargazers_count":107,"open_issues_count":5,"forks_count":3,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-04-13T02:25:21.801Z","etag":null,"topics":["3d","3d-graphics","custom-elements","ecs","entity-component","html-element","lume","threejs","webgl"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lume.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-24T23:58:54.000Z","updated_at":"2024-11-23T00:27:55.000Z","dependencies_parsed_at":"2024-05-28T17:16:12.958Z","dependency_job_id":"e73d7450-2ed4-4266-863b-3a684a481b1d","html_url":"https://github.com/lume/element-behaviors","commit_stats":{"total_commits":142,"total_committers":2,"mean_commits":71.0,"dds":"0.014084507042253502","last_synced_commit":"281af58b56cf1088caf3f3d077fcdabeaebdeba5"},"previous_names":["trusktr/element-behaviors"],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lume%2Felement-behaviors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lume%2Felement-behaviors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lume%2Felement-behaviors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lume%2Felement-behaviors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lume","download_url":"https://codeload.github.com/lume/element-behaviors/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250134368,"owners_count":21380424,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["3d","3d-graphics","custom-elements","ecs","entity-component","html-element","lume","threejs","webgl"],"created_at":"2024-11-10T04:12:10.553Z","updated_at":"2025-10-20T09:41:42.734Z","avatar_url":"https://github.com/lume.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# element-behaviors\n\nElement 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.\n\n\u003ch4\u003e\u003ccode\u003e\u003cstrong\u003enpm install element-behaviors\u003c/strong\u003e\u003c/code\u003e\u003c/h4\u003e\n\nThe next Sparkles demo on CodePen shows how to apply a behavior to multiple elements:\n\nhttps://codepen.io/trusktr/pen/MWzzNdV?editors=1000\n\nhttps://github.com/lume/element-behaviors/assets/297678/0289f294-e5ff-4b7c-b30a-61e29aabea51\n\n# Apply one or more behaviors onto HTML elements\n\nElement behaviors are useful for assigning features onto HTML elements. They are similar to [Custom Elements](https://developers.google.com/web/fundamentals/web-components/customelements), but multiple behaviors can be associated with an element.\n\nElement behaviors have lifecycle methods that are named the same as with\nCustom Elements. This let's us react to the lifecycle events of an\nelement just like a custom element can.\n\nTo help spark your imagination, this is what you might do with Element\nBehaviors. Suppose we are making a Minecraft-like game:\n\n```html\n\u003cender-man has=\"player-aware holds-block\" holds=\"dirt\" position=\"30 30 30\"\u003e\u003c/ender-man\u003e\n\n\u003c!-- uh oh! The enderman is aware of the player, run!...  --\u003e\n\u003cplay-er position=\"40 40 30\"\u003e\u003c/play-er\u003e\n```\n\n```html\n\u003c!-- ...the player got away from the enderman, and found diamond armor and a horse --\u003e\n\u003cender-man has=\"holds-block\" holds=\"sand\" position=\"-20 38 40\"\u003e\u003c/ender-man\u003e\n\u003cplay-er has=\"diamond-helmet diamond-footwear horse\" position=\"100 150 40\"\u003e\u003c/play-er\u003e\n```\n\n# How\n\nTo illustrate with a small example, let's suppose we want to add a behavior to a\nwide variety of elements in an application, and that the behavior will simply\nlog to the console whenever the element is clicked.\n\nUnlike Custom Elements that need to extend from `HTMLElement`, Element Behaviors\ndo not need to extend from any class. Similar to but unlike Custom Element\nlifecycle methods, Element Behavior lifecycle methods all accept a first\nargument `element` which is the element onto which the instance of the behavior\nis applied.\n\nLet's define a `ClickLogger` behavior:\n\n```html\n\u003cscript\u003e\n\t// First define an element behavior class.\n\tclass ClickLogger {\n\t\t// The constructor accepts the `element` in its first parameter.\n\t\tconstructor(element) {\n\t\t\tthis.handler = () =\u003e {\n\t\t\t\tconsole.log('Clicked an element: ', element)\n\t\t\t}\n\t\t}\n\n\t\t// This is called when the `element` is added to the DOM, passed in the `element`.\n\t\tconnectedCallback(element) {\n\t\t\t// Here we create a click handler.\n\t\t\telement.addEventListener('click', this.handler)\n\t\t}\n\n\t\t// This is called when the `element` is removed from the DOM, passed in the `element`.\n\t\tdisconnectedCallback(element) {\n\t\t\t// Don't forget to clean up!\n\t\t\telement.removeEventListener('click', this.handler)\n\t\t}\n\t}\n\n\t// Define the behavior with our class.\n\telementBehaviors.define('click-logger', ClickLogger)\n\u003c/script\u003e\n```\n\nNow we can use the `has=\"\"` attribute to specify which behaviors an element has,\nand in this case we'll give multiple elements the `click-logger` behavior:\n\n```html\n\u003cdiv has=\"click-logger\"\u003eone\u003c/div\u003e\n\u003cp has=\"click-logger\"\u003etwo\u003c/p\u003e\n\u003cbutton has=\"click-logger\"\u003ethree\u003c/button\u003e\n```\n\nThat's all that we need to do! For each DOM element created that has the specified behavior, an\ninstance of the behavior will be constructed, and will log to console any time the elements are clicked.\n\nAn example of that is in [`examples/clicks/`](./examples/clicks/index.html).\n\n# Examples\n\nFor a basic example, see this live pen: https://codepen.io/trusktr/pen/ymPXNb\n\nTo run local examples like the previous `ClickLogger` after cloning this repository, run\n\n```sh\nnpm install\nnpm run examples\n```\n\nThis opens a tab in your browser. Then, for example, click on the `clicks/`\nfolder to see the [`examples/clicks/index.html`](./examples/clicks/index.html)\nfile in action.\n\n# Alternative to Custom Elements for special cases\n\nElement Behaviors can be used as an alternative to Custom Elements, especially in cases where Custom Elements cannot be used at all.\n\nFor example, Custom Elements do not work with SVG because Custom Elements cannot\nextend from `SVGElement`, and special `HTMLElement`s like `\u003ctable\u003e` and `\u003ctr\u003e`\ncan not be extended by Custom Elements in all browsers (Safari does not support\nthe `is=\"\"` attribute, i.e. \"customized built-ins\").\n\nThis is where Element Behaviors are advantageous: they do not need to extend\nfrom any base class, and one or more behaviors can be used on any type of\nelements, whether they are SVG, table elements, etc:\n\n```html\n\u003ctable has=\"click-logger\"\u003e\n\t\u003ctr has=\"coolness awesomeness\"\u003e\n\t\t...\n\t\u003c/tr\u003e\n\u003c/table\u003e\n\u003csvg has=\"some-behavior\"\u003e\n\t\u003crect has=\"other-behavior\"\u003e\u003c/rect\u003e\n\u003c/svg\u003e\n```\n\nThis works great for progressive enhancement where `\u003csvg\u003e` and `\u003ctable\u003e`\nelements will work fine without JavaScript (or prior to JavaScript being\nloaded), and Element Behaviors can augment the elements when JavaScript is\navailable.\n\n# API\n\nThe API is simple. If you know Custom Elements, then you basically know Element Behaviors.\n\n## Behavior classes\n\nThe following is a class showing the APIs that a behavior class can have, in a\nfashion similar to Custom Elements, with an additional `static awaitElementDefined` property. The first argument received by each lifecycle\nmethod is the `element` that has the behavior on it:\n\n```js\nclass SomeBehavior {\n\t// This is called only once, given the `element` that the behavior is attached to.\n\tconstructor(element) {}\n\n\t// This is called any time the associated `element` is appended into the\n\t// DOM, passed in the `element`\n\tconnectedCallback(element) {}\n\n\t// This is called any time the associated `element` is removed from the DOM,\n\t// passed in the `element`.\n\tdisconnectedCallback(element) {}\n\n\t// As with custom elements, define which attributes of the associated\n\t// element that the behavior should react to.\n\tstatic observedAttributes = ['some-attribute', 'other-attribute']\n\n\t// This is called any time any of the `observedAttributes` of the associated\n\t// element have been changed, just like with Custom Elements but with the\n\t// additional passed in `element`.\n\tattributeChangedCallback(attributeName, oldValue, newValue, element) {}\n\n\t// There is one additional API, unlike with Custom Elements. If `static\n\t// awaitElementDefined` is `true`, then the behavior will not be\n\t// instantiated and connected until its host element is defined and upgraded\n\t// (that is, if the host element is possibly a custom element, having a\n\t// hyphen in its name). If the host element has no hyphen in its name, then\n\t// this does not apply, and the behavior will be created and connected\n\t// immediately without waiting. If a possibly-custom element is removed\n\t// before it is defined, then a behavior will not be created and connected\n\t// at all (waiting will have been canceled).\n\tstatic awaitElementDefined = true // Default is false.\n}\n```\n\n## `elementBehaviors.define()`\n\nSimilar to `customElements`, `elementBehaviors` is a global with a `define()` method.\n\nThe first parameter accepts the name of the behavior (a string) that will be defined, and the second\nparameter accepts the class (an instance of Function) that defines the functionality of the\nbehavior.\n\nDefine a behavior, by associating a behavior name with a class:\n\n```js\nclass SomeBehavior {\n\t/* ... */\n}\n\nelementBehaviors.define('some-behavior', SomeBehavior)\n```\n\nAnd now the behavior can be used.\n\n## The `has=\"\"` attribute\n\nTo use behaviors, the special `has=\"\"` attribute is used on desired elements to specify which behaviors\nthey should have.\n\nApply a behavior to an element:\n\n```html\n\u003cdiv has=\"some-behavior\"\u003eone\u003c/div\u003e\n```\n\nAny number of behaviors can be applied to an element. If we define three behaviors, \"foo\", \"bar\",\nand \"baz\" using `elementBehaviors.define()`, we can apply all of them to an element as a\nspace-separated list in the element's `has` attribute:\n\n```html\n\u003cscript\u003e\n\tclass Foo {\n\t\t/* ... */\n\t}\n\telementBehaviors.define('foo', Foo)\n\n\tclass Bar {\n\t\t/* ... */\n\t}\n\telementBehaviors.define('bar', Bar)\n\n\tclass Baz {\n\t\t/* ... */\n\t}\n\telementBehaviors.define('baz', Baz)\n\u003c/script\u003e\n\n\u003cdiv has=\"foo bar baz\"\u003eone\u003c/div\u003e\n```\n\nBehaviors can be added and removed from elements at any time. For example,\nsuppose we want to remove the \"baz\" behavior from the previous `div`, and add\n\"click-logger\". We can do so by changing the value of the `has=\"\"` attribute:\n\n```js\nconst div = document.querySelector('div')\n\ndiv.setAttribute('has', 'foo bar click-logger')\n```\n\nThe new value of the `has` attribute no longer has \"baz\" and now has\n\"click-logger\". The `Baz` behavior will have its `disconnectedCallback()` method\ncalled for cleanup, while a `new ClickLogger` instance will be constructed and\nhave its `connectedCallback()` method called.\n\n\u003e **Note**\n\u003e If you were to call `div.setAttribute('has', 'click-logger')` thinking that you\n\u003e were adding the `click-logger` behavior, you will have removed all three `foo`,\n\u003e `bar`, and `baz` behaviors and the element will have only a `click-logger`\n\u003e behavior because the new `has` attribute is `has=\"click-logger\"`.\n\n## `Element.prototype.behaviors`\n\nAll elements have a new `.behaviors` property that returns a map of strings to\nbehavior instances. This makes it easy to get a behavior instance from an element to\ninteract with its APIs as needed. For example:\n\n```html\n\u003cdiv has=\"some-behavior\"\u003e\u003c/div\u003e\n\n\u003cscript\u003e\n\t// Get the element\n\tconst el = document.querySelector('[has=some-behavior]')\n\n\t// Get the behavior from the element\n\tconst behavior = el.behaviors.get('some-behavior')\n\n\t// do something with `behavior`\n\n\t// Map.forEach\n\tel.behaviors.forEach((behavior, behaviorName) =\u003e {\n\t\tconsole.log('behavior:', behaviorName, behavior)\n\t})\n\n\t// It is iterable.\n\tfor (const [behaviorName, behavior] of el.behaviors) {\n\t\tconsole.log('behavior:', behaviorName, behavior)\n\t}\n\u003c/script\u003e\n```\n\n# Notes\n\n- See this [long issue](https://github.com/w3c/webcomponents/issues/509) on w3c's webcomponents repo,\n  which led to [the issue](https://github.com/w3c/webcomponents/issues/662) where the idea for element-behaviors was born,\n  with some ideas from this [other issue](https://github.com/w3c/webcomponents/issues/663) (thanks to\n  all who helped to discuss the idea!).\n- Uses [custom-attributes](https://github.com/lume/custom-attributes) (originally by @matthewp, forked\n  in [LUME](https://lume.io)) to implement the `has=\"\"` attribute.\n\n---\n\n# Extras (spec and proposal authors can stop reading here)\n\nThe 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:\n\n## TypeScript\n\nIf you are using Solid JSX (f.e. with `@lume/element` or `solid-js` packages)\nyou will want to import the `has=\"\"` attribute type for use in your JSX\ntemplates:\n\n```tsx\nimport type {} from 'element-behaviors/src/attribute-types.solid'\n\nexport function SomeComponent() {\n  return \u003cdiv has=\"foo bar\" ...\u003e\u003c/div\u003e // no error\n}\n\nexport function OtherComponent() {\n  return \u003cdiv has={123} ...\u003e\u003c/div\u003e // error, value should be a string\n}\n```\n\n\u003e **Note** Other types for React JSX, Preact JSX, Svelte templates, Vue\n\u003e templates, etc, are not yet supported but easy to add. Open an issue or PR as\n\u003e needed.\n\n## Solid.js Reactivity\n\nThe `el.behaviors` property is reactive using [Solid.js](https://www.solidjs.com)\nAPIs, meaning we can react to changes in behaviors.\n\nThis can be taken advantage of by first installing `solid-js`,\n\n```sh\nnpm install solid-js\n```\n\nThen in your app you can use `el.behaviors` APIs in a reactive context such as a\nJSX template, or in an effect:\n\n```js\nimport {createEffect} from 'solid-js'\n\n// This effect will re-run any time the values of\n// `el.behaviors.get('some-behavior')` or `behavior.count` change.\ncreateEffect(() =\u003e {\n\tconst behavior = el.behaviors.get('some-behavior') // reactive\n\n\tif (!behavior) return\n\n\t// Log the count any time it changes:\n\n\t// Assume in this example that behavior.count is a reactive (signal) property made with Solid.js:\n\tconsole.log(behavior.count) // reactive\n})\n```\n\n# Contributing\n\nFirst install dependencies:\n\n```sh\nnpm install\n```\n\n## Code\n\nSource files are written in TypeScript, ending in `.ts`.\n\nPlease make sure your editor obeys the format rules in `.editorconfig`. There\nare [Editorconfig](https://editorconfig.org) plugins for just about every text\neditor out there. Also install a [Prettier](https://prettier.io) plugin for\nyour editor, and have it auto format on save. Tests will fail if the formatting\ncheck does not pass.\n\n## Development build mode\n\nRun the package in dev mode (it will rebuild when files change):\n\n```sh\nnpm run dev\n```\n\nThis watches files and automatically incrementally rebuilds the project when any files in `src/`\nhave changed.\n\n## Production build\n\nTo build the package for production, run\n\n```sh\nnpm run build\n```\n\n## Testing\n\nAny files ending with `.test.ts` anywhere in the `tests/` or `src/` folders are test\nfiles that will be ran by [Karma](https://karma-runner.github.io), the test runner.\n\nTo run tests (which will both check code format and run unit tests):\n\n```sh\nnpm test\n```\n\nTo debug tests, we can open a visible [Electron](https://electronjs.org) window in which Karma is\nrunning tests, and use Chrome's devtools for debugging (f.e. stepping through the test code). To do\nso, run:\n\n```sh\nnpm run test-debug\n```\n\n## Publishing a new version\n\nWhen ready to publish a new version, run one of the following depending on which part of the version\nnumber you want to increment (see [SemVer](https://semver.org/) for conventions around version\nnumbers).\n\n```sh\nnpm run realease:patch\nnpm run realease:minor\nnpm run realease:major\n```\n\nAny of the three `release:*` scripts will:\n\n- clean the project of any previous build output\n- stash any changes in the repo\n- build the project in production mode\n- run the project's tests\n- increment the version number (according to SemVer rules depending on if you choose patch, minor,\n  or major)\n- create a new commit containing the version number in the form \"v1.2.3\" as the message\n- tag that commit with a git tag of the same name as the commit message\n- publish the new version to NPM\n- push the commit and the tag to GitHub\n- and finally unstash any changes if there were any\n\n\u003e **Note**\n\u003e If something goes wrong (f.e. an error during the build or test process), fear\n\u003e not, the package will not be published. Fix the failing tests, and try again.\n\n\u003e **Note**\n\u003e After a failure, changes that were stashed will remain stashed.\n\n# TODO\n\n- TypeScript example with `solid-js`\n- TypeScript example with `@lume/element`\n- TypeScript example with `react`\n- TypeScript example with `preact`\n- TypeScript example with `svelte`\n- TypeScript example with `vue`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flume%2Felement-behaviors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flume%2Felement-behaviors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flume%2Felement-behaviors/lists"}