javastick
v0.3.0
Published
Attach JavaScript behaviours to your pages' elements
Downloads
4
Readme
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
yarn add javastick
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
})
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 theobserver
- a
matcher
, that thehandler
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 namedetach(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 behavioursfindDescendantsWithBehaviors(element)
returning descendants of the elements that declares behaviours. It is distinct fromhasBehaviors
as it's likelydocument.querySelectorAll
or other methods would be faster than traversing the DOM and callinghasBehaviors
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:
- it can lead to a lot of events used on other pages being registered for nothing
- 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:
- they're extra elements in the DOM, potentially messing layout (though there's
display: contents
now) - composing them means adding more and more extra elements
- 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:
- It's a "Take everything" approach: don't need to track value changes in attributes, the code for handling it there anyway
- 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:
- Simply reading attributes, to provide convention and parsing
- Observing attribute changes, if reacting to change is necessary
TBD: Declarative event handling
Action system like Stimulus or delegation like Backbone views? Both?