/jail-js

Just-Another-Interesting-Library is a web components library written for Typescript to simplify using W3C Web Components.

Primary LanguageTypeScriptMIT LicenseMIT

JAIL.js - Just-Another-Interesting-Library

Build Status npm version License: MIT

Table of Contents

  1. Intro to Library
    1. Example
      1. Without the Library
      2. With the Library
    2. The Pieces
      1. Decorators
        1. Class Decorators
        2. Property Decorators
        3. Method Decorators
      2. Enumerations
      3. Interfaces
        1. Data Interfaces
        2. Component Interfaces
      4. Typeguards
    3. Getting Started
      1. Consume Library
      2. Build Components
        1. Create your Main HTML
        2. Create your HTML Template
        3. Create your Stylesheet
        4. Create your Javascript

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 &lt;template&gt;.</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 &lt;template&gt;.</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");
    }
}