Write your own DOM element factory for TypeScript

The DOM api allows to manipulate the html document in a browser. It's simple to use, but the code is not very readable. Here's the code needed to create 2 elements and set an attribute:

function foo() {
    let div = document.createElement("div");
    let anchor = document.createElement("a");
    anchor.href = "https://www.meziantou.net";
    div.appendChild(anchor);
    return div;
}

The JSX syntax introduced by React allows to mix JavaScript and HTML-ish. The goal is the create a code that is easy to write and to read. JSX will be compiled by a preprocessor such as Babel (or TypeScript as we'll see later) to a valid JavaScript code. Using JSX, the previous function can be written as:

function foo() {
    return (
        <div>
            <a href="https://www.meziantou.net">meziantou</a>
        </div>
    );
}

If you use React, the code will be converted to:

function foo() {
    return React.createElement("div", { "class": className },
        React.createElement("a", { href: "https://www.meziantou.net" }, "meziantou"));
}

React.createElement creates a virtual DOM that will be translated to real DOM at the end. The idea of the virtual dom is to reduce the number of DOM operations, but it's not very important for this post… For simplicity, just imagine React.createElement is similar to document.createElement.

TypeScript supports the JSX syntax, so you can take advantages of the type checker, IDE autocompletion, and refactoring. TypeScript can preserve the JSX syntax (in case you want to use another precompiler), replace it with React.createElement, or using a custom factory. The last option allows you to use JSX without using React, as long as you provide your own factory.

A factory is just a function with the following declaration:

 interface AttributeCollection {
    [name: string]: string | boolean;
}

var Fragment;

function createElement(tagName: string, attributes: AttributeCollection | null, ...children: any[]): any

So, it's not very complicated to implement this function. You just need to use document.createElement, set the attributes, and add the children. However, there are some point of attentions.

  • JSX does not allow the attribute class. Instead, you have to use className. So, the factory has to handle this case.
  • Using JSX, you can register an event handler using <a onclick={...}></a>. However, the setAttribute function only accept a string value. So, you have to handle this case by using addEventListener.
  • Fragments (<>...</>) are replaced by factory.createElement(factory.Fragment, null, ...). So, we can use a special name to create a DocumentFragment in the createElement function.
namespace MyFactory {
    const Fragment = "<></>";

    export function createElement(tagName: string, attributes: JSX.AttributeCollection | null, ...children: any[]): Element | DocumentFragment {
        if (tagName === Fragment) {
            return document.createDocumentFragment();
        }

        const element = document.createElement(tagName);
        if (attributes) {
            for (const key of Object.keys(attributes)) {
                const attributeValue = attributes[key];

                if (key === "className") { // JSX does not allow class as a valid name
                    element.setAttribute("class", attributeValue);
                } else if (key.startsWith("on") && typeof attributes[key] === "function") {
                    element.addEventListener(key.substring(2), attributeValue);
                } else {
                    // <input disable />      { disable: true }
                    // <input type="text" />  { type: "text"}
                    if (typeof attributeValue === "boolean" && attributeValue) {
                        element.setAttribute(key, "");
                    } else {
                        element.setAttribute(key, attributeValue);
                    }
                }
            }
        }

        for (const child of children) {
            appendChild(element, child);
        }

        return element;
    }

    function appendChild(parent: Node, child: any) {
        if (typeof child === "undefined" || child === null) {
            return;
        }

        if (Array.isArray(child)) {
            for (const value of child) {
                appendChild(parent, value);
            }
        } else if (typeof child === "string") {
            parent.appendChild(document.createTextNode(child));
        } else if (child instanceof Node) {
            parent.appendChild(child);
        } else if (typeof child === "boolean") {
            // <>{condition && <a>Display when condition is true</a>}</>
            // if condition is false, the child is a boolean, but we don't want to display anything
        } else {
            parent.appendChild(document.createTextNode(String(child)));
        }
    }
}

Finally, you need to change the tsconfig.json file to indicate the TypeScript compiler how to convert JSX:

{
    "compilerOptions": {
        "jsx": "react", // use the React mode, so call the factory
        "jsxFactory": "MyFactory.createElement" // The name of the factory function
    }
}

You can now create a file with the extension .tsx, and use the JSX syntax. Hope this helps you writting DOM code!

If you want to see a real usage of a custom factory, you can check my Password Manager project on GitHub.

Which version of EcmaScript should I use in the TypeScript configuration

TypeScript allows to convert most of the ES next features to ES3, ES5, ES6, ES2016, ES2017. Of course, you can also target ES Next. But which version should you target?

Why you should use the highest possible version?

Using the highest version allows allows you to write shorter code, and use more readable features, such as async/await, for..of, spread, etc. It's not only shorter, but also easier to debug. For instance, async/await is rewritten by TypeScript to a state machine. So, the call stack is harder to understand, steping into the next statement is not easy because you have to step into the step machine functions. The source map can sometimes helps, but it doesn't solve all issues. You can also blackbox some scripts. For instance, you can blackbox tslib if you import TypeScript helpers.

So, if you can target ES Next, do it! But unfortunately this is not always possible. Let's see how to choose the right version!

Select your target JS runtime

First, you must know which runtime you want to support. Do you need to run you application in a web browser and which one, in NodeJS or in Electron. Depending of this choice, you know the JS flavor you can use. For instance, if you choose Electron, you know it uses Chromium version XXX so you know which functionalities are available. In you use NodeJS, it also use V8, the JS engine of Chromium. So, it easy to know which features are supported. For web browser, it's a little more complicated. You may want to support multiple browsers, and multiple versions of each browser.

You can check which features are supported by the web browsers and js runtime here: https://kangax.github.io/compat-table/es6. Tip: you can change the ES version on the top.

For instance, if you want to target IE11, you'll have to target ES5. If you want to support NodeJS, Edge or Chromium, ES6 is ok.

Once you know which version you want to use, update the tsconfig.json file to reflect your decision:

{
    "compilerOptions": {
        "target": "ES2016" // "ES3" (default), "ES5", "ES6"/"ES2015", "ES2016", "ES2017" or "ESNext".
    }
}

Which libraries to target?

Changing the target version also change the available libraries. For instance, if you target ES5, you cannot use Promise. But Promise is not a feature that must be implemented by the engine. You can use another library to replace them, such as bluebird. This means you can target ES5 and use Promise as long as you add them using an external library. It's the same for Array.include, and lots of functions.

TypeScript allows to specify which libraries are available. You can select them in the configuration file tsconfig.json:

{
    "compilerOptions": {
        "lib": [
            "ES5",
            "ES2015.Promise",
            "ES2016.Array.Include"
        ],
    }
}

You can find the list of available libraries in the TypeScript documentation.

BTW, you can read my previous post to dynamically import polyfills

Development vs Release configurations?

As I said in the introduction, using a higher version of EcmaScript may help debugging your application. So, it may not be a bad idea to have 2 configurations, one for the development and another one for the release. For instance, the first one can target ES next because you are debugging on a recent browser, while the second one can target ES5 because your customers may use an older browser.

TypeScript supports the configuration inheritance. So, you can create common tsconfig.json that contains all the settings, and a tsconfig.dev.json that inherits from tsconfig.json. You can build using tsc tsconfig.dev.json. You can read the documentation about configuration inheritance for more information.

Babel

If you are using webpack, gulp or any build tool, you may consider the Babel option. The idea is to configure TypeScript to target ES Next, and transpile to another version using Babel. Based on the previous link, Babel allows transpiling more things to a lower ES version than TypeScript. Using webpack, you can also automatically include polyfills with a plugin such as webpack-polyfill-injector.

Conclusion

Choose the configuration that make you the most productive and which will run on your target browsers / runtimes.

Get the name of a TypeScript class at runtime

in .NET, it's easy to get the class name of an object using obj.GetType().Name. In JavaScript, this doesn't work: typeof obj return "object" or something else, but never the name of the class. This doesn't mean you cannot get the name of a class in JS.

In ES6, you can use Function.name to get the name of a function (documentation).

function test() { }
console.log(test.name); // print "test"

Well, in JavaScript, a class is a function! So, you can get its name using the name property:

class Sample { }
console.log(Sample.name); // print "Sample"

For an instance of a class, you can use the constructor property to get the constructor function: obj.constructor. This way you can get the name of the class by getting the name of the constructor function:

const obj = new Sample();
console.log(obj.constructor.name); // print "Sample"

Note 1: If you minify your scripts, some functions/classes may be renamed. So, the name of the class won't be the original name (I mean the name from your original source file), but the name after minification. UglifyJS has an option to not mangle some names uglifyjs ... -m reserved=['$', 'Sample']

Note 2: This doesn't work if the class contains a method named name. In this case, the browser won't automatically add the name property, so you won't be able to get the name of the class. You can read the specification about this behavior.

Note 3: TypeScript doesn't show constructor in the auto-completion. However, this is totally supported and typed.

Generate an HTML form from an object in TypeScript

In the previous post about TypeScript decorators, I used decorators to quickly add validation rules. In this post, we'll use another features of the decorators. TypeScript can automatically add the type of the property to the metadata. Let's see how we can use this information and other custom attributes to automatically generate a form from a class.

The idea is to be able to use the following code:

class Person {
    @editable()
    @displayName("First Name")
    public firstName: string;
    @editable()
    @displayName("Last Name")
    public lastName: string;
    @editable()
    public dateOfBirth: Date;
    @editable()
    public size: number;
}

var author = new Person();
author.firstName = 'Gérald';
generateForm(document.body, author);

Generated form

Let's configure TypeScript to enable decorators and metadata.

  • experimentalDecorators allows to use the decorators in your code.
  • emitDecoratorMetadata instructs the compiler to add a metadata design:type for each property with a decorator.

The project.json should contains the 2 attributes:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
  }
}

