Template Toolkit / Foundation Conversion Guide

From Dreamwidth Notes
Jump to: navigation, search

This guide is intended to assist developers interested in helping us convert legacy Dreamwidth pages to our more modern templating system.

What is a legacy page?

Legacy pages are files in the project's htdocs directory that have the .bml suffix. BML is a markup language that was written specifically for use with LiveJournal, and we are trying to gradually reduce our use of BML by replacing these pages with controllers and views.

If you are not already familiar with the Model-View-Controller (MVC) paradigm for implementing user interfaces, you can think of it like an antique telephone switchboard. The person requesting the page and the database containing the information the page requires are trying to connect and have a conversation. The View describes the person's end of the connection, and the Model describes the database's end of the connection. The Controller is the switchboard operator, making sure the correct information is being sent to the correct place in each direction.

A BML file, by comparison, is like a jumbled plate of spaghetti. To convert it to the new system, the main part of the task involves simply untangling the different types of intermingled code and putting them in their correct places.

Getting started

There is already a great Routing and Template Cookbook that answers many specific questions about "how to do X" in the new system. I also recommend looking around at the existing files in the cgi-bin/DW/Controller directory to get a sense of how things are organized before beginning work on a new contribution.

Here are all the elements of a very basic page using a controller and a template:


The controller is written in Perl and does all the heavy lifting in terms of retrieving and processing information. Here is an example of a simple controller:


   package DW::Controller::Misc;

   use DW::Controller;
   use DW::Routing;
   use DW::Template;

   DW::Routing->register_string( '/misc/whereami', \&whereami_handler, app => 1 );

   sub whereami_handler {
       my ( $ok, $rv ) = controller( authas => 1 );
       return $rv unless $ok;

       my $vars = { %$rv,
           cluster_name => $LJ::CLUSTER_NAME{$rv->{u}->clusterid},

       return DW::Template->render_template( 'misc/whereami.tt', $vars );

All controller modules have the same basic three components:

Connects a requested URL (in our example, /misc/whereami) to the subroutine that can handle the request (&whereami_handler). The app => 1 argument means that it's a site page on dreamwidth.org, not a journal page, which would use user => 1.
Does the common setup work needed for every page request, such as checking whether a user is logged in, getting a list of communities the user can post in, checking any necessary user privileges, authenticating posted forms, etc. If the controller encounters an error during setup, the $ok variable will be false and $rv will contain an appropriate error page. In this example, we have requested the authas menu, which will be made available as $rv->{authas_html} for use in the template.
This connects the file containing the page template (here, misc/whereami.tt) to the variables it needs to populate the page. The only variable we need to add in this simple example is the name of the requested user's cluster. Everything else has already been prepared. For more complicated pages, we may need to include arrays and/or hashes of data that the template can iterate through.

Most controller handlers will also include code for handling posted forms and checking user input for errors. We'll see some examples of that later on.


The template formulates the view that is ultimately rendered and displayed to the user. It uses Template Toolkit, which is a simple but powerful language for extending HTML to include variables and basic logic structures. We store our templates and their sibling translation files in the views/ subdirectory. Here is the template for our simple controller:

    [%- sections.title = '.title' | ml -%]
    <p>[% '.intro' | ml %]</p>
    <form action='[% site.root %]/misc/whereami'>
        [% authas_html %]
    </form><br />
    [%- IF cluster_name -%]
        [%- user_cluster = cluster_name -%]
    [%- ELSE -%]
        [%- user_cluster = '.cluster.unknown' | ml -%]
    [%- END -%]
    <p>[% '.cluster' | ml(user = u.ljuser_display, cluster = user_cluster) %]</p>

Again, much of the work — rendering headers, footers, navigation menus, etc. — is invisibly done for us. All we need to specify here is the title of the page (sections.title) and the central content of the page. Anything not delimited by [% %] is assumed to be normal HTML. Appending | ml to the end of a string or variable name interprets it as text to be filled in by the translation system. The 'site' hash contains several common global variables for easy access (site.root in TT is the same as $LJ::SITEROOT in Perl). Any objects included for use in the page can use the same dot syntax to access methods (e.g. u.ljuser_display).

In addition to the obvious IF / UNLESS / ELSIF / ELSE directives, the other one you will see often is FOREACH, which works a lot like Perl's foreach for iterating over the elements of an array. More information about allowable TT syntax is available from the manual.

Where's the model?

I haven't touched on the model here because well-behaved code already uses independent modules to encapsulate the necessary functions and methods. However, if you are unfortunate enough to stumble upon a BML file that uses SQL statements to update the database directly, you may want to consider creating a new module to package that code more neatly elsewhere, depending on how complicated the interaction is and how often it is repeated. For an example of separation of model vs. controller, compare DW::User::Rename to DW::Controller::Rename, or DW::Media to DW::Controller::Media.

Common translation issues

Most BML files are fairly straightforward to translate following the BML Conversion Workflow, but here are some common issues you may encounter.

Different variables for accessing GET/POST requests

BML pages automatically populate two global variables, %GET and %POST, for accessing submitted form data. To access these values in a controller file, you will need to reference DW::Request->get->get_args and DW::Request->get->post_args, respectively. Similarly, any uses of LJ::did_post() should be replaced with DW::Request->get->did_post. Common usage will look something like the following:

   my $r = DW::Request->get;

   if ( $r->did_post ) {
       my $mode = $r->post_args->{mode};
   } else {
       my $mode = $r->get_args->{mode};

Functions in the BML package

Any functions that begin with "BML::" should be removed or replaced. Some of these are easy — for example, BML::get_request is the same thing as DW::Request->get, and BML::ml is equivalent to LJ::Lang::ml. Others, like BML::self_link, need to be translated to use newer functions that perform a similar purpose, such as LJ::create_url. If you run into one of these and aren't sure what to do, ask!

HTML convenience methods

Many convenience methods have been reimplemented for Template Toolkit. For example, LJ::html_text and LJ::html_submit, used for printing form elements, can be replaced by Template Toolkit functions (form.textbox and form.submit) that are defined in DW::Template::Plugin::FormHTML. In general, we strive to render as little HTML in the controller file as possible, and try to do it all in the template.

Tables for layout

If the page you are converting uses a table for page layout purposes, please, please, please rewrite the HTML to remove the table. This is an accessibility issue! If you're not sure whether a table is for data or for layout, check the opening table tag. If it looks like <table summary=""> it is a layout table and should be removed if possible.

Error processing

Most pages that process form data already have some form of error handling in place. Going forward, we are trying to standardize on using DW::FormErrors to track and report form processing errors. The wiki page on Error Handling shows how to set up and use DW::FormErrors in a typical controller. The really nifty thing about this setup is that our templates automatically handle the display of these errors in a standard way.

Converting a template to use Foundation

Once you have a template, this is how you convert it to use Foundation. Add this line near the top of the template file, either before or just after the title is set:

   [%- CALL dw.active_resource_group( "foundation" ) -%]

That's it! Don't believe me? Try it and see!

Of course, you'll probably notice the appearance of the page is altered in some respects, since Foundation pages use a different source of CSS. In fact, Foundation pages use an enhanced form of CSS known as SCSS, which has a more powerful and programmable syntax than regular CSS. So if you want to tweak the appearance of your Foundation page, you have two options:

1. Use existing formatting options from the style guide. These standard elements and classes are documented in views/dev/style-guide.tt and at http://www.dreamwidth.org/dev/style-guide and are included by default in Foundation pages on our site.
2. Write your own SCSS rules for styling the page. If the page you are converting already has an associated CSS file, you may want to preserve some of the existing rules in the new template, although you are encouraged to evaluate the existing rules and remove any that are no longer needed. Colors in particular should be standardized using the SCSS variables defined in the site skin files (located at htdocs/scss/skins) when possible.

If you do decide to add an SCSS file for your page, it should go in htdocs/scss/pages, and you will need to compile the SCSS to CSS using compass compile, as documented on the SCSS page. You should reference it in your template like so:

   [%- dw.need_res( { group => "foundation" }
   ) -%]

Further reading

Official sites for the software kits included here: