Back to Lightweight Charts

Web Components - Custom Element

website/tutorials/webcomponents/01-custom-element.mdx

5.2.017.2 KB
Original Source

Web Components - Custom Element

:::info

The following describes a relatively simple example that only allows for a single series to be rendered. This example can be used as a starting point, and could be tweaked further using our extensive API.

:::

This guide will focus on the key concepts required to get Lightweight Charts™ running within a Vanilla JS web component (using custom elements). Please note this guide is not intended as a complete step-by-step tutorial. The example web component custom element can be found at the bottom of this guide.

If you are new to Web Components then please have a look at the following resources before proceeding further with this example.

About the example custom element

The example Web Components custom element has the following features.

The ability to:

  • specify the series type via a component attribute,
  • specify the series data via a component property,
  • control the chart, series, time scale, and price scale options via properties,
  • enable automatic resizing of the chart when the browser is resized.

The example may not fit your requirements completely. Creating a general-purpose declarative wrapper for Lightweight Charts™ imperative API is a challenge, but hopefully, you can adapt this example to your use case.

Component showcase

Presented below is the finished wrapper custom element which is discussed throughout this guide. The interactive buttons beneath the chart are showcasing how to interact with the component and that code is provided below as well (within the example app custom element).

import BrowserOnly from '@docusaurus/BrowserOnly';

<BrowserOnly fallback={<div>Loading...</div>}> {() => { require('./assets/wc-example.js'); return <lightweight-chart-example></lightweight-chart-example>; }} </BrowserOnly>

Creating the chart

Web Components are a suite of different technologies which allow you to encapsulate functionality within custom elements. Custom elements make use of the standard web languages html, css, and js which means that there aren't many specific changes, or extra knowledge, required to get Lightweight Charts™ working within a custom element.

The process of creating a chart is essentially the same as when using the library normally, except that we are encapsulating all the html, css, and js code specific to the chart within our custom element.

Starting with a simple boilerplate custom element, as shown below:

js
(function() {
    class LightweightChartWC extends HTMLElement {
        connectedCallback() {
            this.attachShadow({ mode: 'open' });
        }

        disconnectedCallback() {}
    }

    // Register our custom element with a specific tag name.
    window.customElements.define('lightweight-chart', LightweightChartWC);
})();

The first step is to define the html for the custom element. For Lightweight Charts, all we need to do is create a div element to act as our container element. You can create the html by cloning a template (as seen in our usage example below) or by imperatively using the DOM JS api as shown below:

js
// hide-start
class LightweightChartWC extends HTMLElement {
    // ...
    // hide-end
    // Within the class definition
    connectedCallback() {
        // Create the div container for the chart
        const container = document.createElement('div');
        container.setAttribute('class', 'chart-container');

        this.shadowRoot.append(container);
    }
    // hide-line
}

Next we will want to define some basic styles to ensure that the container element fills the available space and that the element can be hidden using the hidden attribute.

js
// Outside of the Class definition
const elementStyles = `
    :host {
        display: block;
    }
    :host[hidden] {
        display: none;
    }
    .chart-container {
        height: 100%;
        width: 100%;
    }
`;

// ...

// hide-start
class LightweightChartWC extends HTMLElement {
    // ...
    // hide-end
    // Within the class definition
    connectedCallback() {
        // highlight-fade-start
        // Create the div container for the chart
        const container = document.createElement('div');
        container.setAttribute('class', 'chart-container');
        // highlight-fade-end
        // create the stylesheet for the custom element
        const style = document.createElement('style');
        style.textContent = elementStyles;
        this.shadowRoot.append(style, container);
    }
    // hide-line
}

Finally, we can now create the chart using Lightweight Charts™. Depending on your build process, you may either need to import Lightweight Charts™, or access it from the global scope (if loaded as a standalone script). To create the chart, we call the createChart constructor function, passing our container element as the first argument. The returned variable will be a IChartApi instance which we can use as shown in the API documentation. The IChartApi instance provides all the required functionality to create series, assign data, and more. See our Getting started guide for a quick example.

js
// hide-start
class LightweightChartWC extends HTMLElement {
    // ...
    // hide-end
    connectedCallback() {
        // highlight-fade-start
        // Create the div container for the chart
        const container = document.createElement('div');
        container.setAttribute('class', 'chart-container');

        // create the stylesheet for the custom element
        const style = document.createElement('style');
        style.textContent = elementStyles;
        this.shadowRoot.append(style, container);
        // highlight-fade-end

        // Create the Lightweight Chart
        this.chart = createChart(container);
    }
    // hide-line
}

Attributes and properties

Whilst we could encapsulate everything required to create a chart within the custom element, generally we wish to allow further customisation of the chart to the consumers of the custom element. Attributes and properties are a great way to provide this 'API' to the consumer.

As a general rule of thumb, it is better to use attributes for options which are defined using simple values (number, string, boolean), and properties for rich data types.

In our example, we will be using attributes for the series type option (type) and the autosize flag which enables automatic resizing of the chart when the window is resized. We will be using properties to provide the interfaces for setting the series data, and options for the chart. Additionally, the IChartApi instance will be accessable via the chart property such that the consumer has full access to the entire API provided by Lightweight Charts™.

Attributes

Attributes for the custom element can be set directly on the custom element (within the html), or via javascript as seen for the properties in the next section.

html
<lightweight-chart autosize type="area"></lightweight-chart>

Attributes can be set and read from within the custom element's definition as follows:

js
// read `type` attribute
const type = this.getAttribute('type');

// set `type` attribute
this.setAttribute('type', 'line');

It is recommended that attributes be mirrored as properties on the custom element (and reflected such that any changes appear on the html as well). This can be achieved as follows:

js
// hide-start
class LightweightChartWC extends HTMLElement {
    // ...
    // hide-end
    // Within the class definition
    set type(value) {
        this.setAttribute('type', value || 'line');
    }

    get type() {
        return this.getAttribute('type') || 'line';
    }
    // hide-line
}

We can observe any changes to an attribute by defining the static observedAttributes getter on the custom element and the attributeChangedCallback method on the class definition.

js
class LightweightChartWC extends HTMLElement {
    // Attributes to observe. When changes occur, `attributeChangedCallback` is called.
    static get observedAttributes() {
        return ['type', 'autosize'];
    }

    /**
     * `attributeChangedCallback()` is called when any of the attributes in the
     * `observedAttributes` array are changed.
     */
    attributeChangedCallback(name, _oldValue, newValue) {
        if (!this.chart) {
            return;
        }
        const hasValue = newValue !== null;
        switch (name) {
        case 'type':
            // handle the changed attribute
            break;
        case 'autosize':
            // handle the changed attribute
            break;
        }
    }
}

Properties

Properties for the custom element are read and set through javascript on a reference to a custom element's instance. This instance can be created using standard DOM methods such as querySelector.

js
// Get a reference to an instance of the custom element on the page
const myChartElement = document.querySelector('lightweight-chart');

// read the data property
const currentData = myChartElement.data;

// set the seriesOptions property
myChartElement.seriesOptions = {
    color: 'blue',
};

We can define setters and getters for the properties if we need more control over the property instead of it being just a value.

js
// hide-start
class LightweightChartWC extends HTMLElement {
    // ...
    // hide-end
    // Within the class definition
    set options(value) {
        if (!this.chart) {
            return;
        }
        this.chart.applyOptions(value);
    }

    get options() {
        if (!this.chart) {
            return null;
        }
        return this.chart.options();
    }
    // hide-line
}

As mentioned earlier, it is recommended that any API which accepts complex (or rich data) beyond a simple string, number, or boolean value should be property. However, since properties can only be set via javascript there may be cases where it would be preferable to define these values within the html markup. We can provide an attribute interface for these properties which can be used to define the initial values, then remove those attributes from the markup and ignore any further changes to those attributes.

js
// hide-line
class LightweightChartWC extends HTMLElement {
    /**
     * Any data properties which are provided as JSON string values
     * when the component is attached to the DOM will be used as the
     * initial values for those properties.
     *
     * Note: once the component is attached, then any changes to these
     * attributes will be ignored (not observed), and should rather be
     * set using the property directly.
     */
    _tryLoadInitialProperty(name) {
        if (this.hasAttribute(name)) {
            const valueString = this.getAttribute(name);
            let value;
            try {
                value = JSON.parse(valueString);
            } catch (error) {
                console.error(
                    `Unable to read attribute ${name}'s value during initialisation.`
                );
                return;
            }
            // change kebab case attribute name to camel case.
            const propertyName = name
                .split('-')
                .map((text, index) => {
                    if (index < 1) {
                        return text;
                    }
                    return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
                })
                .join('');
            this[propertyName] = value;
            this.removeAttribute(name);
        }
    }