The compiler will translate the TypeScript class to JavaScript, and decorates the properties with the required, displayName and design types. Here's an extract of the generated code:

Person = /** @class */ (function () {
    function Person() {
    }
    __decorate([
        editable(),
        displayName("First Name"),
        __metadata("design:type", String) // Added by emitDecoratorMetadata: true
    ], Person.prototype, "firstName", void 0);
    // ...
    return Person;
}());

Let's declare the editable and displayName decorators. You can look at the previous post to get a better understanding of TypeScript decorators.

function editable(target: any, propertyKey: string) {
    let properties: string[] = Reflect.getMetadata("editableProperties", target) || [];
    if (properties.indexOf(propertyKey) < 0) {
        properties.push(propertyKey);
    }

    Reflect.defineMetadata("editableProperties", properties, target);
}

function displayName(name: string) {
    return function (target: any, propertyKey: string) {
        Reflect.defineMetadata("displayName", name, target);
    }
}

Now, you can use the metadata to generate the form. You have to find the editable properties, get their display name to create the label, and their type to create the right input type. For instance, you have to use <input type="number"/> for a property of type number, and <input type="checkbox"/> for a property of type boolean. Then, you have to bind the inputs to the model, so changes in the UI are propragated to the model. The input event should be ok for that.

function generateForm(parentElement: HTMLElement, obj: any) {
    const form = document.createElement("form");

    let properties: string[] = Reflect.getMetadata("editableProperties", obj) || [];
    for (let property of properties) {
        const dataType = Reflect.getMetadata("design:type", obj, property) || property;
        const displayName = Reflect.getMetadata("displayName", obj, property) || property;

        // create the label
        const label = document.createElement("label");
        label.textContent = displayName;
        label.htmlFor = property;
        form.appendChild(label);

        // Create the input
        const input = document.createElement("input");
        input.id = property;
        if (dataType === String) {
            input.type = "text";
            input.addEventListener("input", e => obj[property] = input.value);
        } else if (dataType === Date) {
            input.type = "date";
            input.addEventListener("input", e => obj[property] = input.valueAsDate);
        } else if (dataType === Number) {
            input.type = "number";
            input.addEventListener("input", e => obj[property] = input.valueAsNumber);
        } else if (dataType === Boolean) {
            input.type = "checkbox";
            input.addEventListener("input", e => obj[property] = input.checked);
        }

        form.appendChild(input);
    }

    parentElement.appendChild(form);
}

