JAIL.js - Just-Another-Interesting-Library
Table of Contents
Intro to Library
This library provides developers an abstraction from the work required to create and use native web components. It is built with Typescript, and targets the V1 spec of Web Components and Shadow DOM. It also utilizes features from HTML Imports and the Template tag, providing a full native suite of features.
Instead of trying to explain too much of why, I'll provide some examples of using the web components without the library vs using this library. This is a very limited view. You have to think of using these in more realistic projects with tens to hundreds of different components.
Please Note - This was written back when TS Mixins were expressed using decorators. Since TS has added actual mixins, the library needs to be updated to fit these changes. I've currently locked the version of TS to a version that the library was built on for now, so it will build and run.
Example
Without the Library
- index.html
<html>
<head>
<link rel="import" href="./basic-element.html" />
</head>
<body>
<basic-element>
<div>Hello World</div>
</basic-element>
</body>
</html>
- basic-element.html
<template id="basicElement">
<style>
p { color: orange; }
</style>
<p>Cool, I'm in Shadow DOM. My markup was stamped from a <template>.</p>
<input type="text" id="basicInput">
</template>
<script src="./script"></script>
- basic-element.ts
class BasicElement extends HTMLElement {
/**
* Property for the input child.
*/
private _input: HTMLInputElement;
constructor() {
super();
let ownerDocument = (document.currentScript || document._currentScript).ownerDocument;
let t = ownerDocument.querySelector('#basicElement');
let clone = t.content.cloneNode(true);
// Create a shadow DOM
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(clone);
this._input = shadowRoot.querySelector("#basicInput");
// Set an initial state for the component attributes.
this.setAttribute("attr1", 0);
this.setAttribute("attr2", true);
}
/**
* List all the attributes that should be watched.
*/
static observedAttributes() {
return [ "my-attr1", "my-attr2" ]
}
connectedCallback() {
// Bring in all the DOM children into the shadow DOM, so they get displayed.
for (let child of this.children) {
shadowRoot.appendChild(child);
}
}
attributeChangedCallback(attrName, oldValue, newValue) {
switch(attrName) {
case "my-attr1":
this.onAttr1Changed(oldValue, newValue);
break;
case "my-attr2":
this.onAttr2Changed(oldValue, newValue);
break;
}
}
onAttr1Changed(oldValue: number, newValue: number) {
let attr2 = this.getAttribute("my-attr2");
if (attr2) {
console.log(`Hello, the number value changed from ${oldValue} to ${newValue}`)
}
}
onAttr2Changed(oldValue: boolean, newValue: boolean) {
if (newValue) {
console.log(`Hello, your current value is ${this._input.value}`)
}
}
}
document.registerElement('basic-element', BasicElement);
Couple things to note with not using the library
- No css is being used here because its actually a very complex problem to link css from a file in HTML imports. The relative path of the imports will be whatever the hosting HTML file is, not the HTML file that is being imported. The
<style>
tag is used instead - Without using the web-components polyfill or writing something yourself, a race condition will be created where a parent component is finished loading and is beginning to do some of its functionality and a child component hasn't loaded yet; Unexpected behavior will occur if you try to use the child that hasn't loaded.
With the Library
- index.html
<html>
<body>
<basic-element>
<div>Hello World</div>
</basic-element>
</body>
<!--The classes of all the components in your application need to be loaded.-->
<!--This part could easily be one line if you already are using index module exports.-->
<script src="./basic-element"></script>
</html>
- basic-element.css
p {
color: orange;
}
- basic-element.html
<template>
<p>Cool, I'm in Shadow DOM. My markup was stamped from a <template>.</p>
<input type="text" id="basicInput">
</template>
- basic-element.ts
import { Component, Attributes, Attribute, AttributeChangedListener } from "jail-js/decorators";
/**__dirname isn't available for projects that aren't node based. See web example for how to deal with getting file locations.**/
@Component({
tagName: "basic-element",
templateUrl: `${__dirname}/basic-element.html`,
styleUrl: `${__dirname}/basic-element.css`
})
@Attributes([
{ name: "my-attr1"},
{ name: "my-attr2", value: true }
])
class BasicElement extends HTMLElement {
/**
* Property for the input child.
*/
@QuerySelector("#basicInput") private _input: HTMLInputElement;
@Attribute private myAttr1 = 0; // This value could have also been set using the @Attributes decorator by adding a value to the attribute info.
@Attribute private myAttr2;
@AttributeChangedListener("my-attr1")
onAttr1Changed(oldValue: number, newValue: number) {
if (this.myAttr2) {
console.log(`Hello, the number value changed from ${oldValue} to ${newValue}`)
}
}
@AttributeChangedListener("my-attr2")
onAttr2Changed(oldValue: boolean, newValue: boolean) {
if (newValue) {
console.log(`Hello, your current value is ${this._input.value}`)
}
}
}
The Pieces
Getting Started
A more detailed guide can be found here
To start building your own application using the library start by NPM installing:
npm install --save jail-js
No other steps need to be made to start using the library.
Consume Library
To consume the library in your JavaScript/Typescript:
import { [decorators to import] } from "jail-js/decorators";
import { [interfaces to import] } from "jail-js/interfaces";
import { [typeguards to import] } from "jail-js/typeguards";
import { [enumerations to import] } from "jail-js/enumerations";
Build Components
Create your main HTML
- index.html
<html>
<body>
<my-element></my-element>
<script src="./my-element.js"></script>
</body>
</html>
Create your HTML template
- my-element.html
<template>
<div id="child"></div>
</template>
Create your stylesheet
- my-element.css
#child {
color: red;
}
Create your Javascript
First import the pieces you need from the library, write your code, and prosper.
- my-element.ts
import { Component, Attribute, QuerySelector, Attributes, AttributeChangedListener } from "jail-js/decorators";
import { IOnConnected } from "jail-js/interfaces";
@Component({
tagName: "my-element",
templateURL: "path/to/my-element.html",
styleURL: "path/to/my-element.css" // or ["path/to/style1.css", "path/to/style2.css"]
})
@Attributes([
{ name: "first-attribute" },
{ name: "second-attribute", value: 0 }
])
export class MyElement extends HTMLElement implements IOnConnected {
/**
* Reads and writes the `first-attribute` attribute to the DOM.
*/
@Attribute public firstAttribute: boolean = true;
/**
* Reads and writes the `second-attribute` attribute to the DOM.
*/
@Attribute private secondAttribute: number;
/**
* Reads the child with the #child selector from the DOM.
*/
@QuerySelector("#child") private _child;
/**
* If you add a constructor, you must always call super() as the first line.
*/
constructor() {
super();
// Bind the handler to this event.
this.onChildClicked = this.onChildClicked.bind(this);
}
/**
* Hook up the child clicked event.
*/
onConnected() {
this._child.addEventListener("click", this.onChildClicked)
}
/**
* Increment the attribute when the child is clicked.
*/
onChildClicked() {
this.secondAttribute++;
}
/**
* Log when first-attribute is changed.
*/
@AttributeChangedListener("first-attribute")
onFirstAttributeChanged(oldValue, newValue) {
console.log("first-attribute was changed");
}
/**
* Log when second-attribute is changed.
*/
@AttributeChangedListener("second-attribute")
onSecondAttributeChanged(oldValue, newValue) {
console.log("second-attribute was changed");
}
}