    connectedCallback() {
        // ...

        // Read initial values using attributes and then clear the attributes
        // since we don't want to 'reflect' data properties onto the elements
        // attributes.
        const richDataProperties = [
            'options',
            'series-options',
            'pricescale-options',
            'timescale-options',
        ];
        richDataProperties.forEach(propertyName => {
            this._tryLoadInitialProperty(propertyName);
        });
    }
    // hide-line
}

These attributes can be used to define the initial values for the properties as follows (using JSON notation):

html
<lightweight-chart
    data='[{"time": "2022-09-14", "value": 123.45},{"time": "2022-09-15", "value": 123.45}]'
    series-options='{"color":"blue"}'
></lightweight-chart>

Accessing the chart instance or additional methods

The IChartApi instance will be accessible via the chart property on the custom element. This can be used by the consumer of the custom element to fully control the chart within the element.

js
// Get a reference to an instance of the custom element on the page
const myChartElement = document.querySelector('lightweight-chart');

const chartApi = myChartElement.chart;

// For example, call the `fitContent` method on the time scale
chartApi.timeScale().fitContent();

Using a Custom Element

Custom elements can be used just like any other normal html element after the custom element has been defined and registered. The example custom element will define and register itself (using window.customElements.define('lightweight-chart', LightweightChartWC);) when the script is loaded and executed, so all you need to do is include the script tag on the page.

Depending on your build step for your page, you may either need to import Lightweight Charts™ via an import statement, or access the library via the global variable defined when using the standalone script version.

js
// if using esm version (installed via npm):
// import { createChart } from 'lightweight-charts';

// If using standalone version (loaded via a script tag):
const { createChart } = LightweightCharts;

Similarily, the custom element can either be loaded via an 'side-effect' import statement:

js
// side-effect import statement (use within a module js file)
import './lw-chart.js';

or via a script tag:

html
<script src="lw-chart.js" defer></script>

Once the custom element script has been loaded and executed then you can use the custom element anywhere that you can use normal html, including within other frameworks like React, Vue, and Angular. See Custom Elements Everywhere for more information.

Standalone script example html file

If you are loading the Lightweight Charts™ library via the standalone script version then you can also load the custom element via a script tag (see above section for more info) and construct your html page as follows:

html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta
            name="viewport"
            content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"
        />
        <title>Web component Example</title>
        <script
            type="text/javascript"
            src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.development.js"
        ></script>
        <style>
            #my-chart {
                height: 100vh;
                width: 100vw;
            }
        </style>
    </head>

    <body style="padding: 0; margin: 0">
        <lightweight-chart
            id="my-chart"
            autosize
            type="line"
            series-options='{"color": "red"}'
            data='[{ "time": "2018-10-19", "value": 52.89 },{ "time": "2018-10-22", "value": 51.65 }]'
        ></lightweight-chart>

        <script src="lw-chart.js" defer></script>
    </body>
</html>

Complete Sample Code

Presented below is the complete custom element source code for the Web component. We have also provided a sample custom element component which showcases how to make use of these components within a typical html page.

Wrapper Custom Element

The following code block contains the source code for the wrapper custom element.

<p> <a href={require('!!file-loader!./assets/lw-chart.js').default} download="lw-chart.js" target="\_blank" > Download file </a> </p>

import CodeBlock from '@theme/CodeBlock'; import InstantDetails from '@site/src/components/InstantDetails'; import wrapperCode from '!!raw-loader!./assets/lw-chart.js';

<InstantDetails> <summary>Click here to reveal the code.</summary> <CodeBlock className="language-js">{wrapperCode}</CodeBlock> </InstantDetails>

Example Usage Custom Element

The following code block contains the source code for the custom element showcasing how to use the above custom element.

<p> <a href={require('!!file-loader!./assets/wc-example.js').default} download="wc-example.js" target="\_blank" > Download file </a> </p>

import exampleCode from '!!raw-loader!./assets/wc-example.js';

<InstantDetails> <summary>Click here to reveal the code.</summary> <CodeBlock className="language-js">{exampleCode}</CodeBlock> </InstantDetails>