This is the README for the master branch and is probably out of sync with the last stable release. To see the README for the last stable release select it from the list of tags.
Skate
Skate is a web component library that provides an API to bind behaviour to DOM elements. It's based on the W3C specification for Custom Elements.
- Provides a superset of the Custom Element Spec.
- Hooks for the spec'd lifecycle, templating, custom properties and event delegation.
- Small, 5k min+gz.
- Allows easy transition from selector-based behaviour binding to element binding.
Skate will soon be joining the jQuery Foundation Project Incubator and will be helping to define that process moving forward. Nothing will change for users or contributors before, during or after the transition.
HTML
<my-element></my-element>
JavaScript
skate('my-element', {
created: function (elem) {
elem.textContent = 'Hello, World!';
}
});
Result
<my-element>Hello, World!</my-element>
Documentation
Compatibility
See the Sauce Labs badge at the top.
Questions?
If you have any questions about Skate, feel free to join the public HipChat room and @mention a member.
Installing
You can download the source yourself and put it wherever you want. Additionally you can use Bower:
bower install skatejs
Or NPM:
npm install skatejs
Or JSPM:
jspm install npm:skatejs
Include either dist/skate.js
or dist/skate.min.js
.
UMD (AMD / CommonJS)
UMD files are located in lib/
. Simply require
the lib/index.js
file by whatever means you have and use it in accordance with whatever loader you've chosen.
ES6 Modules
The Skate source is written using ES6 modules. If you're using a transpilation method, then you can import skate from 'src/index';
and use it in your projects as you would any ES6 module.
Global
If you're still skating old school the dist
directory contains the compiled ES5 source. The compiled source does not use a module loader; everything will just work. Access Skate as a global with skate
.
Usage
You define a component by passing a component ID and definition to the skate()
function. The ID you specify corresponds to one of the following:
- Tag name
- Value of the
is
attribute - Attribute name
- Class name
The definition is an object of options that define your component.
skate('my-element', {
created: function (elem) {},
attached: function (elem) {},
detached: function (elem) {},
ready: function (elem) {},
render: function (elem) {
elem.innerHTML = 'Hello, World!';
},
attribute: function (elem, data) {
if (data.oldValue === undefined) {
} else if (data.newValue === undefined) {
} else {
}
},
events: {
click: function (e) {
this;
e;
e.delegateTarget;
},
'click my-element': function (e) {},
'click .something': function (e) {
this && e;
e.delegateTarget;
},
click: [
handler1,
handler2
],
'focus .something': function () {},
'blur .something': function () {}
},
extends: '',
properties: {
prop1: {
attribute: false,
serialize: function (value) {},
deserialize: function (value) {},
coerce: function (value) {}
default: 'default value'
get: function (element) {},
set: function (element, changeData) {}
}
},
prototype: {
get someProperty () {},
someMethod: function () {}
},
type: {}
resolvedAttribute: 'resolved',
unresolvedAttribute: 'unresolved'
});
Component Lifecycle
The component lifecycle consists of several paths in the following order starting from when the element is first created.
prototype
is set up in non-native (alread set up in native)events
are set upproperties
are definedcreated
is invokedrender
is invoked to render an HTML structure to the componentproperties
are initialisedready
is invokedattached
is invoked when added to the document (or if already in the document)detached
is invoked when removed from the documentattribute
is invoked whenever an attribute is updated
Each callback gets the element passed in as the first argument. The attribute callback gets an additional argument with information about the change:
skate('my-element', {
attribute: function (element, change) {
if (change.oldValue === undefined) {
} else if (change.newValue === undefined) {
} else {
}
}
});
The change object contains the following properties:
name
The name of the attribute that was changed.newValue
The new value of the attribute.oldValue
The old value of the attribute.
The attribute
callback is fired whenever an element attribute is created, updated or removed, but not if the attribute already exists on the element when it is first initialised. This is synonymous with the attributeChangedCallback
in the web component spec. The only differences are:
- Undefined attribute values are normalised to be
undefined
instead of null
to be consistent across the board. - The function signature is different: the element is the first argument and the parameters are consolidated into a change object.
Event Binding
skate('my-element', {
events: {
click: function (e) {
this;
e;
e.delegateTarget;
},
'click my-element': function (e) {},
'click .something': function (e) {
this && e;
e.delegateTarget;
},
click: [
handler1,
handler2
],
'focus .something': function () {},
'blur .something': function () {}
}
});
Constructing Elements
There's several different ways to construct an element.
Function Call
var myElement = skate('my-element', {});
var myElementInstance = myElement();
skate.create()
skate('my-element', {});
var myElementInstance = skate.create('my-element');
Constructor
While not the most elegant way, this serves as an ode to the spec.
var MyElement = skate('my-element');
var myElementInstance = new MyElement();
Hydrating Properties
For each of the ways you can construct an element, Skate also allows you to pass a properties object to them. The properties object is used to hydrate property values for the element.
var props = { propname: 'propvalue' };
var myElementInstance = myElement(props);
var myElementInstance = skate.create('my-element', props);
var myElementInstance = new MyElement(props);
Extending Elements
You may extend components using ES6 classes or your favorite ES5 library.
var XParent = skate('x-parent', {
static created () {
}
static get events {
return {
event1 () {}
}
}
});
var XChild = skate('x-child', class extends XParent {
static created () {
super.created();
}
static get events {
return class extends super.events {
event1 (e) {
super.event1(e);
}
event2 () {
}
};
}
});
Due to the semantics of ES6 classes, you must specify any non-prototype members as static. ES6 classes also do not support the object literal syntax. In order to specify properties, just use the getter syntax like we did with events
above.
Custom Methods and Properties
Skate gives you the option to specify custom properties and methods on your component.
skate('my-component', {
prototype: {
callMeLikeAnyNativeMethod: function () {}
}
});
These members are applied directly to the element instance that your component is bound to so you can do stuff like this:
document.getElementById('my-component-id').callMeLikeanyNativeMethod();
Asynchrony
Due to the fact that Skate uses Mutation Observers - and polyfills it for older browsers - elements are processed asynchronously. This means that if you insert an element into the DOM, custom methods and properties on that element will not be available right away. This will not work:
document.body.innerHTML = '<my-component id="my-component-id"></my-component>';
document.getElementById('my-component-id').someCustomMethod();
This is because the component will not be processed until after the block this code is in releases control back to the JavaScript engine. If you need to use the element right away, you must explicitly initialise it in a synchronous manner using skate.init()
:
var element = document.getElementById('my-component-id');
skate.init(element);
element.someCustomMethod();
This is very useful during testing, but can be used for any use case that requires synchronous operation.
API
The following are all available on the skate
object, or available for use from the src/api
or lib/api
folders.
create (componentName, elementProperties = {})
Creates an element for the specified component name
, ensures that it's synchronously initialized and assigns all props
to it. On the surface, this doesn't appear much different than document.createElement()
in browsers that support custom elements, however, there's several benefits that it gives you on top of being a single, consistent and convenient way to do things in any browser and environment.
For example, this can be called in any browser and it will behave consistently:
skate.create('my-element');
In browsers that support custom elements, it is equivalent to:
document.createElement('my-element');
In browsers that do not support custom elements, you would have to manually ensure that the element is initialised synchronously:
var element = document.createElement('my-element');
skate.init(element);
To take this example further, if we've extended an element:
skate('my-element', {
extends: 'div'
});
How we call this function does not change:
skate.create('my-element');
However, in native land this does change:
document.createElement('div', 'my-element');
And in polyfill land, it's much different:
var element = document.createElement('div');
element.setAttribute('is', 'my-element');
skate.init(element);
Both the native and polyfilled examples above expose too many implementation details. It's much better to have one simple and consistent way to create an element.
Alternatives
If you have access to the function / constructor returned from the skate()
call, invoking that does the same exact thing as skate.create()
:
var myElement;
var MyElement = skate('my-element', {});
myElement = skate.create('my-element');
myElement = MyElement();
myElement = new MyElement();
Setting Properties
All methods of constructing an element support passing properties.
myElement = skate.create('my-element', { prop: 'value' });
myElement = MyElement({ prop: 'value' });
myElement = new MyElement({ prop: 'value' });
Passing properties automatically assigns them to the element:
console.log(myElement.prop);
Why not just patch document.createElement()
?
Skate is designed to work with multiple versions of itself on the same page. If one version patches document.createElement()
differently than another, then you have problems. Even if we did do this, how document.createElement()
is called still depends on how the corresponding component has been registered, which is bad, especially when we can infer that information from the component definition.
emit (element, eventName, eventOptions = {})
Emits a CustomEvent
on element
that bubbles
and is cancelable
by default. This is useful for use in components that are children of a parent component and need to communicate changes to the parent.
skate('x-tabs', {
events: {
selected: hideAllAndShowSelected
}
});
skate('x-tab', {
events: {
click: function () {
skate.emit(this, 'selected');
}
}
});
It's preferrable not to reach up the DOM hierarchy because that couples your logic to a specific DOM structure that the child has no control over. To decouple this so that your child can be used anywhere, simply trigger an event.
Emitting Several Events at Once
You can emit more than one event at once by passing a space-separated string or an array as the eventName
parameter:
skate.emit(element, 'event1 event2');
skate.emit(element, [ 'event1', 'event2' ]);
Return Value
The native element.dispatchEvent()
method returns false
if the event was cancelled. Since skate.emit()
can trigger more then one event, a Boolean
return value is ambiguous. Instead it returns an Array
of the event names that were canceled.
Preventing Bubbling or Canceling
If you don't want the event to bubble, or you don't want it to be cancelable, then you can specify those options in the eventOptions
argument.
skate.emit(element, 'event', {
bubbles: false,
cancelable: false
});
fragment (...almostAnything)
Creates a document fragment from the specified node or HTML string and ensures any components within the fragment are synchronously initialised.
You can pass a DOM node:
skate.fragment(document.createElement('my-element'));
A string:
skate.fragment('<my-element></my-element>');
Any traversible item:
skate.fragment([document.createElement('my-element'), '<my-element></my-element>']);
Or a combination of those:
skate.fragment(
document.createElement('my-element'),
'<my-element></my-element>',
[document.createElement('my-element'), '<my-element></my-element>']
);
You are returned a document fragment that contains synchronously initialised components (if you added any components). It's just a normal DocumentFragment
so you can do anything you would normally be able to do. However, it should be noted that the methods aren't decorated to sync init components that you add after calling skate.fragment()
.
If you want to add components that are synchronously initialised, you can just chain skate.fragment()
calls:
skate
.fragment('<my-element></my-element>')
.appendChild(skate.fragment('<my-other-element></my-other-element>'));
init (...elements)
It's encouraged that you use skate.create()
and skate.fragment()
for creating elements and ensuring that they're synchronously initialised, however, there are edge-cases where synchronously initialising an existing element skate.init()
may be necessary.
skate.init(element1, element2);
You shouldn't use skate.init() in native to ensure descendant DOM is initialised as there's stuff native does that we don't emulate with it. See the docs on skate.ready()
for how you can interact with descendant components after they've been upgraded.
noConflict ()
Same as what you'd come to expect from most libraries that offer a global namespace. It will restore the value of window.skate
to the previous value and return the current skate
object.
var currentSkate = skate.noConflict();
No conflict mode is only available from the version in dist/
since that's the only version that exports a global.
ready (elementOrElements, callback)
The skate.ready()
function should not be confused with the ready
lifecycle callback. The lifecycle callback is called when the component element is ready to be worked with. It means that it's been templated out and all properties have been set up completely. It does not mean, however, that descendant components have been initialised.
Background
You maybe thinking "that sucks, why wouldn't they have been initialised?" That's a very good question. In order to realise the problem, we must first know how native custom elements behave.
If you put your component definitions before your components in the DOM loading component-a
before component-b
:
<script src="component-a.js"></script>
<script src="component-b.js"></script>
<component-a>
<component-b></component-b>
</component-a>
The initialisation order will be:
component-a
component-b
If you flip that around so that component-b
is loaded before component-a
, the order is the same. This is because the browser will initialise elements with their corresponding definitions as it descends the DOM tree.
However, if you put your component definitions at the bottom of the page, it gets really hairy. For example:
<component-a>
<component-b></component-b>
</component-a>
<script src="component-a.js"></script>
<script src="component-b.js"></script>
In this example, we are loading component-a
before component-b
and the same order will apply. However, if you flip that around so that component-b
is loaded before component-a
, then component-b
will be initialised first. This is because when a definition is registered via document.registerElement()
, it will look for elements to upgrade immediately.
The problem
If you want component-a
to be able to rely on component-b
being initialised, you'd have to put some constraints on your consumers:
- If you're running native, you must load your definitions at the bottom of the page. You must also ensure that you're loading
component-b
before component-a
. You could use a module loader to ensure component-b
is imported by component-a
, but you still have the constraint of making the consumer load the definitions at the bottom of the page. - If you're running in polyfill land, just make sure that you load
component-b
before component-a
. As above, you could just use a module loader for this.
The problem here is that your consumer is now concerned with implementation details and have constraints placed on them that they shouldn't have to worry about.
The solution
If you want to do something when component-b
is initialised, you can use skate.ready()
.
skate('component-a', {
created: function (elem) {
var b = elem.querySelector('component-b');
b.initialised;
skate.ready(elem.querySelector('component-b'), function (b) {
b.initialised;
});
},
render: function (elem) {
elem.innerHTML = '<component-b></component-b>';
}
});
skate('component-b', {
created: function (elem) {
elem.initialised = true;
}
});
Drawbacks
This does not solve the situation where you want to be notified of future elements that may be added somewhere in your descendant DOM. That is more a concern of what API you choose to expose to your consumers, the rendering path you choose and the problem you're trying to solve. This only concerns itself with the descendant nodes that you know exist. Most of the time this will come from the render
lifecycle callback.
render (element)
Renders invokes the render()
lifecycle callback on the specified element for the components that are bound to it. If no components are found for the element, nothing happens.
var hello = skate('x-hello', {
render: function (elem) {
elem.innerHTML = `Hello, ${elem.name || 'World'}!`;
}
});
var elem = hello();
elem.name = 'Bob';
skate.render(elem);
This makes it extremely useful when using properties because you can rewrite the above component to re-render itself while taking advantave of all that properties
has to offer:
var hello = skate('x-hello', {
properties: {
name: {
attribute: true,
default: 'World',
set: skate.render
}
},
render: function (elem) {
elem.innerHTML = `Hello, ${elem.name}!`;
}
});
var elem = hello();
elem.name = 'Bob';
If you are using the polyfill and are using custom bindings (i.e. classes and attributes) then it will invoke render()
in each of those if they are specified. It's recommended that you only bind one component that does rendering otherwise the result is not predictable. It's up to component authors to write components that follow best practices and it's up to component consumers to use components that follow best practices.
render.html(renderFunction)
This function exists for a simple, default way to render content to your host component. It doesn't do any special diffing or anything, it simply removes all current nodes and adds the new ones. You can return a document fragment, node or string (that will be converted to nodes).
var hello = skate('x-hello', {
render: skate.render.html(function (elem) {
return `Hello, ${elem.name || 'World'}!`;
})
});
Using this is good for simple components, or components where you're using properties to mutate the template that you render from here. Functional UI proponents won't like this method, but this offers the simplest, least opinionated method to build a component as Skate strives to have as little opinion about this as possible.
If you want to re-render your entire component but have it only update the parts that need updating, you can use something like skatejs-dom-diff in a custom renderer. For more information, see the next section.
Writing your own renderers
Writing your own renderers consists of writing a function that returns a function:
function render (renderFn) {
return function (elem) {
elem.innerHTML = renderFn(elem);
};
}
And you could use it like so:
render: render(function (elem) {
return `Hello, ${elem.name || 'World'}!`;
});
If you wanted to do something a little bit more complex, you could use something like skatejs-dom-diff as stated at the end of the previous section:
function render (renderFn) {
return function (elem) {
skateDomDiff.merge({
destination: skate.fragment(renderFn(elem)),
source: elem
});
};
}
And you could use it in the exact same way as used above. The only difference being that it will only update the parts of your element's tree that changed. Everything else stays intact as it was before.
version
Returns the current version of Skate.
Web Component Differences
Skate implements the Custom Element spec with a custom API but it does not polyfill the native methods. Since Skate is a custom element library, it does not polyfill ShadowDOM or HTML Imports.
You can do some pretty cool things with Skate that you can't do with Web Components. For example, you can write polyfills for existing elements:
<datalist>...</datalist>
:
skate('datalist', {
created: polyfillDatalistElement
});
<input placeholder="">
:
var typeAttribute = require('skatejs-type-attribute');
skate('placeholder', {
extends: 'input',
type: typeAttribute,
created: polyfillInputPlaceholder
});
<input type="date">
:
var typeAttribute = require('skatejs-type-attribute');
skate('type', {
extends: 'input',
type: typeAttribute,
properties: {
type: {
set: function (element, change) {
if (change.newValue === 'date') {
makeIntoDatepicker(element);
}
}
}
}
});
<link rel="import" href="path/to/import.html">
(HTML Imports):
var typeAttribute = require('skatejs-type-attribute');
skate('rel', {
extends: 'link',
type: typeAttribute,
properties: {
rel: {
set: function (element, change) {
if (change.newValue === 'import') {
makeIntoHtmlImport(element);
}
}
}
}
});
Custom bindings
Skate supports custom bindings such as the ability to bind functionality to elements that have a particular attribute or classname. This comes in handy when wanting to work with legacy code that uses class / attribute selectors to bind stuff to elements on DOMContentLoaded
because it negates the need to use selectors and / or DOMContentLoaded
altogether. Not only does this have added performance benefits because you're not running selectors or blocking, it also means that you don't have to run any manual initialisation code. Just write your HTML and things happen.
The actual binding functionality isn't built into Skate. Skate simply offers an API for you to use custom bindings that you or others have written. If you want to write a binding, all you have to do is provided a particular interface for Skate to call.
var myCustomBidning = {
create: function (componentDefinition) {
},
filter: function (element, componentDefinitions) {
}
};
There's some that we've already built for you over at https://github.com/skatejs/types.
Considerations
There's a few things that you must consider when building and using custom bindings:
1. You're deviating from the spec.
Skate will always be a superset of the custom element spec. This means that core-Skate will never stray too far from the spec other than offering a more convenient API and featureset.
2. Performance
The filter
callback is performance-critical. This function must be run for every single element that comes into existence. Be wary of this.
3. With great power comes great responsibility
No matter if we decided to expose this as API or not, we'd still have to do a similar algorithm behind the scenes. Since there are many use-cases where writing a component with the Skate API is useful, we felt it was best to offer safe, spec-backed defaults while giving developers a little bit of breathing room.
Transitioning Away from jQuery-style Plugins
Because Skate supports custom bindings as mentioned above, it allows you to do things like refactor your jQuery initialisation code without touching any HTML:
jQuery(function ($) {
$('.tabs').tabs();
});
There's several problems with this approach. First, you're running a selector against the document. This is unnecessary and can get slow in large DOMs even in the latest browsers. Second, it only gets executed on DOMContentLoaded
. If you want to dynamically add some tabs to your document, then you've got to manually call that again once they've been added to the DOM.
With Skate, those problems vanish. No selectors are run and your tabs will automatically be initialised regardless of when they are put into the document.
To refactor that into a Skate component, all you need to do is:
var typeClass = require('skatejs-type-class');
skate('tabs', {
type: typeClass,
created: function (element) {
jQuery(element).tabs();
}
});
Possibly the best part about this is that you don't need to touch any markup and only a minimal amount of JavaScript.
Native Support
If your component is bound via custom tags and your browser supports custom elements then Skate will use the native DOM implementation instead of using Mutation Observers which will have added performance benefits. This all happens underneath the hood and the API does not change.
Polyfills
As you may know, the only way to polyfill Mutation Observers is to use the deprecated DOM 3 Mutation Events. They were deprecated because if you insert 5k elements at once, you then trigger 5k handlers at once. Mutation Observers will batch that into a single callback.
Skate requires that you BYO your own Mutation Observers implementation. There are several out there:
Preventing FOUC
An element may not be initialised right away. To prevent FOUC, you can add the unresolved
attribute to any web component element and then use that attribute to hide the element in your stylesheets.
<style>
[unresolved] {
opacity: 0;
}
</style>
<my-element unresolved></my-element>
The unresolved
attribute will be removed after the created()
callback is called and before the attached()
callback is called.
Additionally, after removing the unresolved
attribute, Skate will add the resolved
attribute. This allows you to transition your styles:
[resolved] {
opacity: 1;
transition: opacity .3s ease;
}
Ignoring Elements
If you have a DOM tree that you don't want Skate to polyfill then you can add the data-skate-ignore
attribute. This is ideal for mitigating performance issues associated with older browsers and inspecting each element that is added to the document. Generally this is only an issue in Internet Explorer and dealing with hundreds of thousands of elements. If your browser natively supports Custom Elements then this attribute is ignored.
<div data-skate-ignore>
</div>
No Conflict
Skate has a noConflict()
method that we have come to expect from libraries that may come into conflict with the same name, or multiple versions of itself. It returns the new skate
while restoring the global skate
to the previous value.
var mySkate = skate.noConflict();
Multiple Version Support
On top of offering a no-conflict mode, Skate plays well with multiple versions of itself on the same page. Prior to version 0.11
Skate did not share a registry or mutation observers. 0.11
and later share a registry and a mutation observer. This means that trying to register the same component in 0.11
and 0.12
would result in an error. Sharing a mutation observer ensures that we don't have more than main mutation observer on the page scanning incoming elements which helps with performance.