You can now use the code at the beginning of the post, and it should work:

var author = new Person();
author.firstName = 'Gérald';
generateForm(document.body, author);

Conclusion

Decorators and metadata are very similar to what C# provides out of the box with attributes. It allows you to enrich the code with additional information, and get those information at runtime. In the previous post, I created a generic validation system. Today, I write a generic form generator in a few lines of code. They are lots of possibilities! If you are familiar with reflection in C#, I think you already have hundreds of ideas 😃

Validation made easy with decorators

Before reading this post, you should read the previous post about Aspect Oriented Programming in TypeScript. It will help you understand the TypeScript decorators.

The idea of this post is to write a class and use decorators to set the validation rules on attributes:

class Customer {
    @required
    public firstName: string;
    @required
    public lastName: string;
}

var customer = new Customer();
customer.firstName = 'Gérald';
validate(customer); // 'lastName' is required

The solution is to use decorators and metadata to attach the validation rules to the associate properties. Then, you can query these metadata to run the rules. TypeScript supports the Metadata Reflection API. This API defines a few methods. Here's are the ones we'll use:

namespace Reflect {
    // Set metadata

    // Reflect.defineMetadata("custom:annotation", value, Customer);
    function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
    // Reflect.defineMetadata("custom:annotation", value, Customer.prototype, "firstName");
    function defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol): void;

    // Get metadata

    // Reflect.getMetadata("custom:annotation", Customer);
    function getMetadata(metadataKey: any, target: Object): any;
    // Reflect.getMetadata("custom:annotation", Customer.prototype, "firstName");
    function getMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any;
}

