1.2.1 • Published 5 years ago

ilm-components v1.2.1

Weekly downloads
2
License
MIT
Repository
github
Last release
5 years ago

ilm-components

ilm-components is a library for developing Web Components using the v1.x Custom Elements and Shadow DOM specifications. It targets component definition and registration, instead of providing a complete framework for app development.

Introduction

ilm-components provides an abstraction for developing Web Components which work in all modern browsers where the Web Components polyfills are supported. Components work with both native Web Components implementations and the webcomponentsjs polyfills, without further work on the part of the developer.

Library features include component inheritance, DOM rendering and attribute handling capabilities, as well as automatic polyfilling of the Web Components spec when required.

The library is intended to be a minimal abstraction of the essentials required to develop Web Components. It has a lightweight footprint and focuses solely on abstracting component definition and registration. If you're looking for a complete component framework, you'll likely prefer something like React or Polymer.

Components are written in pure JavaScript and are intended to be bundled for distribution in apps. Previous versions supported fetching HTML and CSS resources directly to use as templates; this functionality has now been removed in favour of simple pure JavaScript definitions.

Continue reading for an overview of the library, as well as a complete API reference.

Installation

To install the library:

npm install ilm-components

A postinstall script will run. This copies the Web Components polyfills to ./libs/ilm-components in your project directory. This directory must be distributed with your application and deployed to your web server.

When your app loads, the framework will check whether browser polyfills are required. If they are, it will transparently load them from the library directory.

At uninstall, the ./libs/ilm-components directory will be automatically removed from your project root.

You can change where the polyfills are stored using configuration keys. Note that there is no way to disable the automatic npm copying/deletion to the default directory, unless you disable npm scripts entirely for the operation.

Important: npm script path resolution to your root directory is currently just ../../ (from the ilm-components node_modules directory) because npm provides no reliable way to get the working directory of npm install. If you have a complex project structure, the scripts may not execute as intended - be careful, and disable the scripts and manually copy the polyfills if required!

Upgrading to v1.x from v0.x

ilm-components v1.x is a significant upgrade over v0.x and there is currently no advised upgrade path. Upgrading existing components to support v1.x is a non-trivial effort that will likely require significant work.

Changes in v1.x include fundamental changes to the core API, including its operation and method signatures, as well as deep conceptual changes in the framework's operation. Of particular note is the absolute removal of direct HTML/CSS template imports over the network, as well as the removal of v0.x "attribute setter" support.

If you currently have components written using v0.x, it is recommended you continue to use v0.x while you evaluate how to migrate your project to v1.x.

v0.x is no longer supported and will no longer be maintained going forward.

Getting started

ilm-components aims to make Web Components easier to work with for small projects and libraries using a collection of components.

The framework exposes a single Promise-based entrypoint that allows you to load and define web components you wish to use in your application.

The components will be automatically registered and initialised, after the Web Components polyfill is downloaded (if required). Once the promise resolves, your components are upgraded and ready to use in your application.

Defining a component

To define a new component, create a class extending from the ilm-components Component export.

Important: If you define your own constructor, remember to call super(...) first!

import {Component} from "ilm-components";

var attributes = {};

class MyComponent extends Component {

	constructor() {
		super();
	}

	static get name() {
		return "my-component";
	}

}

export default MyComponent;

The above code sample demonstrates a fully-functioning component! You can create instances of the component in the DOM using the my-component element tag - this is defined by the name static getter in the JavaScript source.

<my-component></my-component>

Initialising the framework

The code sample in "Defining a component" won't work just yet. We need to register it with the framework so the browser is aware of its existence.

Before we can register components, ilm-components itself has to be initialised. Call the init(...) method on the framework and wait for the returned promise to be resolved.

import ilm from "ilm-components";
import {MyComponent} from "./MyComponent.js";

ilm.init().then(() => {});

The above code sample initialises ilm-components. You can pass a configuration object to init(...) to define the framework's behaviour.

Why is initialisation required? ilm-components uses this time to check whether Web Components are supported in the browser and, if not, download the required polyfills. The promise gets resolved once Web Components are ready to use - either natively, or from the polyfills.

Registering your components

With ilm-components initialised, you can finally register your components!

Call register(...), passing an array of component classes to register. When the promise resolves, the components are upgraded and ready to use.

