Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Creating a Custom Element Decorator Using TypeScript

DZone's Guide to

Creating a Custom Element Decorator Using TypeScript

In this post, we look at how to use Custom Elements and TypeScript decorators and take a look at some code to explain. Read on to get started!

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

It’s a known fact that I’m betting on Web Components. The last time that I delivered a session about Web Components, someone in the audience asked me how he can remove some of the boilerplate he needs to write in order to create a custom element. I answered that you can probably use a compiler such as Stencil or a library such as Polymer or even write your own TypeScript decorator to do that.

So… a few days ago I had some spare time to sit and play with both Custom Elements and TypeScript decorators. As a result, I wrote a small code snippet that can help you to get started and build your own custom element decorator.

In this post, I’ll share that snippet and explain how to use it. But, first, we will start by introducing decorators.

Let's Decorate Our Code

Decorators are a proposed standard which is still in development. In a nutshell, a decorator is a higher-order function that takes a class, function, property, or parameter as an argument and extends it without modifying its behavior. Decorators are very useful when you want to wrap something with some extra functionality. For example, you can use decorators for validation, instrumentation, logging, or any other cross-cutting concern.

You create a decorator with the @ symbol and an expression which should be a function reference. For example, the following code shows decorator declaration and usage for CustomElement:

const CustomElement = (target) => {
   ... 
}
@CustomElement()
class myClass {
}

Decorator Types

There are four types of decorators:

  • Class - Accepts a constructor function and returns a constructor function. We will use this kind of decorator later on in the example.
  • Function - Accepts 3 arguments which are the object that the function is defined on, the property key, and a property descriptor which gives you access to the property. Function decorators return a property descriptor.
  • Property - The same as a function decorator but it doesn't accept a property descriptor and it shouldn't return anything.
  • Parameter - Accepts three arguments which are:
    • the object on which the function is defined.
    • the function key.
    • the index of the parameter in the function parameter list.

Decorator Factories

If you want a decorator to receive parameters from the outside and that will be generated according to those parameters, you should build a decorator factory. A decorator factory is a function that returns relevant decorators at runtime after it has applied some extra parameters to it. For example, the following code shows a decorator factory:

const CustomElement = (config) => (cls) => {
   // use the config here to affect the class decorator behavior
}

Unfortunately, currently, decorators aren't a part of the JavaScript language and we will have to use a transpiler such as TypeScript when we want to use them.

Note: Be sure to install TypeScript on your machine or in your project before you continue to the main example.

Now that we are a little familiar with decorators, let's start writing our new CustomElement decorator.

Creating a CustomElement Decorator

In custom element scenarios, we will probably want to add a template element and add a shadow DOM to our element. In our decorator, we will accept a template string and a shadow DOM flag which will help us decide whether we should create a shadow root on the element or not.

Note: I'm taking into account that you are familiar with the template element and shadow DOM. If not, there are good explanations for them on MDN.

Let's start with the decorator configuration interface:

interface CustomElementConfig {
    selector:string;
    template: string;
    style?: string;
    useShadow?: boolean;
}

This configuration will be used by the decorator factory to change the behavior of the decorator we will produce. Make sure that selector is the name of the custom element and style will get some CSS styling which will be attached to the template.

We will use a helper function to validate if the provided selector includes at least one dash. The dash check is part of the custom elements specs and each custom element must have at least one dash in its name. Here is the declaration of the validateSelector function:

const validateSelector = (selector: string) => {
    if (selector.indexOf('-') <= 0) {
        throw new Error('You need at least 1 dash in the custom element name!');
    }
};

Now for the decorator implementation:

const CustomElement = (config: CustomElementConfig) => (cls) => {
    validateSelector(config.selector);
    if (!config.template) {
        throw new Error('You need to pass a template for the element');
    }
    const template = document.createElement('template');
    if (config.style) {
        config.template = `<style>${config.style}</style> ${config.template}`;
    }
    template.innerHTML = config.template;

    const connectedCallback = cls.prototype.connectedCallback || function () {};
    cls.prototype.connectedCallback = function() {
        const clone = document.importNode(template.content, true);
        if (config.useShadow) {
            this.attachShadow({mode: 'open'}).appendChild(clone);
        } else {
            this.appendChild(clone);
        }
        connectedCallback.call(this);
    };

    window.customElements.define(config.selector, cls);
};

Let's break up the implementation.

At first, we validate the selector. Then, we create a template element and we attach to it the style we got from the configuration object (if it exists). The main thing happens next. We replace the connectedCallback of the class with a new one which will create the clone from the template and add it to our custom element. If useShadow is true, we make the element a shadow root and attach the cloned template to it. After that, we call the class real connectedCallback that we kept. Last but not least we register the extended class in the custom elements registry.

Using the CustomElement Decorator

The decorator is ready. How can you use it?

Here is a simple usage example:

@CustomElement({
    selector: 'ce-my-name',
    template: `<div>My name is Inigo Montoya</div>
               <div>You killed my father</div>
               <div>Prepare to die!</div>`,
    style: `:host {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      background: #009cff;     
      padding: 16px;         
      border-top: 1px solid black;
      font-size: 24px;
    }`,
    useShadow: true
})
class MyName extends HTMLElement {
    connectedCallback() {
        const elm = document.createElement('h3');
        elm.textContent = 'Boo!';
        this.shadowRoot.appendChild(elm);
    }
}

In this example we send some configurations to the decorator and we also implement some logic in the class connectedCallback function.

We can put the element in our HTML:

<ce-my-name></ce-my-name>

And running the HTML will result in:


Now that you understand how to create your own  CustomElement decorator, you can move on and add more functionality to it.

Summary

In the post, I showed you how you can build your own TypeScript decorator in order to remove some of the custom elements boilerplate code. This technique can also be used in other use cases such as cross-cutting concerns or adding repetitive functionality.

You can find the decorator repository here.

Please share your thoughts about the post in the comments.

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
web dev ,typescript ,element decorator

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}