README
litecomponent
A renderer-agnostic ultralight weight CustomElement
for the modern web, that provides Component semantics with the highest possible performance, highest possible flexibility, lowest possible cognitive and abstractive overhead, depending only on the web-component standard with <1KB size (more like 800 bytes gzipped, really).
Installation
NPM
npm install litecomponent
It provides both es6 modules, that can be accessed as litecomponent/lib
, or cjs by default. Has pkg.module
defined for es6 bundlers, like webpack. So feel free to just use litecomponent
.
Unpkg
To use directly, in the browser.
<script src="https://unpkg.com/litecomponent@latest/dist/index.bundle.js"></script>
It's exported under the name litecomponent
.
Goals
- Provide a full fledged minimal component abstraction with full render control, as stated in the project description.
- JavaScript ecosystem today is huge with new and innovative ways of rendering popping in and out everyday. Eventhough,
litecomponent
has a core goal to stand on it's own, it's flexibility and minimal abstraction makes it ideal to be able to mix and match renderers, and usehyperhtml
,lit-html
,React
,Vue
,Mithril
,Inferno
,CycleJs
etc side-by-side, package each of them as individual isolated and standards compliant web components in the same project, without worrying about one affecting the other. - Do all of the above at no extra cost of performance, or cognitive overhead.
- While you can do this right away by providing your own
render
logic, I'd like to consider maintaining components likeLitHtmlComponent
,HyperHtmlComponent
,ReactLiteComponent
as supported components in the future, under the same project for a more seamless experience.
Features
- It's super simple, and tiny. Read the source.
- Zero dependencies.
- It's renderer and view agnostic. Define your own render logic, if you need, but it has the boilerplate.
- Define your views in
lit-html
,hyperhtml
,jsx
,document.createElement
,React.createElement
or even direct html strings: Your call. (I highly recommendlit-html
orhyperhtml
). You can even use React, or Vue's renderer if full VDOM is your thing and you'd like to package them up as isolated web-components quickly. Better yet - you can use them all in the same application. - It only uses W3C standards, and simply sits on top of the Custom Elements API providing similar conventions.
- Provides an extremely simple Elm like suggestion for dealing with state, but it's really upto to you.
- It's provides
queueRender
,renderNow
, andclearRenderQueue
- all of them do what they precisely say. No misnomer or complications like inReact
whererender
actually means, return a view. (I'd actually call it a design bug in React. It has nothing to do with rendering. It just builds a view - I'd have called itview
). - Explicit control of rendering. You say, when and where to render. But has very sensible automatic rendering logic that's extremely simply to understand, like when an load, update, attribute changes, etc. But everything can be overriden.
- Operates natively on the DOM. There's no VDOM overhead unless you bring it with you (which you happily can, of course!)
Examples
Basic
import { LiteComponent, RenderManager } from "litecomponent";
import { html, render } from "lit-html";
// Set the render function. By default it's a noop.
// Set it only once per application, or alternatively,
// override `_render` function and write your own render logic.
// render is any function that takes two args,
// return item of be rendered (return value of `view`), and the dom node itself.
// lit-html render function is exactly the same.
// Modify other appropriately, depending what you decide to return from the view.
RenderManager.render = render;
class App extends LiteComponent {
view() {
return html`
<div>Hello world!</div>
`;
}
}
customElements.define("x-app", App);
// HTML
// <html><x-app></x-app></html>
Same as above using localized render.
import { LiteComponent } from "litecomponent";
import { html, render } from "lit-html";
class LitHtmlComponent extends LiteComponent {
// Override this function to change any rendering logic.
// This can use hyperhtml, React, Vue, or any custom logic
// as desired.
_render() {
render(this.view(), this.getRenderRoot());
}
}
class App extends LitHtmlComponent {
view() {
return html`
<my-nav></my-nav>
<div>Hello world!</div>
`;
}
}
class Nav extends LitHtmlComponent {
view() {
return html`
<nav>Oo, my nav!</nav>
`;
}
}
Functional
let nameIt = (attrs) => {
// attrs is the actual attributes object
// given out by the DOM. (HTMLElement.attributes)
return html`
<div>Hello ${attrs.name.value}!</div>
`;
}
// registerTag is just for convenience. You can also simply use:
// customElements.define("x-app", LiteFn(App));
registerTag("x-app", nameIt);
// HTML
// <html><x-app name="Jane"></x-app></html>
Timer
class App extends LiteComponent {
constructor() {
super();
this.time = new Date();
this.timerHandle = null;
}
// Note: connected does not mean the component is fully loaded.
// It just means it's connected to the DOM tree. But, if you desire
// load semantics, just call renderNow to finish rendering immediately.
// Components are, by default 'predictably' lazy.
connected() {
super.connected();
this.timerHandle = setInterval(() => {
this.time = new Date();
// The default algorithm uses requestAnimationFrame for scheduled renders.
// So, doesn't matter how many times you call queueRender. It coalesces them as
// expected. But you can use `renderNow`, if you intend otherwise.
// Also, you can use `clearRenderQueue` at any point if you wish to cancel
// any scheduled renders.
this.queueRender();
}, 100);
}
disconnected() {
clearInterval(this.timerHandle);
super.disconnected();
}
view() {
return html`
<div>Time is ${this.time}!</div>
`;
}
}
Simple state management
class App extends LiteComponent {
constructor() {
super();
// If you wish to be stateless, you can pass it
// as attributes, but hey, this example is really just to showcase
// update, so we'll do all sorts of things -- because you can and still
// do so, with sanity.
this.time = new Date();
// Let's do this, just for fun, even though the super.load,
// automatically queues a render.
this.renderNow();
}
connected() {
super.connected();
this.timerHandle = setInterval(() => {
this.dispatch("tick", new Date());
}, 1000);
// Ah, because we can! Also, we already know that we've already rendered
// initial state. So no need to even schedule it again. (Note, this is still okay,
// even if we do, DOM won't be rendered again, since `lit-html` will diff and won't
// really apply anything to the DOM! How cool!
this.clearRenderQueue();
}
disconnected() {
clearInterval(this.timerHandle);
super.disconnected();
}
update(msg, val) {
switch (msg) {
case "tick": { this.time = val; break; }
case "skip": {
// This returns false, so render doesn't get scheduled.
return false;
}
case "evil": {
this.querySelector("div").innerText = "HAHAHA!";
this.renderNow();
return false;
}
}
return super.update();
}
view() {
// If you pass it as attributes from the outside,
// There's no state. Simply do `this.getAttribute` here,
// and print and render from here.
return html`
<div>Time is ${this.time}!</div>
`;
}
}
// If you'd like attrChanged to be fired, you need to set observedProperties,
// as per the DOM spec for custom elements.
// App.observedAttributes = ["value"];
// Oh yeah, you can do this as well.
App.tag = "x-app";
register(App);
API
The entire API is so tiny and simple. You're probably better of reading the source, so you know exactly what it does internally as well.
Here's the LiteElement
:
// Simply returns the next view representation.
// It's recommended to have this as a pure function.
view() { }
// After render method, executed immediately after rendering.
rendered() { }
// When element is a part of the DOM tree.
// called by connectedCallback. Default action is to queue a
// render.
connected() { this.queueRender(); }
// When element is removed from the DOM tree.
// called by disconnectedCallback. Default action is to clear any
// scheduled renders.
disconnected() { this.clearRenderQueue(); }
// Called by adoptedCallback. Default action is to queue a
// render.
adopted() { this.queueRender(); }
// Called by attributeChangedCallback. Default action is to queue a
// render.
attributeChanged(name, oldVal, newVal) { this.queueRender(); }
// Provide the root for the rendering. By default, it provides back the
// element itself (self). If a Shadow DOM is used/needed, then this
// method can be overridden to return the shadow root instead.
getRenderRoot() { return this; }
// Render immediately.
renderNow() {
this.clearRenderQueue();
this._render();
this.rendered();
this._postRender();
}
// Queue a render using the RenderManager scheduler.
queueRender() {
if (this.renderQueueToken !== null) return;
this.renderQueueToken = RenderManager.schedule(this.renderNow);
}
// Clear any previously scheduled render using the RenderManager scheduler.
clearRenderQueue() {
if (this.renderQueueToken === null) return;
RenderManager.cancel(this.renderQueueToken);
this.renderQueueToken = null;
}
/// Lifecycle connections
connectedCallback() { this.connected() }
disconnectedCallback() { this.disconnected() }
adoptedCallback() { this.adopted() }
attributeChangedCallback(name, oldValue, newValue) {
this.attributeChanged(name, oldValue, newValue) }
// Default impl of render, delegated to the RenderManager.
// This internal method can be overriden to provide custom render impls locally,
// while retaining the RenderManager semantics globally.
_render() {
RenderManager.render(this.view(), this.getRenderRoot());
}
_postRender() {
this.dispatchEvent(new Event("render"));
}
And now, the LiteComponent
:
// A component with a minimal opinion on how to handle state, providing
// two tiny additions: the update, and dispatch method, with no other
// changes or overhead.
export class LiteComponent extends LiteElement {
constructor() {
super();
// This is bound early for convenience,
// to be able to use in jsx/template html events.
this.dispatch = this.dispatch.bind(this);
}
// A method for handling state mutations and additional renders.
// Takes a message and value. Returning false, prevent scheduling
// another render. Default is to schedule another render on
// update.
//
// Note that scheduling and clearing renders are extremely cheap
// as long as it's in the same cycle before renders. So, use them
// freely.
update(msg, val) {
return true;
}
// Ideally, designed for dispatching an message which calls the update
// fn, through which state mutation can be handled from one place.
dispatch(msg, val) {
if (this.update(msg, val))
this.queueRender();
}
}
And finally RenderManager
is just a simple object with that holds some useful defaults.
export const RenderManager = function () {
// The default renderer, it's noop. Let the application provide
// a renderer.
let render = function (view, root) { };
// We already assume HTMLElement, so it's makes so sense testing for window and such.
let schedule = window.requestAnimationFrame ? window.requestAnimationFrame : setTimeout;
let cancel = window.cancelAnimationFrame ? window.cancelAnimationFrame : clearTimeout;
schedule = schedule.bind(this);
cancel = cancel.bind(this);
return { render, schedule, cancel };
}();
This is all it does. So, you can swap things out as you like keeping the micro-framework agnostic. That's it! You've almost read the entire source now. Cheers!
FAQ
- Help. I don't see anything on the screen.
The default render function is a noop
. You need to set RenderManager.render
.
One could argue that it could have a sensible default like setting innerHTML, or mutate the DOM with appendChild
, etc. But this way, it's explicit and will simply not render. You just need to do it once.
Alternatively, you can also override _render
, write your own render logic and make subclasses out of it. This is already shown for lit-html
in the examples above. In the future, I'd like to consider maintaining components like LitHtmlComponent
, HyperHtmlComponent
, ReactLiteComponent
as separate supported packages.
attributesChanged
not fired
Set YourComponent.observedAttributes = ["my", "attrs"];
, since Custom Elements are required to set that static property as per the DOM specifications. Please take a look at the custom elements API spec for more information.
- Element not yet rendered inside the
connected
method
The connected callback does not imply loaded. It just implies that the component is now in the DOM tree. So, if a render
is desired before any other action is performed, simply call renderNow
which will immediately finish rendering.
The default action of connected is to queueRender
, so that a render is performed, but the component will not be loaded by the time connected method is called.
This provides the advantage of being lazy, and having the flexibility to act both ways.