How to build a multi-select widget using CanJs

In this post we’re going to design and implement a multi-select widget using CanJs. We’re also going to use StealJs as the dependency management tool.

We begin by downloading the latest version of the JavascriptMVC framework and then creating a ‘multi’ folder with the following 5 files:

  • multi/multi.html
  • multi/multi.js
  • multi/views/multi.ejs
  • multi/views/dropdown.ejs
  • multi/multi.less

A snapshot of the folder organization:

Then we break down the multi-select widget into two complementary behaviours:

  • The user can open and close the multi-select widget
  • The user can select multiple options
The user can open and close the multi-select widget

To implement this behaviour we will create an observable named ‘clientState’ that will contain an ‘open’ attribute that will be set to true/false wether the user clicks the input box, the dropdown or anywhere else on the browser’s window.

The code follows bellow:

multi/multi.html
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <title>Multi</title>
    </head>
    <body>
        <div class="multi">MULTI PLACEHOLDER</div>    
        <script src="../steal/steal.js?multi/multi.js" type="text/javascript" charset="utf-8"></script>
    </body>
</html>
multi/multi.js
steal('can', './views/multi.ejs', './views/dropdown.ejs', './multi.less',
        'can/construct/super',  function(can, MultiEJS, DropdownEJS) {

            var Multi = can.Control({

                setup: function(el, options) {

                    options.clientState = new can.Model({
                        open: false
                    });

                    this._super(el, options);
                },

                init: function(el, options) {

                    this.element.html(MultiEJS({}));

                    this.element.find('.dropdown').html( DropdownEJS({}));

                    this.element.find('.dropdown').hide();
                },

                '.box click': function(el, ev) {

                    this.options.clientState.attr('open', true);
                },

                '{window} click': function(el, ev) {

                    if(!this.element.has(ev.target).length)
                        this.options.clientState.attr('open', false);
                },

                '{clientState} open': function(clientState, ev, newValue, oldValue) {

                    if(newValue)
                        this.element.find('.dropdown').show();
                    else
                        this.element.find('.dropdown').hide();
                }
    });

    new Multi('.multi', {});
});
multi/views/multi.ejs
    <input class="box">
    <ul class="dropdown"></ul>
multi/views/dropdown.ejs
    <li class="item">Alex</li>
    <li class="item">Serge</li>
    <li class="item">John</li>
    <li class="item">Mike</li>
    <li class="item">Mary</li>
multi/multi.less
.select {
        width: 220px;
       .box {
            width: 208px;
            margin: 0;
            padding: 5px;
            border: 1px solid #333;
       }
       ul.dropdown {
            margin: 0;
            padding: 0;
            list-style: none;
            border-left: 1px solid #ccc;
            border-right: 1px solid #ccc;
            li.item {
                padding: 5px;
                border-bottom: 1px solid #ccc;
                cursor: pointer;
                .check { 
                    display: inline-block;
                }
            }
       }
}

And this is how the multi-select widget looks like so far:

The user can select multiple options

We’ll keep track of the selected options in a ‘clientState’ attribute named ‘selectedOptions’. We’ll also add/remove a ‘selected’ class to the dom element representing an option in order to show the user that item has been selected.

multi/multi.js
steal('can', './views/multi.ejs', './views/dropdown.ejs', './multi.less',
        'can/construct/super',  function(can, MultiEJS, DropdownEJS) {

        var Multi = can.Control({

            setup: function(el, options) {

                options.clientState = new can.Model({
                    open: false,
                    selectedOptions: []
                });

                this._super(el, options);
            },

            init: function(el, options) {

                this.element.html(MultiEJS({}));

                this.element.find('.dropdown').html(
                    DropdownEJS({
                        initialOptions: options.data || []
                }));

                this.element.find('.dropdown').hide();
            },

            ...

            '.dropdown .item click': function(el, ev) {

                var item = el.data('item'),
                so = this.options.clientState.selectedOptions,
                index = -1;

                if(el.hasClass('selected')) {

                    index = so.indexOf(item);

                    so.splice(index, 1);

                    el.removeClass('selected');

                } else {

                    so.push(item);

                    el.addClass('selected');
                }

                this.element.trigger('change', [so]);
            }
});
multi/views/dropdown.ejs
<% list(initialOptions, function(io) { %>
    <li class="item" <%= (el) -> el.data('item', io) %>><div class="check"> </div><%= io.text %></li>
<% }); %>
multi/multi.less
.multi {
    width: 220px;
   .box {
        width: 208px;
        margin: 0;
        padding: 5px;
        border: 1px solid #333;
       }
       ul.dropdown {
            margin: 0;
            padding: 0;
            list-style: none;
            border-left: 1px solid #ccc;
            border-right: 1px solid #ccc;
            li.item {
                padding: 5px;
                border-bottom: 1px solid #ccc;
                cursor: pointer;
                .check {
                    display: inline-block;
                }
                &.selected {
                    .check {
                        width: 35px;
                        margin-left: 10px;
                        background: url(../images/checked.gif) no-repeat;
                    }
                }
             }
       }
}

And here’s how the multi-select looks like after these changes:

Final thoughts

In this article I tried to describe the process of building a multi-select widget using CanJs as the MVC framework and StealJs as the dependency management tool.

One of the reasons I decided to write a blog post about building such a common used widget was the fact that is difficult to find quality blog posts on the web that describe widget building step by step.

I hope this article was somehow helpful to developers trying to build similar widgets.

Thank you for reading this post.

comments powered by Disqus