Routing and Template Toolkit
Dreamwidth is currently in the process of moving away from BML and towards a system of routing that employs Template Toolkit for templates.
Contents
The Basics
Making a page with the routing and Template Toolkit system requires a few things:
- Define a URL string or pattern
- Create a handler function, attached to the URL string or pattern
- Make any needed templates
Defining a URL
Here are some examples of registering a URL. There are two main functions to do this with, register_string and register_regex.
# This registers a static string, which is an application page. DW::Routing->register_string( '/misc/whereami', \&whereami_handler, app => 1 ); # This registers a regular expression. Later, we can get the # contents inside the parentheticals. This lets us make "clean" urls # like /nav/read and /nav/create that are equivalent to # /nav?cat=create DW::Routing->register_regex( qr!^/nav(?:/([a-z]*))?$!, \&nav_handler, app => 1 ); # This registers a user page, so, for example, it would be accessed at # username.dreamwidth.org/data/edges # This also explicitly sets our default output format to be JSON. DW::Routing->register_string( "/data/edges", \&edges_handler, user => 1, format => 'json' );
Creating a handler function
I'm going to use the Nav controller as an example. You can see the entire page at DW::Controller::Nav.pm.
We define our URL in the same file we place the handler function, so we'll start there:
package DW::Controller::Nav; use strict; use warnings; use DW::Controller; use DW::Routing; use DW::Template; use DW::Logic::MenuNav; use JSON; # Defines the URL for routing. I could use register_string( '/nav' ... ) if I didn't want to capture arguments # This is an application page, not a user styled page, and the default format is HTML (ie, /nav gives /nav.html) DW::Routing->register_regex( qr!^/nav(?:/([a-z]*))?$!, \&nav_handler, app => 1 );
First, I want to get the options and request:
# handles menu nav pages sub nav_handler { my $opts = shift; my $r = DW::Request->get;
Remember that subpattern I made in my URL regex? I'll figure out if I'm using it or an argument here.
# Check for a category like nav/read, then for a ?cat=read argument, else no category my $cat = $opts->subpatterns->[0] || $r->get_args->{cat} || '';
Here, I'm doing error checking. If I don't get back the array of menu hashes, I'm going to serve an error page that contains a translated error message. Notice how I have to define the whole path to the error message, and can't just use ".error.invalidcat".
# this function returns an array reference of menu hashes my $menu_nav = DW::Logic::MenuNav->get_menu_display( $cat ) or return error_ml( '/nav.tt.error.invalidcat' );
I'm getting some cruft that is needed by the real menu that I don't want to display on this page, so I'll go through all the menus and strip out HTML from the titles. Ideally there would be a Template Toolkit filter that could do this, but I have not found one yet; we may have to make one.
# this data doesn't need HTML in the titles, like in the real menu for my $menu ( @$menu_nav ) { for my $item ( @{ $menu->{items} } ) { $item->{text} = LJ::strip_html( $item->{text} ); } }
This is a nifty part. I can display the contents of this page either as HTML or as JSON (which is made for machine parsing), with very very little additional code.
The first one is how to return something as JSON. I just print out the object conversion to the request and return OK.
# display according to the format my $format = $opts->format; if ( $format eq 'json' ) { # this prints out the menu navigation as JSON and returns $r->print( JSON::objToJson( $menu_nav ) ); return $r->OK;
The HTML format takes a bit more work. We want to pass in more variables to the template and return the rendered template.
Here we go preparing the variables:
} elsif ( $format eq 'html' ) { # these variables will get passed to the template my $vars = { menu_nav => $menu_nav, cat => $cat, }; $vars->{cat_title} = $menu_nav->[0]->{title} if $cat;
And this is the call to render our template (more on making that next):
# Now we tell it what template to render and pass in our variables return DW::Template->render_template( 'nav.tt', $vars );
If my format isn't HTML or JSON, we throw a 404.
} else { # return 404 for an unknown format return $r->NOT_FOUND; } } 1;
And that's the handler!
Creating a template
The template will be placed somewhere logical in $LJHOME/views. This one will be nav.tt. Currently, translation strings will go in nav.tt.text.
This section is a comment:
[%# nav.tt Page that shows the sub-level navigation links given the top-level navigation header %]
This section sets the title of the page. If we have a category, it will be the category title. Otherwise, it will use the ML translation of the title for the page.
[%- IF cat; sections.title = cat_title; ELSE; sections.title = '.title' | ml; END -%]
This section creates the menu sections, applying code for each menu and each item in each menu. If this isn't only displaying a category, it makes the title. You can see the html filter being used in places like [% menu.title | html %] to ensure that all HTML is safe, and the url filter being used in [% menu_item.url | url %] to make sure that URL will be validly encoded.
[% FOREACH menu = menu_nav %] [% IF NOT cat %]<h2 class="[% menu.name %]">[% menu.title | html %]</h2>[% END %] <ul> [% FOREACH menu_item = menu.items %] <li><a href="[% menu_item.url | url %]">[% menu_item.text | html %]</a></li> [% END %] </ul> [% END %]
That template is all that's required to render the HTML for the nav page! You can see it live on Dreamwidth.
Standard tricks
Inserting variables into translation strings
The example above sneaked in the way to use the translation system from within templates, by doing:
[%- IF cat; sections.title = cat_title; ELSE; sections.title = '.title' | ml; END -%]
The relevant part above is bolded. But what if you needed to insert something, such as the sitename or a username, into a string? Here's a fragment that does the latter, drawn from views/misc/pubkey.tt:
[% '.label' | ml(user = u.ljuser_display) %]
Including a file with a temporary ML scope change
[% scope = dw.ml_scope( ); CALL dw.ml_scope( '/stats/site.tt' ); INCLUDE stats/site.tt; CALL dw.ml_scope( scope ); %]
Specifying needed CSS/JS files
[%- CALL dw.set_active_resource_group( 'jquery' ) -%] [%- dw.need_res( 'stc/kitten.css' ) -%] [%- dw.need_res( 'js/ponies.js' ) -%] [%- dw.need_res( ( group => 'jquery' ), 'js/sparkle.js' ) -%]
Note: ignore the first and last if your page doesn't use jQuery.
References
- Template Toolkit Documentation
- DW::Request API Documentation
- DW::Routing API Documentation
- DW::Template API Documentation
- Plugin Documentation
- Filter Documentation