TypeScript patterns: Controller

by dotnetnerd 7. August 2017 11:45

The next pattern I want to take a look at, that is fundamental to how many JavaScript applications are structured, is the Controller pattern.

There are different definitions for what a Controller is on the client, depending on what framework people are used to working with. As you may know I often recommend not using a framework, but I go with the definition that a controller encapsulates the UI logic for a part of a web page. A common practice is to have controllers take the html element that it works on as the first constructor argument, followed by dependencies (services and other controllers) and possibly an options object. This will resemble what you are likely doing if you are writing eg. an MVC application in ASP.NET MVC or WebAPI. In much the same way, this allows you to pass in dependencies, and keep the controller free of hardcoded dependencies.

Another thing that is common to define a practice for is the methods that are needed to define the lifecycle. Two approaches that I have often seen is to have render and addEventHandlers methods that are either called from the constructor, or that are build as a fluent interface by having the methods return this, so the consumer controls the order things are done, and is able to use them as a fluent interface.

A basic and somewhat naive example of this pattern could look like the code below.

module Controller {
   
    export class ProductOptions {
        template: string;

        constructor(template: string) {
            this.template = template;
        }
    }

    export class ProductController {
        element: HTMLElement;
        options: ProductOptions;
       
        constructor(element: HTMLElement, options: ProductOptions) {
             this.element = element;
            this.options = options;

            this.render();
            this.addEventHandlers();
        }

        private render() {
            this.element.innerHTML = this.options.template;
        }

        private addEventHandlers() {
            this.element.querySelector('input.addProductButton').addEventListener('click', () => {
                 this.addToBasket((<HTMLInputElement>this.element.querySelector('.productName')).innerHTML);
            });
        }

        addToBasket(val) {
            if (val) {
                (<HTMLUListElement>this.element.querySelector('.productList')).innerHTML += '<li>' + val + '</li>';
            }
        }
    }
}

From here this controller can be instantiated in out application root.

var product = new Controller.ProductController(document.getElementById('controllerElement'), new Controller.ProductOptions('<div><div class="productName">Bike</div><input type="button" class="addProductButton"><div class="productList"></div>'));

With something like this, we have a simple way of composing our application, and we can go really big, while keeping the codebase simple. The obvious place where you probably want to use a library is for rendering the template, so you get a proper templating language, and move the template to its own file. There are plenty of nice libraries out there, and we are free to choose, or even replace these, because by owning our own architecture, nothing is pushed on us.

Lastly we can tie this in with the Lazy pattern, that a wrote about previously, for encapsulating access to the sub elements. This could allow us to ensure elements are only queried once, and keeps the query selectors from being spread across our methods.

Who am I?

My name is Christian Holm Diget, and I work as an independent consultant, in Denmark, where I write code, give advice on architecture and help with training. On the side I get to do a bit of speaking and help with miscellaneous community events.

Some of my primary focus areas are code quality, programming languages and using new technologies to provide value.

Microsoft Certified Professional Developer

Microsoft Most Valuable Professional

Month List