Medium-Scale Dancer, Part 3: Views

The first two parts of this article series broke the monolithic lib/App.pm file up into a series of Perl modules, each focused on a functional centroid of the application.

If we describe the default lib/App.pm file as monolithic, then we might get away with describing the app's views/ tree as "polylithic" because there is no monolith to break up; the template files in this subtree naturally map to web pages, so the app's URL scheme effectively keeps them separated by purpose.

Although we will not start this part of the series needing to break up an overly large file, I can pass along some tips that might cause you to rethink the current structure of your app's views/* tree.

The View Naming Scheme Should Match the URL Scheme

Older web frameworks like PHP, ASP, and JSP define the web app's URL structure in the file system. The route scheme defined in the previous part of this article series would look like this in PHP:

index.php
mf/index.php
mf/sf/index.php

The view code — HTML and inline JavaScript — is intermixed with the server-side code in the same file. Worse, single files like mf/sf/index.php have to handle multiple HTTP verbs; in our example, GET, POST, PUT, and DELETE. If each verb returns a different web page, you end up with a huge top-level if/elseif/else block dividing the file up into a bunch of logically separate chunks. There are other bad consequences to this web framework design choice, such as that if you want your text editor to do syntax coloring correctly, it needs to be able to switch among PHP, JavaScript, and HTML syntax, all within the same file. The programmer ends up needing to do the same sort of context switching, too.

Simply put, this is a mess.

Template::Toolkit — Dancer's default template system — solves part of this problem by separating the view files from the server-side Perl code. Modern web app development frameworks like Angular solve the rest of it by encouraging you to move all of your JavaScript code into separate *.js files, rather than put it inline in the HTML file.

There is nothing about the way Dancer works that enforces or even encourages any particular views/* file naming scheme. Thus this tip: for your own sanity, I recommend that you name your template files after the corresponding route. The Dancer scheme corresponding to the above PHP scheme might look something like this:

views/top.tt
views/mf/top.tt
views/mf/sf/get.tt
views/mf/sf/post.tt
views/mf/sf/put.tt
views/mf/sf/delete.tt

This scheme maps Dancer routes 1:1 to template files, using a naming scheme that is easy to remember since it's nearly identical to the URL scheme. It's so simple a mapping that you could write a Perl one-liner to do the transform. Restricting yourself to a simple naming convention removes a whole class of details that you have to keep in mind while working on your web app; you don't have to guess where the template file for a given route handler lives, you already know where it lives. You will quickly rediscover this simple mapping years from now when you have to work on the web app again.

This naming scheme is very much a suggestion, rather than a prescription. For one thing, you may find that the DELETE /mf/sf route needs no view, because it just deletes one database record and then redirects you to GET /mf/sf to show the change in the normal view. In that case, you would not need views/mf/sf/delete.tt.

The important thing is to have a well-thought-out naming scheme, not necessarily to follow the one I've laid out above.

Factor Common UI Elements Out

If you reuse a particular UI element across different parts of your web app, put its template file into a special subdirectory of views/. I use views/parts for this.

If you have an HTML fragment page that is incorporated into your web app dynamically on the client side via Ajax, you will need a corresponding Dancer route for it, and probably a Perl module for it, too:

views/parts/component.tt
lib/App/Parts/Component.pm

Then in lib/App/Parts/Component.pm:

sub _get {
    # ...work out how to build the component
    
    return template '/parts/component' => {
        stuff => 'goes here'
    };
}
    
prefix '/parts' => sub {
    get 'component' => \&_get;
};

I chose the naming scheme for the function carefully. I want to be able to use short names here that correspond to the HTTP verb that the function handles without colliding with the short names exported by Dancer's DSL. The standard Perl style rules say that a leading underscore marks a function that isn't meant to be called from outside the module, which is exactly correct here; only the route handler defined below that function should be calling it.

You may prefer a different scheme, such as Get(), even though this is unconventional Perl style. Again, the important thing is to have a consistent style, not that you use the style I've chosen here.

One argument in favor of Get() over _get() is that you might need to be able to reuse this route handler in another of your Perl modules, bodily incorporating the HTML fragment within another web page on the server side, thus saving an Ajax round trip the first time that that web page is served.

For example, lib/App/MajorFeature.pm might have something like this in it:

sub Get {
    # Work out how to build the main part of the top-level /mf page
    ...Perl code here...
    
    # Assemble the pieces
    return template '/mf/top' => {
        component   => App::Parts::Component::Get(),
        other_stuff => 'goes here',
    };
}
    
prefix '/mf' => sub {
    get '/' => \&Get;
    # other route handlers here
};

Then within views/mf/top.tt, you can reference $component to bodily include that component's HTML fragment into the larger page.

Now that we have sorted out the purely server-side HTML and Perl code files, we will look at the Dancer app design decisions that affect its presentation to the browser in the next part of this article series.

Author

This article has been written by Warren Young for the Perl Dancer Advent Calendar 2016.

Copyright

© 2015, 2016 by Educational Technology Resources, licensed under Creative Commons Attribution-ShareAlike 4.0