MVC View Strategy

☝🏾
This entry applies only to projects which have a clear MVC structure, such as a Laravel project. In Craft or Jekyll projects, less strict separation of concerns is usually necessary.

Our codebases have widely varied view layer technologies and needs, so a global standard is not possible. However, there are some abstract principles we use in our templates to keep them DRY but not SNOYCUT (So nested only you can understand them)

What do Views Do?

Views turn data into markup. Their job is to receive generic information from controllers regarding what should go on the page, and to faithfully render that information within HTML pages which our CSS and Javascript can act on. Views should not:

  • Contain logic, (ex. choosing one post from a set of posts and rendering only that one)
  • Do math (ex. calculating the tax for a product based on a percentage tax rate)
  • Request data (ex. fetching the related posts to the current post being shown)

These things are the responsibility of the Model and Controller layer. Views should be simple workers which read data and output markup.Views should:

  • Be structured as simply as they can be
  • Repeat identical markup as rarely as possible
  • Isolate frequently-accessed markup in easy-to-find partials
  • Remain approachable to developers unfamiliar with the templating language in place (twig, underscore, blade, php, etc.)

The "View Layer" is our term for the collection of templates and associated helper code that handles these responsibilities. In most of our sites, all such code will be contained in a /views/ directory.

Templating Basics

In the simplest case, an application would have a template for every route that it renders. That template would contain all the markup required to render that particular page.

home.html

<header class="site-header">Welcome</header>
<main>This is the site</main>
<footer class="site-footer">Copyright 2018</footer>

Of course, we don‘t do that. Starting at a minimum with global elements (header, footer), we "abstract" out parts of the view which are repeated across multiple pages. These are "partials", or "includes". We can’t do this in regular HTML, so we need to be using a templating language like Twig.

home.twig

{% include 'header.twig' %}
This is the site
{% include 'footer.twig' %}

header.twig

<header class="SiteHeader">Welcome</header>
<main>

footer.twig

</main>
<footer class="SiteFooter">Copyright 2018</footer>

With more advanced templating systems like Twig, we can abstract out code in reverse – by taking the global parts and putting them a "layout" which each individual view then "extends". This means that instead of inserting the header and footer into each view, the view is itself inserted into the globally-defined template.

home.twig

{% extends 'layout.twig' %}
{% block content %}
  This is the site
{% endblock %}

This template says

"use the layout.twig file. Find where it places the "content" block, and put 'This is the site' inside there."

layout.twig

<header class="site-header">Welcome</header>
<main>{{ block('content') }}</main>
<footer class="site-footer">Copyright 2018</footer>

Since layout.twig itself is likely to get pretty long, it itself may end up with {% include %} statements to "partial out" complex parts like the header.

layout.twig

{% include 'header.twig' %}
<main>{{ block('content') }}</main>
{% include 'footer.twig' %}

This is our standard layout methodology.

  • Includes/Helpers: Small template parts which are added to individual templates or layouts (These are distinguished later in this document)
  • Layouts: "Master" templates which act as a wrapper for individual page templates
  • Templates: The actual files that get called by our controllers, utilizing a layout and typically some Includes

What Does a Good View Layer Look Like?

In a streamlined, clean view layer, markup should rarely be repeated across different files or within a given file. Whenever markup can easily be "abstracted out" and looped through instead of manually repeated, we try to do that.So if page A has this structure:

page-a.twig

<div class="class-a class-b">
  <div class="class-a__a">
    {{ title }}
  </div>
</div>

And page B has this structure:

page-b.twig

<div class="class-a class-b class-c">
  <div class="class-a__a">
    {{ title }}
  </div>
</div>

It makes a ton of sense to abstract out this markup pattern into an include, making the structure:

page-a.twig

{% include 'my-pattern.twig' %}

page-b.twig

