ANGULAR ENTERPRISE PATTERNS - PT2

This second post in the series will continue over and show how Model Driven Development can improve code reusability

Dec 18th 2018
Angular Enterprise Patterns - Pt2

On the previous post, we covered some of the major concepts and patterns that quickly became standards in our Enterprise Angular applications and introduced a pattern for reusable Components. This pattern consists on having a store providing configuration objects for components based on their Component ID (I strongly recommend you to read part one before proceeding with this post, as it will pick up where the previous one left off).

In this post, I will introduce how Models and Model Driven Development can help improve code reusability, save development time and help to keep a highly maintainable codebase.

When writing software for applications that consist mostly in forms and data grids, we will end up having a lot of similar screens where the content changes possibly per business domain. These business domains might be, for example, split per routes, where '/user/123' represents a user entry and '/cars/search' a list of available cars.

For this purpose, we can define our Models as the data that we want to represent in each of these business domains, where the data can be defined by a specific list of properties. These properties will have constraints such as 'type' or 'label' that will further describe them - Metadata.

Let's start by creating a regular Model to represent the data that will be retrieved from the API for our Cars and Users:

export class Car {
	brand: string;
	model: string;
	horsepower: number;
	cylinders: number;
}

export class User {
	firstName: string;
	lastName: string;
	likesCats: boolean;
}

These Models successfully represent two business domains by listing their properties and their respective types. But what if we want to further describe the properties with labels, icons or other specific types?

Meet Metadata Reflection API, that describes itself as:

Add the ability to augment a class and its members as the class is defined, through a declarative syntax.

Reflect metadata is an ECMAScript proposal that allows us to store a Map in a newly existing [[Metadata]] internal property in objects. This enables various use cases, but in this post, we will be focusing on the ability to further describe our Models with additional constraints.

You can make the package available through npm install reflect-metadata and in your Angular polyfills.ts add the line import 'reflect-metadata'; to make it available application wide.

We will be focusing on the part of the Reflect metadata API that enables us to Add and Get metadata from properties and classes using the following instructions:

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);

// get metadata value of an own metadata key of an object or property
let result = Reflect.getOwnMetadata(metadataKey, target);

I recommend you to read the full API Spec here, as it provides an in-depth explanation of what is happening at a lower level.

Now that know how to attach Metadata to our Model properties, let's create a decorator to further extend our Model properties without modifying them:

// Possible types that we can use to describe a property
export enum PropertyValueType {
	String = 0,
	Number = 1,
	DateTime = 2,
	Select = 3,
	Object = 4,
	Boolean = 5,
	Array = 6,
	Email = 7,
	Password = 8
}

// Strongly typed interface for our property metadata
export interface IPropertyMeta {
	type?: PropertyValueType;
	label?: string;
	editable?: boolean;
	hidden?: boolean;
	icon?: string;
	required?: boolean;
}

// Prefix for our keys in order to avoid writing over the same properties
export const PROPERTY_PREFIX = 'property:';

// String enum to join with the prefix to use on our keys
export enum PropertyMetadataProperties {
	type?: PropertyValueType;
	label?: string;
	hidden?: boolean;
	icon?: string;
}

/**
* Adds property metadata to a model property
*/
export function Property(meta: IPropertyMeta) {
	return (target: object, propertyKey: string) => {
		if (meta !== undefined && meta !== null) {
			Object.keys(meta).forEach(key => {
				Reflect.defineMetadata(`${PROPERTY_PREFIX}${PropertyMetadataProperties[key]}`, meta[key], target, propertyKey);
			});
		}
	};
}

In the example above we created both the type safety elements and a decorator to apply these elements to a property. This is how Metadata will be assigned to properties. The IPropertyMeta will assure that the object that we pass in the meta parameter of the decorator will be applied the correct keys, by using the string enum PropertyMetadataProperties.

We also enumerated a list of the Types that we want to use. Now we can, for example, in a component that uses a model, format the presentation of Boolean types so that it will display a checkmark or a cross instead of it's "true" or "false" value.

Let's now apply our new Property Decorator to our existing Models:

export class Car {
	@Property({
		type: PropertyValueType.String,
		label: 'Brand'
	})
	brand: string;
	  
	@Property({
		type: PropertyValueType.String,
		label: 'Model'
	})
	model: string;
	  
