javastick

0.3.0 • Public • Published

Javastick

A light way to attach JavaScript behaviour to elements of your page, without worrying if they were present at page load or get injected later on.

It is built to support a core set of features (see right after) and exposes all its internals for you to extend.

Features

1. Register behaviours with Javastick

A behaviour is a JavaScript function that accepts the element it'll execute on as parameter. Each key in the options provided to javastick represents one such behaviour (aside from a limited set actual options). For example, you can declare a behaviour that reveals a hidden HTML element like so:

javastick({
  reveal(element) {
    element.hidden = false;
  }
})

If the name you'd like to give collides with an existing option, you can pass them through the behaviors option. This is equivalent to the previous setup:

javastick({
  behaviors: {
    reveal(element) {
      element.hidden = false;
    }
  }
})

2. Declare which elements the behaviours run on through data attributes

Use the data-is (configurable) attribute on HTML to list the names of one or several behaviours you'd like to run on the element. Javastick will take care of running them:

  • if the attribute was already on the element when Javastick starts
  • if an element with the attribute gets added after Javastick is started
  • if a data-is attribute gets updated with a new list behaviours
  • if they get a new data-is attribute

For example, the previous setup would reveal the following paragraph, whatever it's provenance or the provenance of its data-is attribute:

<p hidden data-is="reveal">I'll only appear if Javastick runs</p>

3. Clean up when elements lose their behaviours

Sometimes, the behaviours you're using will require some cleanup if the element gets removed (or the behaviour needs to stop applying to the element). Your behaviour can return a function that Javastick will run when:

  • the element gets removed from the DOM
  • the element's data-is attribute gets updated and the behaviour gets unlisted
  • the element loses its data-is attribute

This is particularly handy for cleaning up event listeners registered on window or document, observers like MutationObserver, IntersectionObserver or ResizeObserver, cancelling HTTP requests and probably plenty of other things.

Let's use a slightly different setup:

javastick({
  behaviors: {
    reveal(element) {
      element.hidden = false;

      return function(elementLosingBehaviour) {
        elementLosingBehaviour.hidden = true;
      }
    }
  }
})

Just like before, the following paragraph will get revealed:

<p hidden data-is="reveal">I'll only appear if Javastick runs</p>

But when this script runs, it'll get back its 'hidden' attribute:

document.querySelector('[data-is="reveal"]').removeAttribute('data-is')

4. Install new behaviours after Javastick started

Javastick supports adding new behaviours after it is started. It'll keep track of which elements needed that behaviour and run it automatically after it's installed. Handy for loading heavy pieces of JavaScript only when needed on the page.

Now let's say we had started Javastick without any behaviour. Note that we'll store the output in an app variable. That's what'll let us install new behaviours later on:

const app = javastick();

And we had our usual paragraph on the page

<p hidden data-is="reveal">I'll only appear if Javastick runs</p>

Once we run the following, the paragraph will get revealed, as if the directive had been there from the start.

app.install('reveal', function(element) {
  reveal(element) {
      element.hidden = false;

      return function(elementLosingBehaviour) {
        elementLosingBehaviour.hidden = true;
      }
    }
})

5. Extend it to suit your needs

Javastick limits itself to this core set of features. This ensures it doesn't burden projects with extra code for more complex features that'll never get run. It does expose most (all?) of its internal parts to let you add whichever features you feel necessary.

See Extensibility options for more details.

Installation

The library is published on NPM

npm install javastick
Or for Yarn
yarn add javastick
Using with bundlers

The package provides an ESM module which should get picked up by your bundler of choice when just importing javastick:

import { javastick } from 'javastick';
javastick({
  // Your Javastick options
})
Loading directly in the browser

AnES module can be loaded directly in the browser with:

<script type="module">
import {javastick} from "./node_modules/javastick/dist/javastick.esm.js"
javastick({
  // Your Javastick options
})

For older browsers, the package also provides and ES5 UMD build to support older browsers:

<script src="./node_modules/javastick/dist/javastick.es5.js" defer></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
  javastick({
    // Your Javastick options
  })
});

Both have a minified counterpart with a .min.js extension, with an associated sourcemap.

Configuration options

 behaviors

A JavaScript object containing the list of behaviours. This object will be modified when installing new behaviours.

onMissingBehaviors

Optional A function that'll be called if an element declares a behavior that's not been installed yet. This could be an opportunity to load it and install it, or report an error.

attribute

Default: "data-is" Which attribute lists the behaviours on elements

root

Default: document.documentElement The root element that Javastick monitors

start

Default: true Whether to start the observer straight away. Set to false if you need to control exactly when Javastic should kick-in, by calling the start function on the returned object:

const app = javastick({
  start: false;
})
// Dynamically import behaviours instead of bundling them all upfront
await importBehaviors(app);
app.start();

The app object

javastick will return an app object with a couple of things

The start method

For starting the app when setting the start option to false. The library trusts you to only call it once. It doesn't make any checks that things have already been started. This means multiple calls will turn into behaviors being registered multiple times.

The observer property

The MutationObserver used for monitoring the DOM. This can allow you to disconnect it to stop monitoring if needed.

The attacher property

This provides a reference to the attacher, allowing you to use attach extra behaviours to elements from outside Javastick or detach behaviours currently attached.

The matcher property

This provides a reference to the matcher, allowing you to use its method for detecting elements declaring behaviours and which they are.

The handler property

A reference to the matcher used by this app, allowing you to run the same code as when elements are added, removed or attributes changed if necessary.

