Creating a Multiselect Component as a Web Component

Share this article

Update 12.05.2016:
Following some discussion in the comments, a second post has been written to address the shortcomings of this one — How to Make Accessible Web Components. Please be sure to read this, too. This article was peer reviewed by Ryan Lewis. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be! Web applications become every day more complicated and require a lot of markup, scripts and styling. To manage and maintain hundred kilobytes of HTML, JS, and CSS we try to split our application into reusable components. We try hard to encapsulate components and prevent styles clashing and scripts interference. In the end a component source code is distributed between several files: markup file, script file, and a stylesheet. Another issue we might encounter is having long markup cluttered with divs and spans. This kind of code is weakly-expressive and also hardly maintainable. To address and try to solve all these issues, W3C has introduced Web Components. In this article I’m going to explain what Web Components are and how you can build one by yourself.

Meet Web Components

Web Components solve all these issues discussed in the introduction. Using Web Components we can link a single HTML file containing the implementation of a component and use it on the page with a custom HTML element. They simplify the creation of components, strengthen encapsulation, and make markup more expressive. Web Components are defined with a suite of specifications:
  • Custom Elements: allow to register a custom meaningful HTML element for a component
  • HTML Templates: define the markup of the component
  • Shadow DOM: encapsulates internals of the component and hides it from the page where it’s used
  • HTML Imports: provides the ability to include the component to the target page.
Having describe what Web Components are, let’s have a look at them in action.

How to Build a Production-Ready Web Component

In this section, we’re going to build a useful multiselect widget that is ready to use in production. The result can be found on this demo page and the whole source code can be found on GitHub.

Requirements

First of all, let’s define some requirements to our multiselect widget. The markup should have the following structure:
<x-multiselect placeholder="Select Item">
    <li value="1" selected>Item 1</li>
    <li value="2">Item 2</li>
    <li value="3" selected>Item 3</li>
</x-multiselect>
The custom element <x-multiselect> has a placeholder attribute to define the placeholder of the empty multiselect. Items are defined with <li> elements supporting value and selected attributes. The multiselect should have the selectedItems API method returning an array of selected items.
// returns an array of values, e.g. [1, 3]
var selectedItems = multiselect.selectedItems();
Moreover, the widget should fire an event change each time selected items are changed.
multiselect.addEventListener('change', function() {
    // print selected items to console
    console.log('Selected items:', this.selectedItems()); 
});
Finally, the widget should work in all modern browsers.

Template

We start creating the multiselect.html file that will contain all the source code of our component: HTML markup, CSS styles, and JS code. HTML Templates allow us to define the template of the component in a special HTML element <template>. Here is the template of our multiselect:
<template id="multiselectTemplate">
    <style>
      /* component styles */
    </style>

    <!-- component markup -->
    <div class="multiselect">
        <div class="multiselect-field"></div>
        <div class="multiselect-popup">
            <ul class="multiselect-list">
                <content select="li"></content>
            </ul>
        </div>
    </div>
</template>
The component markup contains the field of the multiselect and a popup with the list of the items. We want multiselect to get items right from the user markup. We can do this with a new HTML element <content> (you can find more info about the content element on MDN). It defines the insertion point of the markup from shadow host (component declaration in user markup) to the shadow DOM (encapsulated component markup). The select attribute accepts CSS selector and defines which elements to pick from the shadow host. In our case we want to take all <li> elements and set select="li".

Create Component

Now let’s create a component and register a custom HTML element. Add the following creation script to the multiselect.html file:
<script>
    // 1. find template
    var ownerDocument = document.currentScript.ownerDocument;
    var template = ownerDocument.querySelector('#multiselectTemplate');

    // 2. create component object with the specified prototype 
    var multiselectPrototype = Object.create(HTMLElement.prototype);

    // 3. define createdCallback
    multiselectPrototype.createdCallback = function() {
        var root = this.createShadowRoot();
        var content = document.importNode(template.content, true);
        root.appendChild(content);
    };

    // 4. register custom element
    document.registerElement('x-multiselect', {
        prototype: multiselectPrototype
    });
</script>
The creation of a Web Component includes four steps:
  1. Find a template in the owner document.
  2. Create a new object with the specified prototype object. In this case we’re inheriting from an existing HTML element, but any available element can be extended.
  3. Define createdCallback that is called when component is created. Here we create a shadow root for the component and append the content of the template inside.
  4. Register a custom element for the component with the document.registerElement method.