Expanding on the previous example:

import ilm from "ilm-components";
import {MyComponent} from "./MyComponent.js";

ilm.init().then(() => {ilm.register([MyComponent])});

The registration process will polyfill the component definition if required and then define it as a custom element. All existing instances of the element in the DOM will be upgraded to the complete definition. Your component is ready to use!

Components

This section provides detailed guidance on developing components.

Name

Component names are set in the name static getter.

This refers to the HTML element name to register the component as.

Per the Web Components spec, it must include a -.

The name does not need to match the component class name, although commonly it will do.

In component instances, the component name is available as the name property.

Construction

During construction, the component's properties are collected from static getters (see the API reference section on this).

The component's shadow DOM is then created and populated (see rendering), and polyfilled with ShadyCSS if required. The shadow DOM mode is determined by the shadowMode static setter - see API reference below for more details. Shadow DOM event listeners are then attached.

Connected callback

The connected callback event is used to attach the component's light DOM and register light DOM event listeners - this cannot be done during construction as custom elements must not acquire children during construction.

If you implement connectedCallback(...) yourself in a component, you must call super.connectedCallback(...) if you are using the light DOM - or it will not get attached!

Attributes

Define component attributes in the attrs static getter. Return an array of attribute names.

These attributes are used as the component's "observed" attributes. When the attribute value changes, the component will be notified by the Web Components API.

You do not need to manually implement the observedAttributes getter defined in the Web Components spec. The ilm-components component class automatically presents your attributes as observed attributes.