Extensibility options

The default javastick function provides pre-configured defaults not only for the configuration options, but for the implementation of its internals.

From a high level, Javastick is made of 4 parts:

  • an observer, responsible for detecting changes to the DOM
  • a handler, which will act on the changes detect by the observer
  • a matcher, that the handler uses for checking if an element has behaviors or not and which those are
  • an attacher, tracking which behaviours are available and responsible for attaching/detaching them from elements

Those can be overriden through a set of options, allowing you to extend how javastick insides work for managing more complex scenarios.

In addition, the project offers an unconfigured runtime export, letting you tree-shake out any of the defaults you're not using when bundling your final project.

attacher, attacherOptions

The attacher keeps track of which behaviors are available and which are attached to which element. You can provide Javastick a pre-constructed object or let Javastick create it, passing it any attacherOptions provided.

The attacher must return an object with 2 methods:

  • attach(element, behaviorName): to attach the behavior with the given name
  • detach(element): to detach the behavior

Javastick actually comes with two attachers:

  • a simple attacher expecting all behaviors to be provided upfront
  • an updatableAttacher that allows directives to be installed

A custom attacher could let you provide new features like the ability to detach all behaviors, create a convention in the behaviorName to pass options to the behaviours or get notified when directives are attached or detached from elements.

attacherOptions

They're the recipient of the behavior and onMissingBehavior configuration options (see above). They also let you to provide a custom Map (or WeakMap if using the simple attacher) for storing which behaviours are attached to which element via the behaviorsByElement option.

This allows you to get access to that list at any time or extend the attachers feature. This is how the updatableAttacher is build on top of the attacher.

matcher and matcherOptions

The matcher is what lets Javastick pick which elements declare behaviours and which those are. You can provide Javastick a pre-constructed object or a Function that'll get given any matcherOptions provided to Javastick.

The matcher must return an object with 3 methods:

  • hasBehaviors(element) returning whether an element declares behaviours
  • findDescendantsWithBehaviors(element) returning descendants of the elements that declares behaviours. It is distinct from hasBehaviors as it's likely document.querySelectorAll or other methods would be faster than traversing the DOM and calling hasBehaviors on each element.
  • getBehaviors(element) returning an array of the behavior names

A custom matcher could let you swap to using classes, or a combination of attributes (like ARIA attributes) for deciding that an element needs some custom behaviour.

handler and handlerOptions

The handler handles the changes of the DOM: elements being added, removed or their attributes updated. You'll likely want to let Javastick create it from a Function, passing any provided handlerOptions merged with {attacher,matcher}. Though you can also provide a pre-built object if you fancy.

The handler must return an object with 3 methods, all receiving the element that changed:

  • onAddedElement
  • onRemovedElement
  • onAttributeChange

The default observer (see right after), will also pass the MutationRecord that prompted the change.

With a custom handler, you could tweak when the updates are actually run or schedule them by batches of 16ms to limit the impact of large DOM changes attaching/detaching lots of behaviours.

observer and observerOptions

The observer is a light wrapper over a MutationObserver, monitoring additions, removals and attribute changes in the DOM under its root. The observerOptions will be merged with some default options when monitoring starts.

With MutationObserver being widely supported and a polyfill available, I couldn't imagine a use case for overriding that function, but the option is there if you need.

Alternatives

Event delegation

Made popular by jQuery, it's handy for handling events on elements injected in the page after load, however:

  1. it can lead to a lot of events used on other pages being registered for nothing
  2. it doesn't handle code needing to modify a node being inserted (for ex. adding classes, updating attributes...)

It might still be handy to boost performance when needing to react to the same event from a bunch of elements.

Custom Elements

With major browsers now having support for closer elements, they're another good way to attach JS behaviours without worries of what brought the elements in the DOM. However:

  1. they're extra elements in the DOM, potentially messing layout (though there's display: contents now)
  2. composing them means adding more and more extra elements
  3. TBC: applying them to built-in elements (through is) require one custom element per type of element (and can only support one custom element)

 Stimulus

Recently, Stimulus has risen as a way to attach JavaScript to DOM nodes regardless of what brought them in, especially in the Ruby on Rails community. It goes beyond just attaching JavaScript and provides features for observing attributes' values, handling events... This is great and brings a lot of consistency but:

  1. It's a "Take everything" approach: don't need to track value changes in attributes, the code for handling it there anyway
  2. Hooking a simple behaviour requires to create a whole class just for the sake of running what could have been a Function

This is actually what sparked the creation of this library: trying to see if it was possible to break down the features provided by Stimulus so you can only embark what you need.

Possible extensions

Just some vague plans for now, more details will be added

TBD: Event delegation only if needed

Only register event delegation listeners if elements requiring it are actually in the DOM

TBD: Declarative "children of interest"

Track children of a given node marked by specific attributes for quicker access, including having collection of targets. Allow to react to target

TDB: Declarative "attributes of interest"

Track attribute values of a given node and allow to react to their change and parse their value. Allows customisation of attribute name and possibly two ways:

  1. Simply reading attributes, to provide convention and parsing
  2. Observing attribute changes, if reacting to change is necessary

TBD: Declarative event handling

Action system like Stimulus or delegation like Backbone views? Both?

Readme

Keywords

none

Package Sidebar

Install

npm i javastick

Weekly Downloads

0

Version

0.3.0

License

MIT

Unpacked Size

66.4 kB

Total Files

8

Last publish

Collaborators

  • rhumaric