{% include 'my-pattern.twig' with {
  'additional_classes': 'class-c'
 %}

my-pattern.twig

<div class="class-a class-b {{ additional_classes }}">
  <div class="class-a__a">
    {{ title }}
  </div>
</div>

The first structure has two blocks which are intrinsically linked – any time one is updated, the other is likely to need to be updated. This is a maintenance headache. If a new developer is making a page which calls for a similar pattern, they might not know about page-a and page-b, and might make it again by hand. Perhaps their version is slightly different, introducing an inconsistency to the design and a headache in QA.

"Abstracting out" the markup pattern forces developers to be consistent across different uses of the same entity. It also greatly increases the legibility of the original templates, by clearly dictating what is getting printed where within the template, and giving developers a "birds-eye view" of the page as a whole. It also saves time, since any time this markup pattern changes design, the change can be done in one place.

Anti-Pattern

Taking this idea to the extreme, some views can end up too abstracted, with includes nested in includes five or six times between the actual view and the markup it creates. Consider if we decided to partial out the `class-a__a` div, because, after all, there is a separate markup pattern which uses it as `<span class="class-a__a">` , and we want to consolidate.

my-pattern.twig

<div class="class-a class-b {{ additional_classes }}">
  {% include 'class-a__a' with {
    'element_type': 'div'
  } %}
</div>

Why stop there? Let’s say we have a similar markup pattern somewhere else in the app which uses a table with ClassB, but not ClassA. Can’t we consolidate with a Twig embed?

my-pattern.twig

{% embed 'my-super-pattern.twig' with {
  'element_type': 'div'
  'additional_classes': "class-c class-a"
} %}
  {% block content %}
    {% include 'class-a__a' with {
      'element_type': 'div'
    } %}
  {% endblock %}
{% endblock %}

Yes, in one sense we have reduced the amount of repetition in the codebase by abstracting out the pattern into the super pattern and sharing it with another part of the application. But we have left ourselves with a hard-to-read and hard-to-update structure which is not hospitable to new developers. These are bad templates that are now SNOYCUT (So nested only you can understand them).

The art of building a good view layer is balancing the need to be DRY with the need to be sensible and legible. We build sites to be worked on for years by many people. Taking DRY to the extreme will always lead to difficulties given this constraint.Consider the classic example of `.title` within our design system. We have tried many times to create flexible partials so that can generate instances of our objects entirely via includes. For some elements it can be OK, but for many of our elements, the number of corner cases and exceptions causes the include to end up being really gnarly.

For example, let’s say we made a title include that is meant to be used any time we want a `.title` element.In a reasonable case, it would look like:

{% include "includes/title.twig" with {
  element: "h2"
  extensions: ["primary", "primary--font-size-small"],
  content: "Suggestions For Ways to Spend Your MLK Day"
} %}

And the element it generates would look like:

<h2 class="title title--primary title--primary--font-size-small">
  Suggestions For Ways to Spend Your MLK Day
</h2>

The actual HTML output is already simpler then the tag used to generate it! Then you get into the corner cases – what if an extension needs a special child, or we want to put some conditionals into the content? Very quickly the template can become unwieldy as it handles all the permutations. In these cases we prefer to see the markup as the API for generating view objects, as funky as that sounds.

So, our default is that most Objects are created "by hand" in templates, this means you do type class="object-name" a lot, but it seriously reduces the complexity of the application. A key downside obviously is that you can’t change the markup for all instances of `object-name` all at once, but since our system is pretty locked down, that happens very rarely.

On the other hand, we do often bundle CSS objects into a "Prefab" include.

image

Note that these partials are really locked to the content they display.

image

Heuristics

When working on views, it can be useful to consider some guiding heuristics to decide how to balance between DRY and SNOYCUT.

  • Total Lines of code required to render a given view. This is a really simplistic way to look at it, but can be helpful in guiding your higher-level choices. In our page-a and page-b example from earlier...
    • Manually writing out the HTML in each template was 10 total lines of code
    • Partialling out the shared pattern led to 9 total lines
    • Creating the frankenstein shared pattern meant we needed 14 total lines, plus the lines in the new embed patttern.
  • Total templates needed to render a given view. If you need 17 nested template files to render the about page, you’ve gone too far.
  • Amount of times a given class string (ex. class="whatever whatever--extension") is written within the codebase. Identical matches probably indicate that abstraction is required.

Again, this is more art than science. You are balancing the need for DRY with the need to keep the codebase understandable and legible. Consider other developers, but also future you.

Example Project

Consider a simple Twitter application. Here is how we would likely structure the view layer:

  • /templates The actual page templates rendered by the backend
    • home.twig
    • stream.twig
    • tweet.twig
    • settings.twig
  • /layouts The page "shells" for the templates to sit in
    • master.twig
    • basic.twig Used for settings page
  • /includes The markup associated with specific types of data
    • tweet.twig Included on many pages
    • user.twig
    • setting.twig
    • image-block.twig Included by tweet.twig, user.twig
    • dropdown.twig included by setting.twig, tweet.twig
    • module--island.twig Included by tweet.twig, user.twig