The following code attach a metadata named "validation" to the property and the class. The metadata for the property is a list of ValidationRule. For the class we attach the list of properties to validate.

First, you need to install the metadata polyfill:

npm install reflect-metadata

Then, let's create a function to add the metadata to the property and class:

import "reflect-metadata";

function addValidationRule(target: any, propertyKey: string, rule: IValidationRule) {
    let rules: IValidationRule[] = Reflect.getMetadata("validation", target, propertyKey) || [];
    rules.push(rule);

    let properties: string[] = Reflect.getMetadata("validation", target) || [];
    if (properties.indexOf(propertyKey) < 0) {
        properties.push(propertyKey);
    }

    Reflect.defineMetadata("validation", properties, target);
    Reflect.defineMetadata("validation", rules, target, propertyKey);
}

Let's implement the required validation rule:

interface IValidationRule {
    evaluate(target: any, value: any, key: string): string | null;
}

class RequiredValidationRule implements IValidationRule {
    static instance = new RequiredValidationRule();

    evaluate(target: any, value: any, key: string): string | null {
        if (value) {
            return null;
        }

        return `${key} is required`;
    }
}

function required(target: any, propertyKey: string) {
    addValidationRule(target, propertyKey, RequiredValidationRule.instance);
}

Finally, you can validate an object using the metadata:

function validate(target: any) {
    // Get the list of properties to validate
    const keys = Reflect.getMetadata("validation", target) as string[];
    let errorMessages: string[] = [];
    if (Array.isArray(keys)) {
        for (const key of keys) {
            const rules = Reflect.getMetadata("validation", target, key) as IValidationRule[];
            if (!Array.isArray(rules)) {
                continue;
            }

            for (const rule of rules) {
                const error = rule.evaluate(target, target[key], key);
                if (error) {
                    errorMessages.push(error);
                }
            }
        }
    }

    return errorMessages;
}

function isValid(target: any) {
    const validationResult = validate(target);
    return validationResult.length === 0;
}

Conclusion

In the previous post, I showed how to use decorators to change the default behavior of a method or class. Metadata allows to use decorators for new scenario. If you look at it, it really similar to C#.