	@Property({
		type: PropertyValueType.Number,
		label: 'Horse Power'
	})
	horsepower: number;
	  
	@Property({
		type: PropertyValueType.Number,
		label: 'Cylinders'
	})
	cylinders: number;
}
export  class  User  {
	@Property({
		type: PropertyValueType.String,
		label: 'First Name'
	})
	firstName: string;
	  
	@Property({
		type: PropertyValueType.String,
		label: 'Last Name'
	})
	lastName: string;
	  
	@Property({
		type: PropertyValueType.Boolean,
		label: 'Likes Cats'
	})
	likesCats: boolean;
}

Now that we have our Models and Decorator done, the next step is to create a function that will allow us to extract its values so that we can use them further down. Since we have multiple Models and our goal is to achieve as much reusability as possible, we will now create a Base Model abstract class that all Models will extend so that all of the related behavior is shared across all existing Models:

 

Here we instantiated the two Models and passed an object with its respective properties and values in the constructor so that we can initialize them. Finally, we called the getPropertyMeta('firstName') to output to the console all the metadata assigned to the firstName property.

All the heavy lifting is being done in the Models new abstract base class BaseModel in baseModel.ts:

export abstract class BaseModel {
	constructor(data: object) {
		Object.assign(this, data);
	}

	/**
	* Get all metadata from a property
	*/
	getPropertyMeta(property: string) {
		return this.getAllMetadata(property, PROPERTY_PREFIX);
	}

	/**
	* Retrieves a map of all the metadata from a property
	*/
	private getAllMetadata(property: string, filterTerm?: string):  object {
		let keys = Reflect.getMetadataKeys(this, property);
		if (filterTerm) {
			keys = keys.filter(key => key.includes(filterTerm));
		}

		const keysObj = {};
		if (Array.isArray(keys) && keys.length > 0) {
			keys.forEach(key => keysObj[key] = Reflect.getMetadata(key, this, property));
		}
		keysObj['property'] = property;
		return keysObj;
	}
}

This class can be further extended to provide very useful functionalities, such as writing specific metadata keys, overriding existing metadata, eject, import metadata and so on. 

This can make our life easier and even improve our component reusability and to do so, we will use Models to improve the Component Registry Pattern that was presented in the previous post. By passing a model in a component configuration, we can use it to adapt its behavior and/or presentation as desired. For example, our Data Grid can now be refactored to generate its columns based on the provided Model. This will allow us to remove the columns configuration from the provided object. 

Since a Model represents a business domain, one page can use the same Model to generate forms, Data Grids and so on. Pages that aggregate different domains may use more than one. You can even develop mixins for when you want to combine multiple Models. This is one of its major advantages, as we are greatly improving the overall code reusability and keeping the business domain code in a single place. It is also important to point out that every time we use a Model on a different context and extend it, either by adding properties or metadata, that change will be reflected application wide. This may cause the Property Metadata interface to grow dramatically. One solution for this problem is creating another Interface and Decorator and splitting their responsibilities, for example, by presentation and behavior. A @Property decorator may add presentation and an @Attribute decorator might add behavior related keys.

Let's now go back to the previous post example and refactor our Data Grid to generate columns dynamically, based on the provided Model:

 

As you can see, the main changes were made in the data-grid component and the component configuration.

In the configuration we, replaced the columns property for the Model, which is instanced passing an object in its constructor that initialises all its properties as null. We need to have the Class properties with value to be able to use Object.getOwnPropertyNames(this).

The data grid component now generates columns based on the Model properties metadata, by iterating through them and creating a new column array with all the needed data for rendering. There is also a new type check in the data grid template, so that we can apply formats to property values, in this case, changing the presentation of Boolean types for cross and checkmark emojis.

In this post we, covered how we can use the Reflect Metadata proposal polyfill to create Models. We also introduced what I consider to be one of the most useful use cases for Models in Angular enterprise applications: further enabling code reusability by providing the Component Registry Components with a business Model from where to extract domain related data. This avoids having it spread across various Component configurations, increasing maintainability and keeping the codebase as agile as possible.

In a next post we, will cover how we can take what we have done so far and create a set of Components and Services to dynamically create forms from provided Models.

Written by Nuno Rocha | Front End Lead at Cleverti

background

Cleverti in the media: 2017 goals and 2016 review in je

Back to News.. Next Article