In component instances, the component attributes are available as the attrs property (in extended components, this will not include attributes from the component's parents).

Changes to attributes

You do not need to manually implement attributeChangedCallback(...) - an implementation is already provided by the ilm-components component class.

When the value of an attribute defined in attrs changes, ilm-components will first try to invoke the component method with the signature ${attribute}ChangedCallback(new, old).

If this method does not exist, the value of this[attribute] will be set to the new attribute value, thus automatically mirroring DOM changes to state changes.

Note: This is currently one-way only. State changes are not yet automatically mirrored to DOM attribute values.

Event listeners

You can define event listeners within your components to automatically have them attached to DOM elements.

Event listeners are defined with the eventListeners static getter, which should return array of objects with the following structure:

  • event - event name to subscribe to (string)
  • selector - DOM selector string to identify element to subscribe to (string, as for querySelector(...))
  • bind - callback when event is fired (function)
  • all - whether to subscribe to the event on all elements matching selector, or just first (defaults to false)
  • dom - whether to apply the subscription to "shadow" or "light" DOM.

These subscriptions will be automatically applied after automatic initiation of each of your component's shadow and light DOMs.

Rendering

Component templates are defined inside your class.

Two methods are provided to return template content to render:

  • render(...) - Invoked during construction. Provides content to use for your component's shadow DOM.
  • renderDom(...) - Invoked when attached to the DOM. Provides content to use for your component's light DOM.

Both of these methods should return an HTMLTemplateElement or an HTML string. If a string is returned, it will be converted to an HTMLTemplateElement if it does not already define one as its root tag.

You may provide hooks in your template content (if it is a string) using slots to invoke additional render methods - this is described in more detail under rendering extended components, as this is the primary use case for this feature.

Important: Because the light DOM is constructed - and light DOM event handlers added - during DOM attachment in connectedCallback(...), you must remember to call super.connectedCallback(...) in any connectedCallback(...) override you implement - otherwise, your light DOM will not get constructed.

Extending components

You may extend components to create child components. You can only extend other ilm-components components - extending built-in elements is not supported, due to the current lack of support for is in the Web Components specs and community.

Basic extensions

Extend the component class and define a new name in the child.

Attributes of child components

Child components automatically inherit all the attributes of their parents. These are exposed automatically as observed attributes.

You can view the combined attributes of a component using the getAttributesAll(...) static method. You can get just the parent attributes with getAttributesParent(...).

Define the unique child attributes with the attrs static getter as normal. Existing parent attributes will be automatically defined, so the parent can continue to respond to attribute events as normal.

Rendering extended components

There are multiple approaches to rendering components which are extensions. They all apply to both render(...) and renderDom(...).

1. New template

The parent template is ignored and not used. Just re-declare the render method and return new content.

2. Parent template

Do not declare the render method in the child template.

3. Combine templates - in the child

In the child component, you can use super to call parent render methods. When combined with ES6 string literals, it is trivial to neatly interpolate the output of parent render methods into a new template.

4. Combine templates - in the parent

Similar to 3, but the parent provides hook methods invoked during rendering, the output of which can be customised by the child.

5. Use ilm-components template slots

This is a specialist implementation.

Slots in templates with a data-ilm attribute can be used to seamlessly provide template hook points in pure HTML strings.

When rendering, templates are parsed to find all the slots with a data-ilm attribute. The following process occurs:

  • Invoke the component method named after data-ilm; it should return a template element/HTML string akin to that to be returned by render(...)/renderDom(...).
  • Insert the template contents at the position of the slot in the DOM node being rendered.
  • Remove the slot.

This implementation is provided so HTML can be imported from files (using build tools) while still providing hooks for child templates to extend. Any slots without a data-ilm attribute are ignored and remain as regular slots for user-provided content in the DOM.

Lifecycle callbacks

ilm-components supports lifecycle callbacks which are run when key events occur within the framework. These are tied to the lifecycle of your components at current.

The currently supported lifecycle callbacks are:

  • construct – Fired during component construction, before the shadow DOM is attached
  • constructed – Fired during component construction, after the shadow DOM is attached (last event in constructor)
  • connect – Fired when component is connected to the DOM, before the light DOM is attached (first event in connectedCallback)
  • connected – Fired when component is connected to the DOM, after the light DOM is attached and component is completely connected (last event in connectedCallback).

Note that if you override constructor(...) or connectedCallback(...) in your components, this may impact the logical timing of these callbacks... per the advice above in components, always call the super method first before running code in your overrides.

To register a subscription to a callback, you should use the callbacks object in ilm-components configuration – see configuration below. Each named callback within the object has an array. Functions in the array will be excecuted sequentially, and synchronously, when the callback is triggered. Each callback is passed the current component instance.

These lifecycle events provide you an opportunity to take action whenever a new component is constructed or connected to the DOM – for example, to log its details, pass it some information, or attach it to an event engine.

You may also define your own lifecycle callbacks by calling hook(...) within your component (see the Component API reference below) and passing the name of your callback; any subscriptions registered for this callback in the callbacksconfiguration object will be invoked when you call the hook, and passed the component instance.

How it works

This section provides a brief overview of how the library functions.

Web Components v1.x is currently only fully supported in the newest versions of browsers. Polyfills are required in all older versions, and also in many popular browsers which remain in active use (e.g. IE11).

This presents challenges for working with Web Components today, as the polyfills are invasive. They can also be tricky to load if you want your application to run without the polyfills on those browsers which are already compatible with the spec.

To function correctly, the Web Components polyfills must be applied before you try to extend HTMLElement. Therefore, if you're creating a bundle which imports your component definitions at runtime and then trying to install the polyfills, things will likely not work as expected. You will likely end up dynamically loading your component definitions once Web Components are available, or have to add multiple script definitions.

ilm-components lets you turn this around: create a single bundle, link it in your head and then download the Web Components polyfill (if required) at runtime, without running into any issues with defining components before the platform is available.

The framework also supports the Web Components polyfills native shim for when you are transpiling components back to ES5 and need compatibility with all possible environments:

  • ES5/no native WC support
  • ES6/no native WC support
  • ES6/native WC support - see the shimEnabled configuration key to enable this feature if required.

Calling the framework's init(...) method returns a promise that resolves when Web Components are available, which may be immediate (in a browser with native support) or once the polyfills are downloaded and ready to use. You can then register your component definitions.

The astute reader will realise this alone doesn't solve the issue above described - if we're using a single bundle, we'll have still defined our component classes before the polyfill loaded, so our components won't work properly.

ilm-components avoids this by dynamically changing the prototype of the Component class. All your components extend from ilm-components' Component.

On browsers supporting Web Components out of the box, this simply extends from HTMLElement, but when not available the class is set to extend from the ilm-components Polyfill class (polyfill.js).

This uses reflection to dynamically construct the class as an instance of HTMLElement on construction. As no instances will ever be constructed before the polyfill is loaded, as we don't actually register our components until after the init(...) Promise resolves, we can reliably define components before the polyfills are downloaded, and still upgrade them correctly afterwards.

The final step is to check when registering our components whether the Component class is extending from the polyfill class; if it is, we then set the component's prototype to HTMLElement.prototype now the polyfills are available.

The result: We can transparently construct web components at runtime, using a single JS bundle in the script tag, and only download the polyfills when they're required.

What do you need to do? Add ilm-components to your project and make your components extend from Component. That done, you can call init(...) on the ilm export to obtain guaranteed Web Components support, wait for the Promise to resolve and then call ilm.register(...) with your component definitions.

API

This section provides detailed information on ilm-components and its usage.

API

ilm-components.js has the following exports:

  • ilm - ilm-components framework instance (default export)
  • Component - ilm-components component class, to be extended by all your component definitions
  • config - Configuration class instance
  • env - Information about environment and Web Components availability
  • utils - Utility methods used by the framework, primarily related to template and DOM processing, which of be of value to framework consumers

ilm

The ilm export is the ilm-components framework instance.

init(ilm={})

Initialise the framework.

Saves the configuration object ilm - see configuration.

Downloads and applies the Web Components polyfill/native shim if required.

Returns a Promise which resolves when Web Components are available and ready to use.

register(components)

Register the components in the array components.

Polyfills each component class if necessary, setting its constructor and prototype appropriately. The component is then registered with the browser's custom elements API and made ready to use.

After all components have been registered, the document is upgraded to transform existing element instances in the DOM into the new components.

Returns a Promise that resolves when all components are registered and ready to use.

Component

The Component export is the ilm-components component class from which all components should inherit. This documentation describes the public interface methods available to component instances.

Properties

  • name - see static getter.
  • attrs - see static getter.
  • shadowMode - see static getter.

Static getters

  • name - Get component element name (string)
  • attrs - Get component attributes (array)
  • eventListeners - Default event listeners to register - see event listeners (array of objects)
  • shadowMode Get mode of component shadow DOM (string, open)
  • observedAttributes - For Web Components API; returns getAttributesAll(...) by default (array)

Static methods

  • getAttributes(...) - returns attrs by default
  • getAttributesAll(...) - returns the component's attributes, as well as all those attributes inherited from parents
  • getAttributesParent(...) - get attributes from parent, if the component has a parent component

hook(hook)

Call a lifecycle callback hook.

All subscriptions registered for the callback in the callbacks configurations object (array of callbacks to invoke) will be invoked sequentially and synchronously, and passed the component instance.

render()

Invoked at construction to render the element's shadow DOM.

Return an HTMLTemplateElement or string (will be converted to an HTMLTemplateElement).

renderDom()

Invoked when attached to the DOM to render the element's light DOM.

Return an HTMLTemplateElement or string (will be converted to an HTMLTemplateElement).

renderTemplate(html, css=null, template=true)

Render a template consisting of HTML and CSS.

Returns a fully-formed template with the CSS added as the first <style> tag, if specified.

If template is false, the template will not be wrapped in <template> tags, so the innerHTML/content will effectively be returned.

Returns a string of the template innerHTML/outerHTML depending on template, as described above.

constructTemplate(tpl)

Construct a component DOM template.

tpl - string of template content.

Creates a template element from the passed value and parses any component DOM slots found within it. Returns the generated document fragment.

consumeDocumentFragmentStyles(frag, {dom="shadow", consume=true}={})

Extract styles from a DocumentFragment and append them to the component's root stylesheet, then optionally remove the style nodes from the DocumentFragment.

  • frag - DocumentFragment to extract from
  • dom - Name of the DOM root to return (either shadow for shadow root, or light/anything else for light/real DOM).
  • consume - Remove styles from the document fragment after extraction

Invoked by attachDom(...) and attachShadowDom(...) during construction when rendering the component's templates since #56 to address the v1.1.x issue whereby when rendering component slot templates, the component would end up with multiple style tags as a consequence of each slot template having its own stylesheet – thus breaking first-child and other CSS selectors. Using this model, the component's styles are all contained within a single style as the true first-child of the root component (one each for light and shadow DOMs).

getDom({dom="shadow"}={})

Get the component's DOM root node.

  • dom - Name of the DOM root to return (either shadow for shadow root, or light/anything else for light/real DOM).

Returns the specified DOM root node.

getDomSlotContent(slot)

Get the contents of a component's DOM slot.

  • slot - An HTMLSlotElement which is a valid component DOM slot (has a data-ilm attribute).

Returns a document fragment corresponding to the component's DOM slot contents.

getStyleTagFromNode(node, {create=false, add=true}={})

Get a style tag from a DOM node. Returns the first found style tag in the node, or, if none is found, optionally creates one and appends it to the node.

  • node - Node to get style tag from
  • create - Create new tag if not existing found
  • add - When creating new tag, prepend it to the DOM node after creation as the first child

Returns a style tag or null as described above (null if no existing tag found and creation disabled).

renderDomSlots(dom)

Render all the component DOM slots in a DOM node and return the updated DOM node. Extracts all the component DOM slots, gets their content and inserts it into the DOM node in the place of the slot (removing the slot).

attachEventListeners(listeners)

Attach event listeners. Accepts an array of event listener object definitions as described in event listeners and registers them all.

publishEvent(event, {detail=null, bubbles=true, init={}}={})

Publish an event. Dispatches an event on the component, constructing a new CustomEvent. The init property can contain additional properties to set on the event.

Events will be composed by default, so they will passthrough the shadow DOM boundary. This can be disabled by setting compose: false in init.

$(selector, all=false)

Query DOM. This queries the shadow DOM with querySelector(...) strings. Returns one element, or all matching elements if all.

\$$(selector, all=false)

Query light DOM. This queries the light DOM with querySelector(...) strings. Returns one element, or all matching elements if all.

css(css)

Prepare a CSS string for use in a template by wrapping it in <style></style> tags.

html(html)

Prepare an HTML string for use in a template by wrapping it in <template></template> tags.

template(html, css=null, tag=false)

Prepare a template from HTML/CSS. Alternative to renderTemplate(...) added in v1.1.5 as response to #54; uses renderTemplate(...) but does not wrap in <template> by default (can be enabled with tag).

connectedCallback()

Default handler for connectedCallback(...) which attaches the component's light DOM, since this cannot be performed in construction as custom elements cannot acquire children during construction.

attributeChangedCallback(attribute, old, value)

Default handler for attributeChangedCallback(...). Invokes the method ${attribute}ChangedCallback(...) with (value, old), if it exists, or else sets this[attribute] to the new value.

Configuration

The following configuration keys are available:

KeyDescriptionDefault
envEnvironment - when in production, disables loggingproduction
pathsPaths used by the framework to download polyfills (see below)-
shimEnabledWhether to use the ES5 native shim. When enabled, the shim will be downloaded in browsers where Web Components are available natively. This should be enabled if you are transpiling component definitions back to ES5 and need to maintain compatibility with browsers that natively support Web Components. Read the documentation for more details.true
polyfillWrapFlushCallbackIf enabled, intercept polyfill wrapFlushCallback events during component registration, to defer document upgrades until all components are registered - this can significantly improve upgrade performance. Read the polyfill documentation for more details on how this works.true
callbacksObject defining lifecycle callback subscriptions. Define callbacks as named keys, with value of an array of function callbacks to call when the lifecycle event occurs. See lifecycle callbacks documentation for more details.N/A

Configuration is currently set when initialising the framework with init(...).

Paths

The paths key is an object with the following keys:

  • / - Root path (all other paths resolved relative to this) - .
  • polyfill - Polyfill path (relative to root) - /libs/ilm-components/webcomponents-lite.js
  • polyfillShim - ES5 native shim path (relative to root) - /libs/ilm-components/native-shim.js

Paths can be set in configuration by passing a new object with the paths to change set.

End of documentation.

©James Walker 2018. Licensed under the MIT License.

1.2.1

5 years ago

1.2.0

5 years ago

1.1.6

5 years ago

1.1.5

6 years ago

1.1.4

6 years ago

1.1.3

6 years ago

1.1.2

6 years ago

1.1.1

6 years ago

1.1.0

6 years ago

0.4.1

6 years ago

0.4.0

6 years ago

0.3.1

6 years ago

0.3.0

6 years ago

0.2.1

6 years ago

0.2.0

6 years ago

0.1.7

6 years ago

0.1.6

6 years ago

0.1.5

6 years ago

0.1.4

6 years ago

0.1.3

6 years ago

0.1.2

6 years ago

0.1.1

6 years ago

0.1.0

6 years ago