Web Components with X-Tag framework

by Javier Puerto
Tags: Open Source , Web , Web Components

The W3C has accepted Custom Elements, HTML templates, HTML imports and Shadow DOM as part of the HTML standard. Some of the proposals will be included in the DOM specifications directly. These are good news for web developers, we can start now to create applications based on web components.

Tools to build something bigger. Building with Web Components. (Foto: pexels.com)

What is a Web Component?

A Web Component is basically a custom element that contains all the functionality and needed style to work, ready to use. We can define our custom elements or inherit from an existing one to add more functionality.

As example, we can create a new element with the x-tag framework like:

xtag.create('be-test', class extends XTagElement {
  constructor (){
    alert("Hello World!");
  }
});

In our web page we just need to use the new element:

<be-test></be-test>

And in the console panel we will be able to see the message Hello World! when the page loads. It does not look very powerful at first look.

For more information about the web-components, visit the webcomponents.org.

Writing our first component.

Be-dog (woof, woof)

We are going to write a simple component that will display random dog pictures on every reload and every click.

For this task we need:

Set up the project.

We are going to create a new directory be-dog. Then init a new git repository and a new npm project.

$> mkdir be-dog
$> cd be-dog
$> git init
$> npm init
$> npm install --save-dev axios x-tag

There is no need to use any build tool. We will create the web component extension in the default index.js location.

Creating the component

The x-tag framework x-tag allows developers to write custom web components using a framework, avoiding incompatibilities between browsers. A polyfill is still required for browsers that does not support all the Web Components specifications. Later, we just need to import the framework before our web components. To start we can write the following code:

class BeDog extends XTagElement {
    constructor() {
        super();
        this._url = '';
        this.refresh();
    }

    '::template'() {
        return `<img src="${this._url}"/>`;
    }

    refresh() {
        axios.get('https://random.dog/woof.json')
            .then(response => {
                this._url = response.data.url;
                this.render();
            }).catch(error => console.log(error));
    }

}

var beDog = xtag.create('be-dog', BeDog);
export default beDog;

Here we defined a private empty attribute _url in the constructor. Then we call our refresh() method that will call the random dog service via axios and will render an image with the source of the retrieved random picture, in case of error, it will be logged in the console.

Now we can create a new directory demo with an index.html file for testing:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>BeDogs demo</title>
    <script src="https://cdn.bootcss.com/x-tag/2.0.9-alpha/x-tag-polyfilled.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="../index.js"></script>
</head>
<body>
<be-dog></be-dog>
</body>
</html>

Now you can open the demo page in your favourite browser. That's it! As simple as it gets.

Adding some logic to the component.

At this point you maybe already discovered a few issues. And yes, you are right, there is room for improvement:

  • The images are sometimes too big to be displayed. We need to add styles.
  • The service can point to videos. With our current implementation we are only able to display images. We want to see our doggos moving, isn't it?
  • We want new dog's pictures and we want them now, without reloading. (Woof!)

Let's start by adding styles: For this task we are going to expose a class attribute in our component. We will discover that it is pretty simple to implement with the x-tag framework.

    '::template'() {
        return `<img class="${this._class}" src="${this._url}"/>`;
    }

    set 'class::attr' (value) {
        this._class = value;
    }

We just added a method class::attr with the suffix ::attr, and the x-tag framework will create a new attribute for us. Then we use the new attribute in our template to display the selected class.

Now in the demo page we can just import the latest bootstrap CSS from a CDN resource and update our code as follows:

<div class="container">
    <be-dog class="img-fluid"></be-dog>
</div>

You can try opening the demo page. All images will be visible without overlapping the viewport. The CSS will adjust it to the available screen size.

Let's focus on videos now. For the task we will use the multiple templates option of the x-tag framework and choose which one will be rendered after the random dog service responds.

    'image::template'() {
        return `<img class="${this._class}" src="${this._url}"/>`;
    }

    'video::template'() {
        return `<video class="${this._class}" autoplay> <source src="${this._url}" type="video/mp4"> </video>`;
    }

    refresh() {
        axios.get('https://random.dog/woof.json')
            .then(response => {
                this._url = response.data.url;
                if (this._url.endsWith('mp4')) {
                    this.render('video');
                } else {
                    this.render('image');
                }
            }).catch(error => console.error(error));
    }

We renamed the previous template by prefixing the name image and create a new template video to handle the videos. Based on the content the server responds with, we choose either video or image as template. You can try it directly reloading the demo page. Now the videos will work and play automatically.

The last but funnier part, reload every time we click the element! And it could not be simpler.

    'click::event'() {
        this.refresh();
    }

Yeah, with the event suffix we can bind methods to standard or custom events. In this case we reload the image on element's click. Go to test it now and see some dog pictures, they are cute.

Be-cat (Meow)

We all love the dogs, they are adorable but there is another pet that is the indisputable leader of Internet... yes, CATS. And we have to create a component for them.

For the task, we are going to copy the be-dog project directory to be-cat directory. Then we just have to replace all references to "dog" by "cat". Then change the service URL and the logic:

    refresh() {
        axios.get('https://aws.random.cat/meow')
            .then(response => {
                this._url = response.data.file;
                if (this._url.endsWith('mp4')) {
                    this.render('video');
                } else {
                    this.render('image');
                }
            }).catch(error => console.error(error));
    }

That's all, cat lovers can now have fun too.

Working with web components

Wait, developing with components should allow us to reuse common code and be-dog and be-cat are very similar. We could abstract the implementation and create a be-media component that will take care of rendering a media element, then we can separate the service to obtain the URL so we can reuse it. Let's do it!

Be-media component

We can again copy a the be-dog or be-cat project and rename the references to "be-media". We are going to update the component to remove the refresh() method and the click event handler. Then as this component must display a media file, we will add a new attribute src to render it.

Here is how the be-media web component code will look.

class BeMedia extends XTagElement {
    constructor() {
        super();
    }

    'image::template'() {
        return `<img class="${this._class}" src="${this._src}"/>`;
    }

    'video::template'() {
        return `<video class="${this._class}" autoplay> <source src="${this._src}" type="video/mp4"> </video>`;
    }

    set 'class::attr' (value) {
        this._class = value;
    }

    set 'src::attr' (value) {
        this._src = value;
        if (this._src.endsWith('mp4')) {
            this.render('video');
        } else {
            this.render('image');
        }
    }

}

var beMedia = xtag.create('be-media', BeMedia);

export default beMedia;

The src attribute method handler will take care of rendering the right template based on the extension of the URL. We can test it in the demo page by providing URLs with video or image.

Now we want to extend this functionality in the be-cat and be-dog projects. For the task we have to include the new be-media project. We can either use the github repository or configure it to a local project.

  "devDependencies": {
    "axios": "^0.18.0",
    "x-tag": "^2.0.9-alpha",
    "be-media": "../be-media"
  }

Then in the code we just have to extend the be-media component and add our functionality.

import BeMedia from 'be-media';

class BeDog extends BeMedia {
    constructor() {
        super();
        this.refresh();
    }

    'click::event'() {
        this.refresh();
    }

    refresh() {
        axios.get('https://random.dog/woof.json')
            .then(response => this.src = response.data.url)
            .catch(error => console.error(error));
    }

}

Pretty simple, we should do the same for the be-cat component. Now our components are decoupled and we are ready to reuse them in other projects.

You can find the repositories for all components developed in this blog post on GitHub.

I hope you enjoyed and learnt something from this tutorial. If not you may at least had fun with all the dogs and cats. See you in the next blog post.