To learn more about creating custom elements, I suggest you to check out Eric Bidelman’s guide.

Render Multiselect Field

The next step is to render the field of the multiselect depending on selected items. The entry point is the createdCallback method. Let’s define two methods, init and render:
multiselectPrototype.createdCallback = function() {
    this.init();
    this.render();
};
The init method creates a shadow root and finds all the internal component parts (the field, the popup, and the list):
multiselectPrototype.init = function() {
    // create shadow root
    this._root = this.createRootElement();

    // init component parts
    this._field = this._root.querySelector('.multiselect-field');
    this._popup = this._root.querySelector('.multiselect-popup');
    this._list = this._root.querySelector('.multiselect-list');
};

multiselectPrototype.createRootElement = function() {
    var root = this.createShadowRoot();
    var content = document.importNode(template.content, true);
    root.appendChild(content);
    return root;
};
The render method does the actual rendering. So it calls the refreshField method that loops over selected items and creates tags for each selected item:
multiselectPrototype.render = function() {
    this.refreshField();
};

multiselectPrototype.refreshField = function() {
    // clear content of the field
    this._field.innerHTML = '';

    // find selected items
    var selectedItems = this.querySelectorAll('li[selected]');

    // create tags for selected items
    for(var i = 0; i < selectedItems.length; i++) {
        this._field.appendChild(this.createTag(selectedItems[i]));
    }
};

multiselectPrototype.createTag = function(item) {
    // create tag text element
    var content = document.createElement('div');
    content.className = 'multiselect-tag-text';
    content.textContent = item.textContent;

    // create item remove button
    var removeButton = document.createElement('div');
    removeButton.className = 'multiselect-tag-remove-button';
    removeButton.addEventListener('click', this.removeTag.bind(this, tag, item));

    // create tag element
    var tag = document.createElement('div');
    tag.className = 'multiselect-tag';
    tag.appendChild(content);
    tag.appendChild(removeButton);

    return tag;
};
Each tag has a remove button. The remove button click handler remove the selection from items and refreshes the multiselect field:
multiselectPrototype.removeTag = function(tag, item, event) {
    // unselect item
    item.removeAttribute('selected');

    // prevent event bubbling to avoid side-effects
    event.stopPropagation();

    // refresh multiselect field
    this.refreshField();
};

Open Popup and Select Item

When the user clicks the field, we should show the popup. When he/she clicks the list item, it should be marked as selected and the popup should be hidden. To do this, we handle clicks on the field and the item list. Let’s add the attachHandlers method to the render:
multiselectPrototype.render = function() {
    this.attachHandlers();
    this.refreshField();
};

multiselectPrototype.attachHandlers = function() {
    // attach click handlers to field and list
    this._field.addEventListener('click', this.fieldClickHandler.bind(this));
    this._list.addEventListener('click', this.listClickHandler.bind(this));
};
In the field click handler we toggle popup visibility:
multiselectPrototype.fieldClickHandler = function() {
    this.togglePopup();
};

multiselectPrototype.togglePopup = function(show) {
    show = (show !== undefined) ? show : !this._isOpened;
    this._isOpened = show;
    this._popup.style.display = this._isOpened ? 'block' : 'none';
};
In the list click handler we find clicked item and mark it as selected. Then, we hide the popup and refresh the field of multiselect:
multiselectPrototype.listClickHandler = function(event) {
    // find clicked list item
    var item = event.target;
    while(item && item.tagName !== 'LI') {
        item = item.parentNode;
    }
    
    // set selected state of clicked item
    item.setAttribute('selected', 'selected');

    // hide popup
    this.togglePopup(false);

    // refresh multiselect field
    this.refreshField();
};

Add Placeholder Attribute

Another multiselect feature is a placeholder attribute. The user can specify the text to be displayed in the field when no item is selected. To achieve this task, let’s read the attribute values on the component initialization (in the init method):
multiselectPrototype.init = function() {
    this.initOptions();
    ...
};

multiselectPrototype.initOptions = function() {
    // save placeholder attribute value
    this._options = {
        placeholder: this.getAttribute("placeholder") || 'Select'
    };
};
The refreshField method will show placeholder when no item is selected:
multiselectPrototype.refreshField = function() {
    this._field.innerHTML = '';

    var selectedItems = this.querySelectorAll('li[selected]');

    // show placeholder when no item selected
    if(!selectedItems.length) {
        this._field.appendChild(this.createPlaceholder());
        return;
    }

    ...
};

