Generate an HTML form from an object in TypeScript

 
 
  • Gérald Barré

In the previous post about TypeScript decorators, I used decorators to quickly add validation rules. In this post, we'll use other 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:

TypeScript
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 from the Person classGenerated form from the Person class

Let's configure TypeScript to enable decorators and metadata.

  • experimentalDecorators allows using 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 contain the 2 attributes:

JSON
{
  "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:

JavaScript
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.

TypeScript
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 propagated to the model. The input event should be ok with that.

TypeScript
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:

TypeScript
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 this 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 😃

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub