There will be certain pieces of the pages in our site that are
repeated. Things like headers, footers, and certain application-specific
widgets will appear on multiple pages, or every page. We could copy the
markup for those pieces into the pageâs template, but it will be easier to
manage a single shared template for each of these common elements. Copying
and pasting a reference to a child template is more maintainable than
copying and pasting the markup itself. If we donât want to have to repeat
the code that imports our CSS and client-side JavaScript, we can
templatize a default version of the pages in our site, storing all that
information there, in addition to site-wide headers and footers. We can
leave a space for the content of each page and reuse the rest. Letâs
create a file with this boilerplate markup called
parent.html
:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>{{pageTitle}}</title> <link rel="stylesheet" src="css/style.css" /> {{>stylesheets}} </head> <body> {{>content}} <footer>© 2011 Node for Front-End Developers</footer> </body> {{>scripts}} </html>
Every template engine handles nested templates differently. While
Mustache uses the {{>...}}
syntax you see above,
templating engines that are otherwise very similar to Mustache may use
something different. Try not to get too caught up in the particular syntax
of this engine, just know that the greater-than sign here indicates a
child template, and weâll use that to do composition.
To allow this parent template to load individual child templates and create full pages, weâll have to add some robustness into our application code. Instead of loading and rendering templates inline, weâll load another third-party module to manage loading, and move rendering to its own function.
The requirejs
module works like the client-side
Require.js utility, providing a slightly different approach to dependency
management than Nodeâs. The difference weâre taking advantage of below is
the text.js
extension. Weâll need to download this from the
Require.js site and save it in our Node applicationâs root directory. This
will allow Require to load and manage text files, guaranteeing that
theyâre loaded efficiently and available when theyâre needed. Adding the
prefix text!
to the path of the dependency (our template, in
this case) tells Require to load the file as text instead of trying to
evaluate it:
var connect = require("connect"), fs = require("fs"), mustache = require("mustache"), requirejs = require("requirejs"), parentTmpl; // configure requirejs to fall back to Node's require if a module is not found requirejs.config({ nodeRequire: require }); connect( connect.static(__dirname + "/public"), connect.router(function(app) { app.get("/show/:tmpl/:firstName/:lastName", function(req, res) { var userName = { firstName: req.params.firstName, lastName: req.params.lastName }; // once the parent template is loaded, render the page requirejs(["text!public/parent.html"], function(_parentTmpl) { parentTmpl = _parentTmpl; render(res, req.params.tmpl + ".html", userName); }); }); }) ).listen(8000); function render(res, filename, data, style, script, callback) { // load the template and return control to another function or send the response requirejs(["text!public/" + filename], function(tmpl) { if (callback) { callback(res, tmpl, data, style, script); } else { // render parent template with page template as a child var html = mustache.to_html( parentTmpl, {content: data}, {content: tmpl, stylesheets: style || "", scripts: script || ""} ); res.end(html); } }); }
While we could make our system for rendering templates within a wrapper far more robust, this smaller amount of code will let us do quite a bit (within certain constraints). In addition to the content for each page, our parent template and its rendering function will accept additional CSS or JavaScript imports as string literals. We can allow the render function to send back the rendered HTML without modification, or we can override that behavior by passing in an additional callback to call once the child template loads.
Now that we have files being loaded and requests being made in order, you can begin to see the issues with nesting callbacks that frighten some people who are new to Node. Weâve abstracted out some functionality, but there are still anonymous functions and nested logic. If youâre used to DOM scripting, this probably isnât as scary, but youâve probably also recognized it as something that needs to be managed. Although the code above is all in a single file for clarity, in a real application weâd put all of our template rendering code into its own module and move the logic into named functions private to that module.
Get Node for Front-End Developers now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.