multiselectPrototype.createPlaceholder = function() {
    // create placeholder element
    var placeholder = document.createElement('div');
    placeholder.className = 'multiselect-field-placeholder';
    placeholder.textContent = this._options.placeholder;
    return placeholder;
};
But this is not the end of the story. What if a placeholder attribute value is changed? We need to handle this and update the field. Here the attributeChangedCallback callback comes in handy. This callback is called each time an attribute value is changed. In our case we save a new placeholder value and refresh the field of multiselect:
multiselectPrototype.attributeChangedCallback = function(optionName, oldValue, newValue) {
    this._options[optionName] = newValue;
    this.refreshField();
};

Add selectedItems Method

All we need to do is to add a method to the component prototype. The implementation of the selectedItems method is trivial – loop over selected items and read values. If the item has no value, then the item text is returned instead:
multiselectPrototype.selectedItems = function() {
    var result = [];

    // find selected items
    var selectedItems = this.querySelectorAll('li[selected]');

    // loop over selected items and read values or text content
    for(var i = 0; i < selectedItems.length; i++) {
        var selectedItem = selectedItems[i];

        result.push(selectedItem.hasAttribute('value')
                ? selectedItem.getAttribute('value')
                : selectedItem.textContent);
    }

    return result;
};

Add Custom Event

Now let’s add the change event that will be fired each time the user changes the selection. To fire an event we need to create a CustomEvent instance and dispatch it:
multiselectPrototype.fireChangeEvent = function() {
    // create custom event instance
    var event = new CustomEvent("change");

    // dispatch event
    this.dispatchEvent(event);
};
At this point, we need to fire the event when the user selects or unselects an item. In the list click handler we fire the event just when an item was actually selected:
multiselectPrototype.listClickHandler = function(event) {
    ...
    
    if(!item.hasAttribute('selected')) {
        item.setAttribute('selected', 'selected');
        this.fireChangeEvent();
        this.refreshField();
    }
    
    ...
};
In the remove tag button handler we also need to fire the change event since an item has been unselected:
multiselectPrototype.removeTag = function(tag, item, event) {
    ...
    
    this.fireChangeEvent();
    this.refreshField();
};

Styling

Styling the internal elements of Shadow DOM is pretty straightforward. We attach few particular classes like multiselect-field or multiselect-popup and add necessary CSS rules for them. But how can we style list items? The problem is that they are coming from shadow host and don’t belong to the shadow DOM. The special selector ::content comes to our rescue. Here are the styles for our list items:
::content li {
    padding: .5em 1em;
    min-height: 1em;
    list-style: none;
    cursor: pointer;
}

::content li[selected] {
    background: #f9f9f9;
}
Web Components introduced a few special selectors, and you can find out more about them here.

Usage

Great! Our multiselect functionality is completed, thus we’re ready to use it. All we need to do is to import the multiselect HTML file and add a custom element to the markup:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="import" href="multiselect.html">
</head>
<body>
    <x-multiselect placeholder="Select Value">
        <li value="1" selected>Item 1</li>
        <li value="2">Item 2</li>
        <li value="3" selected>Item 3</li>
        <li value="4">Item 4</li>
    </x-multiselect>
</body>
</html>
Let’s subscribe to change event and print selected items to the console each time the user changes the selection:
<script>
    var multiselect = document.querySelector('x-multiselect');
    multiselect.addEventListener('change', function() {
        console.log('Selected items:', this.selectedItems());
    });
</script>
Go to the demo page and open browser console to see selected items each time the selection is changed.

Browsers Support

If we look at browser support, we see that Web Components are fully supported by Chrome and Opera only. Nevertheless, we can still use Web Components with the suite of polyfills webcomponentjs, which allows to use Web Components in the latest version of all browsers. Let’s apply this polyfill to be able to use our multiselect in all browsers. It can be installed with Bower and then included in your web page.
bower install webcomponentsjs
If we open the demo page in Safari, we’ll see the error in the console “null is not an object”. The issue is that document.currentScript doesn’t exist. To fix the issue, we need to get ownerDocument from the polyfilled environment (using document._currentScript instead of document.currentScript).
var ownerDocument = (document._currentScript || document.currentScript).ownerDocument;
It works! But if you open multiselect in Safari, you’ll see that list items are not styled. To fix this other issue, we need to shim styling of the template content. It can be done with theWebComponents.ShadowCSS.shimStyling method. We should call it before appending shadow root content:
multiselectPrototype.createRootElement = function() {
    var root = this.createShadowRoot();
    var content = document.importNode(template.content, true);

    if (window.ShadowDOMPolyfill) {
        WebComponents.ShadowCSS.shimStyling(content, 'x-multiselect');
    }

    root.appendChild(content);
    return root;
};
Congratulations! Now our multiselect component works properly and looks as expected in all modern browsers. Web Components polyfills are great! It obviously took huge efforts to make these specs work across all modern browsers. The size of polyfill source script is 258Kb. Although, the minified and gzipped version is 38Kb, we can imagine how much logic is hidden behind the scene. It inevitably influences performances. Although authors make the shim better and better putting accent on the performance.

