
Security News
Deno 2.2 Improves Dependency Management and Expands Node.js Compatibility
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
Skate is a web component library based loosely on the Custom Element spec that allows you to define behaviour for custom and existing elements using tag names, attribute names and class names.
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.
HTML
<my-component></my-component>
JavaScript
skate('my-component', {
created: function (element) {
element.textContent = 'Hello, World!';
}
});
Result
<my-component>Hello, World!</my-component>
IE9+ and all evergreens.
If you have any questions about Skate, feel free to join the public HipChat room and @mention a member.
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
Include either dist/skate.js
or dist/skate.min.js
.
UMD files are located in lib/
. Simply import lib/skate.js
and use it as normal.
The Skate source is written using ES6 modules. If you're using a transpilation method, then you can import skate from 'src/skate';
and use it in your projects as you would any ES6 module.
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
.
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:
is
attributeThe definition is an object of options defining your component.
skate('my-component', {
// Called before the element is displayed.
created: function (element) {
},
// Called after the element is displayed.
attached: function (element) {
},
// Called after the element is removed.
detached: function (element) {
},
// Attribute callbacks that get triggered when attributes on the main web
// component are inserted, updated or removed. Each callback gets the
// element that the change occurred on and the corresponding changes. The
// change object contains the following information:
//
// - type: The type of modification (created, updated or removed).
// - name: The attribute name.
// - newValue: The new value. If type === 'removed', this will be undefined.
// - oldValue: The old value. If type === 'created', this will be undefined.
attributes: {
'my-attribute': {
// The element's default value if not already specified on the element.
// Can also be a function that returns a value. The function gets passed
// The element as its only argument.
value: 'default value',
// Called when the attribute is set for the first time.
created: function (element, change) {
},
// Called on all attribute modifications except for the intial creation.
updated: function (element, change) {
},
// Called when the attribute is removed.
removed: function (element, change) {
},
// Called when created, updated or remove is not specified. This is the
// same as specifying a function instead of an options object for a
// given attribute.
fallback: function (element, change) {
}
}
},
// The event handlers to bind to the web component element. If the event
// name is followed by a space and a CSS selector, the handler is only
// triggered if a descendant matching the selector triggered the event.
// This is synonymous with Backbone's style of event binding in its
// views.
events: {
'click': function (element, eventObject) {
},
'click .some-child-selector': function (element, eventObject, currentTarget) {
}
},
// Restricts a particular component to binding explicitly to an element with
// a tag name that matches the specified value. This value is empty by
// default.
//
// Depending on the component type, it behaves like so:
//
// - When applied to a custom element, the component ID is used to match the
// value of the element's `is` attribute and the element's tag name is
// matched against the value specified here. This conforms with the custom
// element spec.
//
// - When given to a component that binds to an element using an attribute,
// the value specified here must match the element's tag name.
//
// - When specified on a component that is bound using a class name, this
// value must match the element's tag name, as with attribute components.
//
// - If the value is empty, then the component is not restricted at all.
extends: '',
// Properties and methods to add to each element instance. It's notable
// that the element's prototype is not modified. These are added after the
// element is instantiated. Since the methods and properties are applied to
// the element, `this` inside a method will refer to the element.
prototype: {
callMeLikeAnyNativeMethod: function () {
}
},
// A function that renders a template to your element. You can literally use
// any templating engine you like here.
template: function (element) {
bindLatestHipsterTemplateEngineTo(element);
},
// The binding methods this component supports. For example, if you specify
// the `type` as `skate.type.ELEMENT`, then the component will only be bound
// to an element whos tag name matches the component ID.
//
// - `ELEMENT` Tag name only.
// - `ATTRIBUTE` Attribute names.
// - `CLASSNAME` Class names.
type: skate.type.ELEMENT,
// The attribute name to add after calling the created() callback.
resolvedAttribute: 'resolved',
// The attribute name to remove after calling the created() callback.
unresolvedAttribute: 'unresolved'
});
The component lifecycle consists of three callbacks:
created
Called before the element is displayed.attached
Called after the element is displayed.detached
Called after the element is removed.The created
callback gets triggered before the element is shown and is only ever fired once. Without full web-component support, we can only emulate the created
callback to ensure the element is hidden. For more information see Preventing FOUC. The attached
and detached
callbacks are fired each time the element is attached and detached from the DOM, which can happen multiple times.
As with the spec, when you define a component that is compatible with tag bindings, your call to skate()
will return an element constructor for you to use:
var MyComponent = skate('my-component', {
created: function (element) {
element.textContent = 'something';
},
prototype: {
logTextContent: function () {
console.log(this.textContent);
}
}
});
It is favourable to use a constructor in your code wherever possible because it will synchronously initialise the component and call the created
callback. Only when you insert it into the DOM will the attached
callback be called:
var element = new MyComponent();
// Logs: "something"
element.logTextContent();
// Asynchronously calls the `attached` callback.
document.body.appendChild(element);
An attribute lifecycle definition can take three forms. First, it does something similar to what we see in the Web Component spec:
skate('my-component', {
attributes: function (element, change) {
}
});
A notable difference, though, is that this callback gets called for attributes that already exist on the element as this is more predictable. This also allows you to have initialisation code for attributes, rather than forcing the developer to do this in one of the lifecycle callbacks.
This is called for each attribute on an element when:
The second form of a callback takes an object of attribues and handlers.
skate('my-component', {
attributes: {
'my-attribute': function handleInsertAndUpdate (element, change) {
}
}
});
This allows you to specify which attributes you want to listen to and will call the specified function when:
The third form gives you more granularity and flexibility, and is the same form that the example component at the top takes:
skate('my-component', {
attributes: {
'my-attribute': {
created: function (element, change) {
},
updated: function (element, change) {
},
removed: function (element, change) {
}
}
}
});
The created
handler gets called when:
The updated
handler gets called when:
The removed
handler gets called when:
Callbacks that get fired for attributes that already exist on an element get called after the attached
callback is triggered.
You may also specify a fallback
callback that will get called if a specific callback for the type of modification isn't found. For example, if you wanted to do the same thing on created
and updated
but something different on removed
, then you'd do something like:
skate('my-component', {
attributes: {
'my-attribute': {
fallback: function (element, change) {
},
removed: function (element, change) {
}
}
}
}
Note that doing:
skate('my-component', {
attributes: {
'my-attribute': {
fallback: function (element, change) {
}
}
}
}
Is the same thing as:
skate('my-component', {
attributes: {
'my-attribute': function (element, change) {
}
}
}
The only difference is that the former allows you to specify other options for that specific attribute.
If you want to specify a default value for your component you may do so by setting the default
option for an attribute.
skate('my-component', {
attributes: {
'my-attribute': {
value: 'default value'
}
}
}
That would ensure that the attribute my-attribute
is set to default value
but only if that attribute doesn't already exist.
Event binding allows you to declare which events you want to listen for and also offers you the ability to use event delegation, Backbone style.
As we saw above:
skate('my-component', {
events: {
'click': function (element, eventObject) {
},
'click .some-child-selector': function (element, eventObject, currentTarget) {
}
}
});
The first click
handler gets executed whenever the component receives a click event regardless of what triggered it. The second click .some-child-selector
handler gets executed only when it receives a click event that came from a descendant matching the .some-child-selector
selector, This will also get fired for any ancestor of the target, up to the component element, that matches the selector. The currentTarget
parameter is the element which the delegate selector matched.
Events listeners are not automatically removed from the element when it is removed from the DOM. This is because Skate does not know if you intend to re-insert the element back into the DOM. Skate leaves it up to you and the JavaScript engine's garbage collector to manage this.
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();
It's important to understand that the Element.prototype
is not modified as part of this process.
To template a component, all you need to do is define a function that takes an element as its first argument. When templating is invoked during an element's lifecycle, this function will be called and the element being Skated will be passed in as the first argument. It's up to you at this point to invoke whatever templating engine you want.
For example, Handlebars:
skate('my-component', {
template: function (element) {
var compiled = Handlebars.compile('<p>Hello, {{ name }}!</p>');
element.innerHTML = compiled({ name: element.getAttribute('name') });
}
});
A good way to reuse a template function is to simply create a function that takes a string and returns a function that templates that string. The following example will compile the HTML using Handlebars and when invoked it will take all the attributes on the element and pass them in to the compiled template function as the context. This way, you can use any of the attributes specified on the element.
function handlebarify (html) {
var compiled = Handlebars.compile(html);
return function (element) {
var attrs = {};
for (var a = 0; a < element.attributes.length; a++) {
var attr = element.attributes[a];
attrs[attr.name] = attr.value;
}
element.innerHTML = compiled(attrs);
};
}
skate('my-component', {
template: handlebarify('<p>Hello, {{ name }}!</p>')
});
If you wanted to fully embrace Web Components, you could even use Shadow DOM:
function shadowDomTemplate (shadowHtml) {
return function (element) {
element.createShadowRoot().innerHTML = shadowHtml;
};
}
skate('my-component', {
template: shadowDomTemplate('<header><content select="h2"></content></header><section><content></content></section>')
});
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.
Skate uses the Custom Element spec as a guideline but it does not polyfill it, nor does it polyfill the behaviour of 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="">
:
skate('placeholder', {
extends: 'input',
type: skate.type.ATTRIBUTE,
created: polyfillInputPlaceholder
});
<input type="date">
:
skate('type', {
extends: 'input',
type: skate.type.ATTRIBUTE,
attributes: {
type: function (element, change) {
if (change.newValue === 'date') {
makeIntoDatepicker(element);
}
}
}
});
<link rel="import" href="path/to/import.html">
(HTML Imports):
skate('rel', {
extends: 'link',
type: skate.type.ATTRIBUTE,
attributes: {
rel: function (element, change) {
if (change.newValue === 'import') {
makeIntoHtmlImport(element);
}
}
}
});
Because Skate can also bind to attributes and classes, it offers a way to transition away from jQuery-style plugins to web components.
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 DOMReady. 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:
skate('tabs', {
type: skate.type.CLASSNAME
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.
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.
Skate mostly polyfills Mutation Observers, but only internally. It is not usable outside of Skate at the moment since it only polyfills what Skate needs to function.
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. What Skate does to mitigate this overhead is to queue up each event to be processed on the next tick. Once queued, it then batches them up like Mutation Observers do, and then calls the callback only once.
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;
}
If you need to ignore an element and its descendants you can add the data-skate-ignore
attribute to an element.
<div data-skate-ignore>
<!-- Everything including the container will be ignored. -->
</div>
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 skate
to the previous value.
var mySkate = skate.noConflict();
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.
The .editorconfig
, .jscs
and .jshint
configs are all set up. If you can, enable these in your editor of choice.
To get a dev environment up and running, all you should need to do is run:
npm install
To run tests:
npm test
If you want to keep the Karma server alive to run them in your browser of choice:
npm test -- --keepalive
To run tests in a specific browser:
npm test -- --browsers Chrome,Firefox
To lint your files with jscs
and jshint
:
npm run lint
To build the distribution all you have to do is run:
npm run dist
This will build dist/skate.js
and dist/skate.min.js
. Don't worry about doing this in a PR; it'll avoid conflicts.
To build the lib
(ES5 + UMD) files:
npm run lib
To release all you've got to do is run npm release
. You can either specify the release type
, or tag
.
npm run release -- --tag x.x.x
Or:
npm run release -- --type minor
To deploy the documentation, run the following command from the branch or tag which you want to deploy:
npm run deploy
The MIT License (MIT)
Copyright (c) 2014 Trey Shugart
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
FAQs
Skate is a library built on top of the W3C web component specs that enables you to write functional and performant web components with a very small footprint.
The npm package skatejs receives a total of 3,556 weekly downloads. As such, skatejs popularity was classified as popular.
We found that skatejs demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 8 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
Security News
React's CRA deprecation announcement sparked community criticism over framework recommendations, leading to quick updates acknowledging build tools like Vite as valid alternatives.
Security News
Ransomware payment rates hit an all-time low in 2024 as law enforcement crackdowns, stronger defenses, and shifting policies make attacks riskier and less profitable.