In the previous post about TypeScript decorators, I used decorators to quickly add validation rules. In this post, we'll explore more decorator features. TypeScript can automatically add each property's type to the metadata. Let's see how we can use this information along with custom attributes to automatically generate a form from a class.
The goal is 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 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 decorate the properties with the required, displayName, and design types. Here is an excerpt 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, use the metadata to generate the form. Find each editable property, read its display name to build the label, and use its type to choose the correct input element. For instance, use <input type="number"/> for a number property, and <input type="checkbox"/> for a boolean property. Then, bind each input to the model so that UI changes propagate back. The input event handles this well.
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. They let you enrich code with additional information and retrieve it at runtime. In the previous post, I created a generic validation system. Today, I wrote a generic form generator in just a few lines of code. There are lots of possibilities! If you are familiar with reflection in C#, you probably already have hundreds of ideas 😃
Do you have a question or a suggestion about this post? Contact me!