Polymer & X-Tag

Talking about Web Components I should mention Polymer. Polymer is a library built on top of Web Components that simplifies the creation of components and provides plenty of ready-to-use elements. The webcomponents.js polyfill was a part of Polymer and was called platform.js. Later, it was extracted and renamed. Creating Web components with Polymer is way easier. This article by Pankaj Parashar shows how to use Polymer to create Web Components. If you want to deepen the topic, here is a list of articles that might be useful: There is another library that can make working with Web Components simpler, and that is X-Tag. It was developed by Mozilla, and now it’s supported by Microsoft.

Conclusions

Web Components are a huge step forward in the Web development field. They help to simplify the extraction of components, strengthen encapsulation, and make markup more expressive. In this tutorial we’ve seen how to build a production-ready multiselect widget with Web Components. Despite of the lack of browser support, we can use Web Components today thanks to high-quality polyfill webcomponentsjs. Libraries like Polymer and X-Tag offer the chance to create Web components in an easier way. Now please be sure to check out the follow up post: How to Make Accessible Web Components. Have you already used Web Components in your web applications? Feel free to share your experience and thoughts in the section below.

Frequently Asked Questions (FAQs) about Creating a Multiselect Component as a Web Component

How Can I Customize the Appearance of the Multiselect Component?

Customizing the appearance of the multiselect component can be achieved through CSS. You can style the component by targeting the shadow DOM elements. For instance, you can change the background color, border, and font size of the dropdown items. However, it’s important to note that some styles may not be applied due to the encapsulation of the shadow DOM.

Can I Use the Multiselect Component in a Framework like React or Angular?

Yes, you can use the multiselect component in any JavaScript framework that supports custom elements. This includes popular frameworks like React, Angular, and Vue.js. You just need to import the component and use it as a regular HTML element in your JSX or template code.

How Can I Preselect Multiple Options in the Multiselect Component?

Preselecting multiple options can be done by setting the ‘selected’ attribute on the option elements. This attribute accepts an array of values that match the ‘value’ attribute of the option elements you want to preselect.

How Can I Handle the Selection Change Event in the Multiselect Component?

The multiselect component dispatches a ‘change’ event whenever the selection changes. You can listen to this event and handle it in your JavaScript code. The event object contains a ‘detail’ property that holds the current selection.

Can I Disable Some Options in the Multiselect Component?

Yes, you can disable some options by setting the ‘disabled’ attribute on the option elements. Disabled options will be unselectable and usually have a different appearance to indicate their state.

How Can I Add a Placeholder to the Multiselect Component?

You can add a placeholder to the multiselect component by setting the ‘placeholder’ attribute. The placeholder is a short hint that describes the expected value of the component when no option is selected.

Can I Use the Multiselect Component Without a Dropdown?

Yes, you can use the multiselect component without a dropdown by setting the ‘dropdown’ attribute to ‘false’. In this case, the options will be displayed as a list and the user can select multiple options by clicking on them.

How Can I Limit the Number of Selectable Options in the Multiselect Component?

Currently, there is no built-in way to limit the number of selectable options in the multiselect component. However, you can implement this feature by listening to the ‘change’ event and deselecting options when the limit is reached.

Can I Search for Options in the Multiselect Component?

The multiselect component does not support searching for options out of the box. However, you can add this feature by implementing a custom search function and filtering the options based on the search query.

How Can I Sort the Options in the Multiselect Component?

The options in the multiselect component are displayed in the order they are added. If you want to sort the options, you need to sort the array of options before adding them to the component.

Artem TabalinArtem Tabalin
View Author

Artem is a web developer with 8 years experience in back-end and front-end development. He worked at DevExpress creating DevExtreme and currently works at Criteo. He loves learning new web-related things and believes in open source.

multiselect componentpolymerweb componentsX-Tag
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week