custom-elements-helper
A collection of Custom Elements helpers for the v1 spec.
This documentation is in alphabetical order. If you want a basic understanding of how things work, skip the Attributes & Elements chapters. It's most important to know how BaseController and defineCustomElement works. Everything else is optional.
Attributes
Attributes are packages of properties & methods that you can mix in on your custom elements controllers.
Important: Usage on native HTML elements, e.g. <img>
or <a>
, is not possible due to browser support restrictions.
Media
The media
attribute adds media query support to your custom element.
Properties
-
media
-string
- Value of the media attribute -
matchesMedia
-boolean
- True when the current viewport matches the media query
Methods
-
whenMediaMatches()
- Returns aPromise
that resolves once the media query first is matched -
whenMediaUnmatches()
- Returns aPromise
that resolves once the media query first is unmatched -
watchMedia(match, unmatch)
- Takes two callbacks that are called when the media query matches and unmatches, respectively
Example usage
The following example will mixin the media attribute.
The init().render().bind()
cycle from the base controller will only start once the viewport matches the (min-width: 768px)
media query (because of the resolve()
override).
Javascript
import { defineCustomElement, BaseController, AttrMedia as media } from 'custom-elements-helpers';
defineCustomElement('foo-bar', {
attributes: [ media ],
controller: class extends BaseController {
resolve() {
return Promise.all([
super.resolve(),
this.whenMediaMatches()
]);
}
}
});
HTML
<foo-bar media="(min-width: 768px)">
</foo-bar>
Touch Hover
The touch-hover
attribute adds touch support for hover interactions.
It adds is-touch
and is-hover
classes to your custom element so that you can style your animations accordingly.
Properties
-
touchHover
- Returnsauto
whentouch-hover
is set. Else, returnsfalse
.
Methods
-
enableTouchHover()
- Call this method inside thebind
method of your custom element
This binds some methods to touchstart & click, so that it works like this:
On non-touch devices, a hover is a hover and a click is a click.
On touch devices, a first tap adds the is-touch
class to the element. From this moment on you know that the current device is a touch device. The first tap also adds the is-hover
class. A second tap removed the is-hover
class.
It also makes sure the touch device's native :hover
handling gets blocked properly so you can handle it like you wish.
Example usage
import { defineCustomElement, BaseController, AttrTouchHover as touchHover } from 'custom-elements-helpers';
defineCustomElement('foo-bar', {
attributes: [ touchHover ],
controller: class extends BaseController {
bind() {
this.enableTouchHover();
return this;
}
}
});
<foo-bar class="foo-bar" touch-hover>
Foo!
<span class="foo-bar__show-on-hover">
Bar!
</span>
</foo-bar>
.foo-bar {
display: inline-block;
position: relative;
}
.foo-bar__show-on-hover {
display: none;
position: absolute;
left: 0;
top: 120%;
width: 100%;
}
.foo-bar:not(.is-touch):hover .foo-bar__show-on-hover,
.foo-bar.is-hover .foo-bar__show-on-hover {
display: block;
}
Controllers
Currently, there is only one controller, conveniently named BaseController.
BaseController
It's important to extend from this controller if you create an element using defineCustomElement
from the utils.
A BaseController instance is a standardized wrapper for your custom element logic. It lays out a consistent blueprint across developers & projects that streamlines the lifecycle of your controller.
Lifecycle: Birth
With Custom Elements, your controller gets triggered by your browser automatically when the element you defined gets attached to the DOM. This is what happens next:
Step 1: Resolve
The resolve
method should return a Promise
.
It allows you to wait for something, preload something, … If you want your controller to kick in immediately, return a resolved Promise
, by using return Promise.resolve(true)
.
The default behavior is to wait until document.readyState === 'complete'
.
Once the promise has resolved, your custom element will have the is-resolved
class attached. You can use this to style your element differently when it's in a loading state.
Step 2: Init
Once your custom element instance is resolved, the init
method is called. You can do your initial setup here.
Step 3: Render
After your initial setup, the render
method is called. You can assume a DOM-ready and configured environment here. this.el
has a reference to the custom element DOM node so go wild (if you have to).
Step 4: Bind
With everything rendered, now's the time to attach your event listeners, using this.on
and this.once
. Read more below.
Working with events
There are four methods on the BaseController that will help you tremendously when working with events and custom elements. When you use these methods, you make sure that event listeners that you've added are removed when the custom element is no longer in the DOM etc.
You use on(name, handler, target = null, options = false)
when you want to listen for an event. target
falls back to this.el
. You can also pass a selector in the name (e.g. click .button
). When you do this, your handler gets a second argument pointing to the target.
Some examples:
this.on('mouseenter', (e) => {
console.log('Mouse entered the custom element');
});
this.on('click .button', (e, target) => {
console.log('Click on', button.textContent');
});
this.on('mousemove', (e) => {
console.log('Mouse move somewhere on body');
}, document.body);
The once()
method has the same footprint but will remove itself after one event.
These event listeners can also be removed. Use off(name, target = null)
to remove an event listener registered through on
. target
falls back to this.el
.
Keep in mind that all targets for the same event should be removed separately. If you have a click handler on both this.el
and document.body
, you would have to call:
this.off('click'); // Falls back to `this.el`
this.off('click', document.body);
A custom element can also emit events to communicate to other parts of your website.
Using emit(name, data = {}, options = {})
you can easily pass around data.
The data
object will be passed on to your handler as e.detail
. The options
object can have bubbles
and cancelable
as specified in the spec.
An example:
// A setter on your `foo-bar` element
set current(to) {
this._current = to;
this.emit('foo-bar:current', { current: this._current });
}
// In another custom element its `bind()` method
this.on('foo-bar:current', (e) => {
console.log(`Current changed to ${e.detail.current}`);
}, document.body);
Lifecycle: Death
When an element gets detached from the DOM, the BaseController goes through some housekeeping methods.
Step 1: Destroy
Remove the is-resolved
class from the element.
Step 2: Unbind
Remove all event listeners that you registered through on
or once
.
Elements
AJAX Form
Wrap AJAX form around a working <form>
to have it send over AJAX.
Form submits are supported over GET
, POST
and JSONP (with the jsonp
attribute).
The AJAX form submits all form data to the form's action
, using the form's method
.
By default, it looks for an element with class js-ajax-form-success
to show when the submit was successful. When a form submit is successful, the form will be removed (by default). Likewise, it looks for an element with class js-ajax-form-error
to show an error message.
Override the default success & error handlers by overriding onSuccess(res)
and onError(err)
.
Roadmap
- Client-side form validation
- Handling server-side form validation
- Per-field error messages
Attributes
-
jsonp
-boolean
- Needs JSONP for AJAX handling (e.g. createsend)
Example usage
import { defineCustomElement, ajaxForm } from 'custom-elements-helpers';
defineCustomElement('my-ajax-form', {
attributes: ajaxForm.attributes,
controller: class extends ajaxForm.controller {
onSuccess(res) {
// Custom success handler
}
onError(err) {
// Custom error handler
}
}
});
<my-ajax-form>
<form action="/subscribe" method="POST">
<input type="email" name="email" />
<input type="submit" value="Subscribe" />
</form>
</my-ajax-form>
Key Trigger
You can use key trigger to bind a key to an anchor. It'll look for the href
attribute on itself or on the first child that has one and trigger the click
event on it.
Attributes
-
key
-integer
- Key code of the key you want to listen for
Example usage
import { defineCustomElement, keyTrigger } from 'custom-elements-helpers';
defineCustomElement('my-key-trigger', keyTrigger);
<my-key-trigger key="37">
<a href="/foo">Previous</a>
</my-key-trigger>
<my-key-trigger key="39">
<a href="/bar">Next</a>
</my-key-trigger>
Smooth State
Smooth State will listen for internal clicks and asynchronously fetch the contents and swap them, allowing you to transition between pages while not losing browser state.
Warning: this has quite a few known bugs. Use with caution (and test in Safari)
Methods
There are four public hooks where you can trigger animations. Each hook should return the transition
object it gets passed in as an argument. You can make modifications to this object & they will adapt the transition accordingly.
-
onBefore()
- Before the page is fetched -
onStart()
- While the page fetch is running, but before it is ready -
onReady()
- When the page fetch is ready, but before it is rendered -
onAfter()
- After the fetched page is rendered
Example usage
This is kind of complex to demo, example will be added later.
Utilities
Define
This exports the defineCustomElement(tag, options = {})
method. Use this instead of customElements.define
to register your custom elements.
The tag
is the name of your custom element. The spec defined this to need a hypen. If in doubt, use the mr
-prefix.
The options
object is where the magic happens. Currently, a attributes
and controller
key is supported. controller
is a class that extends from BaseController
(see chapter Controllers).
attributes
is an array of attributes that you want to support on your custom element. This removes repeating-yourself getters and setters into an easy to read syntax. You can also mix in more extensive behaviors from this library (see chapter Attributes). If you pass a mix-in it should provide a static attachTo
method. Some examples below:
import { AttrMedia as media } from 'custom-elements-helpers';
defineCustomElement('foo-bar', {
attributes: [
'stringattr',
{ attribute: 'intattr', type: 'int' },
{ attribute: 'boolattr', type: 'boolean' },
media // We can do this because it exposes .attachTo
]
});
The type int
parsed this.intattr
as an integer, so it's an actual number value.
The type boolean
makes sure this.boolattr
is always boolean.
The type string
is the default.
Events
These methods are currently scoped for internal use only.
Quick heads up:
parse
parses an event name like click .button
getPath
is a cross-browser equivalent for e.path
(This should be made public)
HTML
parseHTML(html, selector = null)
takes a string of HTML and returns an object { title, content, meta }
.
The title is the document title.
The content is the DOM node(s) matching your selector. If no selector is given, the whole parsed body will be returned.
The meta key holds an array of { name, property, content }
meta tags. This matches with the <meta name="foo" property="bar" content="baz">
. The property
attribute is only used by OpenGraph, AFAIK. Other meta tags will only have the content
attribute. The name viewport
is blacklisted, to avoid accidentaly setting or removing the viewport metatag.
renderNodes(content, container)
takes a DOM node content
and renders its children into container
. This removes existing content from container
.
cleanNodes(nodes, selector)
takes a DOM node nodes
and filters its child nodes against selector
. If the child node matches selector
, it gets removed from nodes
. Keep in mind that this works on the reference itself so it will change the original DOM node.
Polyfill
To polyfill, we recommend the document-register-element
polyfill. To keep polyfill usage opt-in, it's not required as a dependency.
It's as simple as:
import installCustomElements from 'document-register-element';
installCustomElements(window);