Custom elements – NextGen Markup pt.3

Not sure what this is about? Have you read the first and second part?

After some delay (won’t get into that now) I get to write something new. After spending 2 days in the 2013 Fronteers Conference, I finally got the time and energy to write something about the hottest part of Web Components: custom elements.

As I was starting to think exactly how to structure this blog post and all the things I should write for an adequate presentation of the custom elements, I realized that nothing I wrote could possibly come even close to the awesome guide to custom elements, written by Eric Bidelman. So instead of writing a tutorial on how custom elements work, lets try creating an element and seeing it in action.

Polyfils

Obviously the support for custom elements today is still very low, if any. However there are 2 awesome projects right now that act as really good polyfils.

polymer.js

First of all, Google’s own polymer project. This is way more than a simple polyfil. It brings to today’s browsers all the web components and even more: custom elements, templates, shadow dom, imports. And on top of that, it adds a lot of bells and whistles, including data binding, pointer events, specific elements and more. Really, this is way more than a polyfil, it’s a whole suite of features.

x-tags

On the other side, it’s Mozilla’s own polyfil, called x-tags. This is only about custom elements, and this is what we’ll use for this tutorial. Also, a project called Brick exists for hosting a lot of elements created with this lib.

Let’s start

So, for the purpose of this tutorial, lets build an autocomplete box. First, the requirements:

  • The autocomplete preloads all the data from a source on load and then searches through them on the client side
  • There is a minimum number of characters required for the search to happen
  • There is a small waiting period from the moment the user presses a button, till the moment the search happens
  • Only dependency should be x-tags library (yes, there can still be projects without jQuery)

Of course we are not going to create a production ready autocomplete box, but these requirements are enough to demonstrate the x-tag library.

First off, lets build a very simple page and lets just include the x-tag library and our own scripts and styles file. Oh, and lets create our new custom element.

<html>
<head>
    <title>Custom Elements Experiment</title>
    <link rel="stylesheet" href="styles.css">
    <link rel="stylesheet" href="x-tag/x-tag-components.css">
</head>
<body>

    <input is="x-autocomplete"/>

<script src="x-tag/x-tag-components.js"></script>
<script src="scripts.js"></script>
</body>
</html>

See our input element? By using the attribute is we are telling the browser (well, the x-tag library actually) that this input element is in reality a custom element named x-autocomplete.

But how does the library know how to handle this element? Well, we just need to define it with js (the term in this case is register) and make sure that it extends from the input.

xtag.register('x-autocomplete', {
    extends: 'input',
    lifecycle: {
        created: function(){
            console.log(this.__proto__);
        }
    }
});

The lifecycle.created is basically the equivalent to an onload event. It is triggered when the element is created. The rest of the lifecycle callbacks are: inserted, removed, attributeChanged.

If we check our console for that log, we will see that the element really extends the HTMLInputElement. If we had omitted the extends property, then it would have simply been HTMLElement.

Getting the data

Included with the demo, there is a very simple json file, containing the names of 10 programming languages. We will use that, as our data source. Now, lets try and load the data with an ajax call, when our custom element is created.

Since we are not using any other libraries, lets just create a very simple function to make that ajax call and get the json.

function ajax(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
     xhr.onreadystatechange = function () {
        if (xhr.readyState < 4 || xhr.status !== 200) {
            return;
        } else if (xhr.readyState === 4) {
            callback(xhr.responseText);
        }
     };
    xhr.send('');

    return xhr;
}

Now, we need somehow to tell the element, where to get the data from. No better place for this, than adding it as a parameter, on the element itself.

<input is="x-autocomplete" source="data.json" />

Looks nice, customizable, reusable, it’s perfect! And now, we actually need to instruct our element to fetch those data when it is created. When we need a custom method for our element, we can put it under the methods property. Lets create a function called loadData

xtag.register('x-autocomplete', {
    extends: 'input',
    lifecycle: {
        created: function(){
            this.loadData();
        }
    },
    methods: {
        loadData: function () {
            var self = this;
            ajax(this.getAttribute("source"), function (response) {
                console.log(response);
            });
        }
    }
});

Refreshing the page, will actually log the json data! Notice how we get the attribute value for the source, using this.getAttribute(“source”)? Remember how we said the element is an HTMLInputElement? This means we can use all functions we already know, like for example getAttribute.

But only getting the data is not enough. We need to store it somewhere along the element, so that we can access them at any time. Luckily, we have a mechanism for getters and setters on the element, through a property called accessors. Lets see how it looks like.

xtag.register('x-autocomplete', {
    extends: 'input',
    lifecycle: {
        created: function(){
            this.loadData();
        }
    },
    accessors: {
        languages : {
            get : function () {
                return this.jsonData;
            },
            set : function (json) {
                this.jsonData = json;
            }
        }
    },
    methods: {
        loadData: function () {
            var self = this;
            ajax(this.getAttribute("source"), function (response) {
                self.languages = JSON.parse(response);
            });
        }
    }
});

This way, whenever we use something like this.languages we have immediate access to our data. (ok, I’ll admit it… this would work without defining accessors, so I am going a bit too far there. But I wanted to demonstrate this functionality too).

Searching

