PLEASE NOTE: All Kickflip functionality has been merged back in the main SkateJS project. Use that instead.
Kickflip is now deprecated because all of its functionality has now been merged into the main Skate project.
Kickflip gives you a simple, opinionated wrapper around Skate to write functional web components against W3C specs.
- Custom elements backed by SkateJS that uses native Custom Element support if available.
- Functional rendering pipeline backed by Google's Incremental DOM.
- Public API exposed through partial-polyfilling of the Shadow DOM Spec using our Named Slot Prollyfill.
- Because we polyfill the relevant DOM methods and properties for the named-slot API, Kickflip is inherently compatible with libraries such as Incremental DOM, Virtual DOM and React while still offering a native DOM API for consumers of your components.
Package managers:
bower install kickflip
jspm install npm:kickflip
npm install kickflip
- Global / UMD bundles are located in
dist/
. - UMD sources are located in
lib/
. - ES2015 modules are located in
src/
.
Here's some examples of how to build web components with Kickflip.
<x-hello name="John"></x-hello>
import { string } from 'kickflip/src/properties';
import { div } from 'kickflip/src/vdom';
import kickflip from 'kickflip';
kickflip('x-hello', {
properties: {
name: string()
},
render (elem) {
div(`Hello ${elem.name}!`);
}
});
<x-counter></x-counter>
import { number } from 'kickflip/src/properties';
import { div } from 'kickflip/src/vdom';
import kickflip from 'kickflip';
const intervals = new WeakMap();
kickflip('x-counter', {
properties: {
count: number({ default: 0 })
},
attached (elem) {
intervals.set(elem, setInterval(function () {
++elem.count;
}, 1000));
},
detached (elem) {
clearInterval(intervals.get(elem));
},
render (elem) {
div(`Count: ${elem.count}`);
}
});
<x-todo>
<x-item>Item 1</x-item>
<x-item>Item 2</x-item>
<x-item>Item 3</x-item>
</x-todo>
import kickflip, { emit, link, render } from '../../src/index';
import create, { button, form, input, p, slot, text } from '../../src/vdom';
const Xtodo = kickflip('x-todo', {
events: {
'list-add' (e) {
const item = Xitem();
item.textContent = e.detail;
this.appendChild(item);
},
'item-remove' (e) {
this.removeChild(e.detail);
}
},
render (elem) {
create('x-list');
}
});
const Xlist = kickflip('x-list', {
events: {
submit (e) {
e.preventDefault();
emit(this, 'list-add', { detail: this.value });
}
},
properties: {
value: { default: '' }
},
render (elem) {
form(function () {
input({ onkeyup: link(elem), value: elem.value });
button({ type: 'submit' }, 'add');
});
slot();
}
});
const Xitem = kickflip('x-item', {
events: {
'click button' (e) {
emit(this, 'item-remove', { detail: this });
}
},
render () {
slot();
text(' ');
button('remove');
}
});
The Kickflip API exports the following from the Skate API:
On top of that, it provides several other API functions to bring Skate into a functional, named-slot world.
The main kickflip()
function takes the same arguments as the skate()
function, it just massages them to make using them a bit more streamlined and opinionated.
The behaviour of the following options are altered:
Properties are always linked to attributes (attribute: true
) unless explicitly specified. Properties now accept a render
option that tells the component whether or not it should re-render if the corresponding property is set. If a function
, it should return whether or not setting the property should queue a render()
. It receives the same information set()
gets. You can also specify a boolean
value.
The render()
function is wrapped so that it always creates a shadow root and renders using Incremental DOM.
The link()
function returns a function that you can bind as an event listener. The handler will take the event and propagte the changes back to the element
. This essentially allows for 2-way data-binding but is safer as the propagation of the user input value back to the component element will trigger a re-render ensuring all dependent UI is up to date.
import { string } from 'kickflip/src/properties';
import { input } from 'kickflip/src/vdom';
import kickflip, { link } from 'kickflip';
kickflip('my-input', function () {
properties: {
value: string()
},
render (elem) {
input({ onchange: link(elem), type: 'text' });
}
});
By default the propSpec
defaults to e.currentTarget.getAttribute('name')
or "value"
which is why it wasn't specified in the example above. In the example above, it would set value
on the component. If you were to give your input a name, it would use the name from the event currentTarget
as the name that should be set. For example if you changed your input to read:
input({ name: 'someValue', onchange: link(elem), type: 'text' });
Then instead of setting value
on the component, it would set someValue
.
You may explicitly set the property you would like to set by specifying a second argument to link()
:
link(elem, 'someValue')
The above link would set someValue
on the component.
You can also use dot-notation to reach into objects. If you do this, the top-most object will trigger a re-render of the component.
link(elem, 'obj.someValue')
In the above example, the obj
property would trigger an update even though only the someValue
sub-property was changed. This is so you don't have to worry about re-rendering.
You can even take this a step further and specify a sub-object to modify using the name of the currentTarget
(or value
, of course) if propSpec
ends with a .
. For example:
input({ name: 'someValue', onchange: link(elem, 'obj.'), type: 'text' });
The above example would set obj.someValue
because the name of the input was someValue
. This doesn't look much different from the previous example, but this allows you to create a single link handler for use with multiple inputs:
const linkage = link(elem, 'obj.');
input({ name: 'someValue1', onchange: linkage, type: 'text' });
input({ name: 'someValue2', onchange: linkage, type: 'checkbox' });
input({ name: 'someValue3', onchange: linkage, type: 'radio' });
select({ name: 'someValue4', onchange: linkage }, function () {
option({ value: 1 }, 'Option 1');
option({ value: 2 }, 'Option 2');
});
The above linkage would set:
obj.someValue1
obj.someValue2
obj.someValue3
obj.someValue4
The property render()
function is called before re-rendering the component. If must return true
in order for a re-render to occur. If you specify a boolean
instead of a function for this option, then true
will always re-render and false
will never re-render. This option defaults to a function
that returns true if oldValue
is !==
to newValue
and false
otherwise.
The state
function is a getter or setter depending on if you specify the second state
argument. If you do not provide state
, then the current state of the component is returned. If you pass state
, then the current state of the component is set. When you set state, each property is set which queues a re-render depending on if the properties have specified they want to re-render.
Component state is derived from the declared properties. It will only ever return properties that are defined in the properties
object. However, when you set state, whatever state you specify will be set even if they're not declared in properties
.
Kickflip includes several helpers for creating virtual elements with Incremental DOM.
The elementName
argument is the name of the element you want to create. This can be a string or function that has the id
or name
property set, which makes it compatible with any function as well as Skate component constructors (that use id
because WebKit doesn't let you re-define name
).
The attributesOrChildren
argument is either an object
, a function
that will render the children for this element or a string
if you only want to render a text node as the children.
The children
argument is a function
that will render the children of this element or a string
if you are only rendering text.
create('select', { name: 'my-select' }, function () {
create('option', { value: 'myval' }, 'My Value');
});
The vdom
API also exports functions for every HTML5 element. You could rewrite the above example using those instead:
import { option, select } from 'kickflip/src/vdom';
select({ name: 'my-select' }, function () {
option({ value: 'myval' }, 'My Value');
});
The text()
function is also exported directly from Incremental DOM and you could use that if you wanted to instead of specifying as string:
import { option, select, text } from 'kickflip/src/vdom';
select({ name: 'my-select' }, function () {
option({ value: 'myval' }, function () {
text('My Value');
});
});
This is very useful if you need to render text with other elements as siblings, or do complex conditional rendering.
The vdom
API also exports a slot()
function so that you can use named slots.
import { slot } from 'kickflip/src/vdom';
slot();
Kickflip adds some opinionated behaviour to Incremental DOM.
We ensure that if you pass the class
attribute, that it sets that via the className
property.
This gives the virtual element a key
that Incremental DOM uses to keep track of it for more efficient patches when dealing with arrays of items.
ul(function () {
li({ key: 0 });
li({ key: 1 });
});
Any attribute beginning with on
will be bound to the event matching the part found after on
. For example, if you specify onclick
, the value will be bound to the click
event of the element.
button({ onclick: e => console.log(e) }, 'Click me!');
You can also bind to custom events:
create('my-element', { onsomecustomevent: e => console.log(e) });
Events are added / removed using addEventListener()
/ removeEventListener()
so they will fire for bubbling events.
This tells Incremental DOM to skip the element that has this attribute. This is automatically applied when slot()
is called as the slotted elements will be managed by the parent component, not by the current diff tree. Elements that have this attribute cannot have children.
This is also helpful when integrating with 3rd-party libraries that may mutate the DOM.
div({ skip: true });
This is an array that tells Incremental DOM which attributes should be considered static.
div({ statics: ['attr1', 'prop2'] });
If you specify false
as any attribute value, the attribute will not be added, it will simply be ignored.
Returns the current version of Kickflip.
The vdom
module is provided for a simple way to write virtual DOM using only functions. If you don't want to do that, you can use any templating language that compiles down to Incremental DOM calls.
If you wanted to use JSX, for example, you'd have to add support for compiling JSX down to Incremental DOM calls. This can be done using the following modules:
- https://github.com/thejameskyle/incremental-dom-react-helper
- https://github.com/jridgewell/babel-plugin-incremental-dom
Once that is set up, you may write a component using JSX. For example, a simple blog element may look something like:
import { string } from 'kickflip/src/properties';
import { IncrementalDOM } from 'kickflip/src/vdom';
import kickflip from 'kickflip';
kickflip('my-element', {
properties: {
title: string()
},
render (elem) {
<div>
<h1>{elem.title}</h1>
<slot name="description" />
<article>
<slot />
</article>
</div>
}
});
And it could be used like:
<my-element title="Eggs">
<p slot="description">Article description.</p>
<p>Main paragraph 1.</p>
<p>Main paragraph 2.</p>
</my-element>