Skip to content

Model View Controller

Model–view–controller is a software design pattern commonly used for developing user interfaces that divides the related program logic into three interconnected elements.

Model View Controller

In order to create an MVC app, we also need a template engine to render our HTML views. Typeix resty support any templating engine, for this example we are going to use handlebars.

Installation

Let's start by installing typeix cli and creating new project!

npm i -g @typeix/cli
Let's create new project:
typeix new app-mvc
cd app-mvc
Let's install handlebars:
npm i --save handlebars
npm i --save-dev @types/handlebars

Goal is to create following structure:

src
 L services
    L template.service.ts
 L interceptors
    L render.interceptor.ts   
 L app.controller.ts
 L app.service.ts
 L app.module.ts
 L bootstrap.ts
views
 L main.hbs

Templates

All templates we should keep outside source files, inside views folder:

mkdir views

Let's create custom template main.hbs:

<!DOCTYPE html>
<html lang="en">
<head>
  <link rel="icon" type="image/png" href="/favicon.png" />
  <link rel="stylesheet" href="/assets/css/main.css" />
  <meta charset="UTF-8">
  <title>{{title}}</title>
</head>
<body>
<img src="/assets/logo.png" width="100"/>
<h1>Headline: {{name}}</h1>
<p>
  Methods id: {{id}} name: {{name}};
</p>
</body>
</html>

Service

Let's create a template service which is responsible for loading, compiling and rendering template from disk.

NOTE:

In following example we are going to load and compile template and store it in memory for production use it's better to precompile template and loading it from disk as javascript file.

import {normalize} from "path";
import {readFile} from "fs";
import {Injectable} from "@typeix/resty";
import {compile} from "handlebars";


@Injectable()
export class TemplateEngineService {

  templates: Map<string, HandlebarsTemplateDelegate> = new Map();

  /**
   * Gets template path
   * @return {String}
   */
  static getTemplatePath(name: String): string {
    return normalize(process.cwd() + "/views/" + name + ".hbs");
  }

  /**
   * Read file from disk
   * @param template
   */
  async readFile(template: String): Promise<HandlebarsTemplateDelegate> {
    const path = TemplateEngineService.getTemplatePath(template);
    if (this.templates.has(path)) {
      return Promise.resolve(this.templates.get(path));
    }
    return new Promise((resolve, reject) => {
      readFile(path, {encoding: "utf8"},
        (err, data) => {
          if (err) {
            reject(err);
          } else {
            const tpl = compile(data);
            this.templates.set(path, tpl);
            resolve(tpl);
          }
        }
      );
    });
  }

  /**
   * Load template from disk
   * @param template
   * @param data
   * @returns {NodeJS.ReadableStream}
   */
  async compileAndRender(template: String, data: any): Promise<Buffer> {
    const tpl = await this.readFile(template);
    const html = tpl(data);
    return Buffer.from(html);
  }
}

Controller

In controllers, we need to use template engine service to compile and render template with custom data.

import {Inject, Controller, GET, PathParam, ResolvedRoute, IResolvedRoute} from "@typeix/resty";
import {TemplateEngineService} from "~/services/templating-engine.service";
import {Render} from "~/interceptors/render.interceptor";

@Controller({
  path: "/"
})
export class HomeController {

  @Inject() engine: TemplateEngineService;

  @GET()
  async actionIndex(): Promise<Buffer> {
    return await this.engine.compileAndRender("main", {
      id: "NO_ID",
      name: "this is home page",
      title: "Home page example"
    });
  }
}
NOTE:

In following example we did not use models, we just send data from controller as mock.

Render Interceptor

In order to make repeatable TemplateEngineService code obsolete, we will move that code into method interceptor.

import {
  createMethodInterceptor,
  Inject,
  Injectable, Interceptor, Method
} from "@typeix/resty";
import {TemplateEngineService} from "~/components/templating-engine.service";

@Injectable()
export class RenderInterceptor implements Interceptor {
  @Inject() engine: TemplateEngineService;
  async invoke(method: Method): Promise<any> {
    const data = await method.invoke();
    const result = await this.engine.compileAndRender(method.decoratorArgs.value, data);
    return await method.transform(result);
  }
}

/**
 * Asset loader service
 * @constructor
 * @function
 * @name Render
 *
 * @description
 * RenderInterceptor template
 */
export function Render(value: string) {
  return createMethodInterceptor(Render, RenderInterceptor, {value});
}
In following example you can see that Injecting TemplateEngineService is no longer required because we created custom decorator, and we decorated our controller action.
import {Inject, Controller, GET, PathParam, ResolvedRoute, IResolvedRoute} from "@typeix/resty";
import {Render} from "~/interceptors/render.interceptor";

@Controller({
  path: "/"
})
export class HomeController {

  @GET()
  @Render("main")
  async actionIndex(): Promise<Buffer> {
    return {
      id,
      name,
      title: "Template engine with typeix"
    };
  }
}