Ok, now that we have the data, we need to be able to search them. For tutorial purposes, we will use a very complicated algorithm that goes like this: iterate through the array and if it matches, return the result. Lets create a search method on our element, that takes a string as a parameter:

    methods: {
        loadData: function () {
            var self = this;
            ajax(this.getAttribute("source"), function (response) {
                self.languages = JSON.parse(response);
            });
        },
        search: function (str) {
            var i, result = [];
            for (i=0;i<this.languages.length;i++) {
                if (this.languages[i].name.indexOf(str) > -1) {
                    result.push(this.languages[i]);
                }
            }
            return result;
        }
    }

All well and easy up to now, but we need a way to call this function. Lucky for us, we have full control over events, by using the events property. So we can write something simple for now, like this:

    events: {
        'keyup' : function () {
            console.log(this.search(this.value));
        }
    }

More attributes

We talked about 2 more requirements earlier: having a minimum number of characters for the search, and having a small delay while we wait for the user to type his input. Both of these will be customizable through attributes on the element. Lets see how that would look like:

    <input is="x-autocomplete"
        source="data.json"
        minlength="2"
        waitforuser="200"
    />

minlength is really easy to handle. Lets just call the search function, only if the textbox value length is at least equal to our minimum length

    events: {
        'keyup' : function () {
            if (this.value.length < this.getAttribute("minlength")) {
                return;
            }
            console.log(this.search(this.value));
        }
    }

For the waitforuser it is a bit more complicated. We will have a timeout to actually do the search, but the timeout will be resetted if the user presses a key within the waitforuser limit (in ms). Still following me? Good! The code would then look something like this:

    events: {
        'keyup' : function () {
            var self = this;
            if (this.value.length < this.getAttribute("minlength")) {
                return;
            }
            clearTimeout(this.timeout);
            this.timeout = setTimeout(function () {
                console.log(self.search(self.value));
            }, this.getAttribute('waitforuser'))
        }
    }

See what I did there? I added another property on the element, without an accessor. Yeap, still works. So now, the user starts typing, and only when he pauses for waitforuser amount of milliseconds, then the search will happen.

Rendering the results

Ok, we have our textbox, we have our data, search is working… one thing left: show the result to the user. This involves some DOM manipulation and for the purpose of this tutorial, we will just do something quick and dirty. The focus point is elsewhere, anyway.

First, we need a function that creates the list element and puts it near the x-autocomplete (for ease of development, we put it right before). And second, we need a function that takes an array of items and populates that list based on that. These two functions will look like this:

        createList: function () {
            var list = document.createElement('ul');
            this.hideList();
            var rect = this.getBoundingClientRect();
            list.style.top = (rect.top+25)+"px";
            list.style.left = rect.left+"px";
            this.parentNode.insertBefore(list, this);
        },
        repopulateList: function (items) {
            var li, i, list = this.previousSibling;

            this.emptyList();

            for (i=0;i<items.length;i++) {
                li = document.createElement('li');
                li.innerHTML = items[i].name;
                li.setAttribute('data-id', items[i].id);
                list.appendChild(li);
            }

            this.showList();
        },

This is basic javascript (and actually not very well written) just to make the list work. It doesn’t have anything to do with the custom elements, so we won’t get into depth. However we see a few more helper functions appearing in there, just to tidy things up a bit:

        emptyList: function () {
            var list = this.previousSibling;
            while (list.hasChildNodes()) {
                list.removeChild(list.lastChild);
            }
        },
        showList: function () {
            this.previousSibling.className = 'x-autocomplete-list';
        },
        hideList: function () {
            this.previousSibling.className = 'x-autocomplete-list hidden';
        }

And finally, we will just add a click event on all the list items, so that the textbox is filled with the correct value, when an item is clicked

        createList: function () {
            var self = this;
            var list = document.createElement('ul');
            this.hideList();
            var rect = this.getBoundingClientRect();
            list.style.top = (rect.top+25)+"px";
            list.style.left = rect.left+"px";
            this.parentNode.insertBefore(list, this);

            xtag.addEvent(list, 'click:delegate(li)', function () {
                self.value = this.innerHTML;
                self.hideList();
            });
        },

Notice the xtag.addEvent? x-tag library adds a small number of tools and helpers to facilitate development. These include:

  • Event binding and delegation
  • DOM selectors and traversal
  • Helpers like object to array transformation and requestAnimationFrame

You can check the full list here.

That’s it!

It wasn’t that hard, was it? But what about support? Well, based on the x-tag documentation, all modern browsers are supported. IE9+ is also supported. So basically you can start using this today, if you want.

You can pick up the source code from Github repo. There is also a small css file to make sure the list appears correctly which you can also find in Github.

You can see a live demo of the code here.

3 Responses to Custom elements – NextGen Markup pt.3
  1. Erik Isaksen Reply

    Thanks for the tutorials! Well done. I am building a Polymer app now and I am very interested in x-tags and Brick. This is is super helpful. My understanding of Polymer and x-tags frameworks looking at the source code are that they use the same polyfill files. Is this no longer the case?

    • Nikos Reply

      To be honest, I hadn’t noticed that. Taking a very quick look at the source code, it seems you are right. For example in this file in github, there are comments mentioning that pieces of code were taken from the Polymer project.

  2. [...] practice, you often cannot fit the same element everywhere. Imagine the autocomplete built in the previo... prevent-default.com/responsive-x-tags

Leave a Reply

Your email address will not be published. Please enter your name, email and a comment.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>