Working on the front end of a web application can be difficult. As your software evolves and small changes add up, a simple piece of code can become brittle and unwieldy. Using view models in your application layer and a disciplined templating approach can increase a software’s maintainability and help engineers achieve front end zen.

At Yext I work on the Location Manager, a CMS system for business location data. Because the location data editor is so central to our products it has evolved significantly since it was created. Over time, related templates have increased in complexity and the render statement for the page was taking over forty arguments. In addition we were using Groovy templates where arbitrary Groovy (or Java) code is permitted. This lack of restriction within templates lead to complicated logic and even RPC calls. The application of view models and data-driven templates using Soy have allowed us to tame this beast.

View Models

View models are classes which are a representation of the user interface. The way in which data is stored does not always directly translate into the way in which the data is presented, and view models provide a mapping between the two.

The following is a simplified example of a view model and the corresponding template.

public class LinkView {
    public String url;
    public String displayValue;
}
/**
 * @param view The link view model 
 */
{template .link}
  <a href="{$view.url}">{$view.displayValue}</a>
{/template}

Let’s consider the scenario where the following theoretical changes are needed for the website’s links.

  1. A link without a display value should fall back to using the url
  2. In certain cases the link should open up in a new tab.
  3. When the link is an email, the url should be prepended with mailto:

The Template Padawan

/**
* @param view The link view model 
* @param? isEmail True if the link is an email address
* @param? shouldOpenInNewTab True if the link should open in a new tab
*/
{template .link}
  {if $isEmail}
    {let $href : 'mailto:' + $view.url /}
  {else}
    {let $href : $view.url /}
  {/if}
  <a href="{$href}"
    {if $shouldOpenInNewTab} target="_blank"{/if}>
    {if $view.displayValue}
      {$view.displayValue}
    {else}
      {$view.url}
    {/if}
  </a>
{/template}

When following this approach, templates will become more and more difficult to work with over time. As new considerations arise, render arguments are added and updates are made to the logic within the template. One pattern in particular that is a ‘code smell’ for templating is when your make use of maps from domain object id to some other attribute. These maps should often be refactored into properties of view models.

Instead we can take a different approach to implementing the changes to the the website links.

The Template Master

public class LinkView {
    public String href;
    public String displayValue;
    public boolean shouldOpenInNewTab = false;

    private LinkView(String href, String displayValue) {
        this.href = href;
        this.displayValue = displayValue;
    }

    public static LinkView forWebsite(String url) {
        return forWebsite(url, url);
    }

    public static LinkView forWebsite(String url, String displayValue) {
        return new LinkView(url, displayValue);
    }

    public static LinkView forEmail(String email) {
       return forEmail(email, email);
    }

    public static LinkView forEmail(String email, String displayValue) {
       return new LinkView("mailto:" + email, displayValue);
    }

    public void shouldOpenInNewTab() {
        this.shouldOpenInNewTab = true;
    }
}
/**
* @param view The link view model 
*/
{template .link}
  <a href="{$view.href}"
     {if $view.shouldOpenInNewTab} target="_blank"{/if}>
    {$view.displayValue}
  </a>
{/template}

The result is a much simpler template structure where the only logic within the template is that required to decide how to build the HTML. With the first approach, when you want to add new considerations you would have update every single affected caller to provide the new render argument. When taking the second approach, only the view model and the template have to be updated to add new considerations.

Micro-layouts

When creating HTML templates, sometimes you want to reuse common page structure like headers, footers, and navigation. We call those layout templates.

Example Layout Template

/**
* @param bodyContent The content for the body
* @param currentPage The current page you are on
*/
{template .layout}
  <header>Hello world website</header>
  <ul>
    <li{if currentPage == 'home'} class="selected"{/if}>Home</li>
    <li{if currentPage == 'account'} class="selected"{/if}>Account</li>
    <li>Logout</li>
  </ul>
  <div class="main-body-container">
    {$bodyContent}
  </div>
  <footer>
    &copy; Hello World
  </footer>
{/template}

{template .account}
  {call .layout}
    {param currentPage : 'account' /}
    {param bodyContent}
    <h2>This is the account page</h2>
    <p>The account page content can be rendered here</p>
    {/param}
  {/call}
{/template}

Whenever possible, templates should be reused. You should avoid duplicating any HTML throughout your codebase and breaking a template into sub-templates can help with reusability. On the other hand, a template can be overused when you try to make it fit multiple varying use cases. This will be obvious when the template is packed full of logic that is specific to each use case.

One solution that can be applied to fix this problem is using layout templates for a small portion of the page, what I now deem the micro-layout. These micro-layouts allow you to reuse as much template code as possible without packing the templates full of use case specific logic.

Moving toward zen

In conclusion, I believe that well structured view models and a disciplined templating approach can lead software engineers toward front end zen. Working with complex and fragile templates can be time consuming and apt to breakage. In addition, reducing the number of render arguments passed to a template can speed up future updates to existing code.