Summary
I am using Express + Jade for my web application, and I'm struggling with rendering partial views for my AJAX navigation.
I kind of have two different questions, but they are totally linked, so I included them in the same post. I guess it will be a long post, but I guarantee it's interesting if you have already struggled with the same issues. I'd appreciate it very much if someone took the time to read & propose a solution.
TL;DR : 2 questions
Requirements
My layout.jade is :
doctype html
html(lang="fr")
head
// Shared CSS files go here
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
block content
// Shared JS files go here
script(src="js/jquery.min.js")
My page_full.jade is :
extends layout.jade
block content
h1 Hey Welcome !
My page_ajax is :
h1 Hey Welcome
And finally in router.js (Express) :
app.get("/page",function(req,res,next){
if (req.xhr) res.render("page_ajax.jade");
else res.render("page_full.jade");
});
Drawbacks :
My layout.jade remains unchanged :
doctype html
html(lang="fr")
head
// Shared CSS files go here
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
block content
// Shared JS files go here
script(src="js/jquery.min.js")
My page_full.jade is now :
extends layout.jade
block content
include page.jade
My page.jade contains the actual content without any layout/block/extend/include :
h1 Hey Welcome
And I have now in router.js (Express) :
app.get("/page",function(req,res,next){
if (req.xhr) res.render("page.jade");
else res.render("page_full.jade");
});
Advantages :
Drawbacks :
Using Alex Ford's technique, I could define my own render
function in middleware.js :
app.use(function (req, res, next) {
res.renderView = function (viewName, opts) {
res.render(viewName + req.xhr ? null : '_full', opts);
next();
};
});
And then change router.js (Express) to :
app.get("/page",function(req,res,next){
res.renderView("/page");
});
leaving the other files unchanged.
Advantages
Drawbacks
renderView
method feels a litle dirty. After all, I
expect my template engine/framework to handle this for me.I don't like using two files for one page, so what if I let Jade decide what to render instead of Express ? At first sight, it seems very uncomfortable to me, because I think the template engine should not handle any logic at all. But let's try.
First, I need to pass a variable to Jade that will tell it what kind of request it is :
In middleware.js (Express)
app.use(function (req, res, next) {
res.locals.xhr = req.xhr;
});
So now my layout.jade would be the same as before :
doctype html
html(lang="fr")
head
// Shared CSS files go here
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
block content
// Shared JS files go here
script(src="js/jquery.min.js")
And my page.jade would be :
if (!locals.xhr)
extends layout.jade
block content
h1 Hey Welcome !
Great huh ? Except that won't work because conditional extends are impossible in Jade. So I could move the test from page.jade to layout.jade :
if (!locals.xhr)
doctype html
html(lang="fr")
head
// Shared CSS files go here
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
block content
// Shared JS files go here
script(src="js/jquery.min.js")
else
block content
and page.jade would return to :
extends layout.jade
block content
h1 Hey Welcome !
Advantages :
req.xhr
test in every route or in every
viewDisadvantages :
These are all techniques I thought of and tried, but none of them really convinced me. Am I doing something wrong ? Are there cleaner techniques ? Or should I use another template engine/framework ?
What happens (with any of these solutions) if a view has its own JavaScript files ?
For example, using solution #4, if I have two pages, page_a.jade and page_b.jade which both have their own client-side JavaScript files js/page_a.js and js/page_b.js, what happens to them when the pages are loaded in AJAX ?
First, I need to define an extraJS
block in layout.jade :
if (!locals.xhr)
doctype html
html(lang="fr")
head
// Shared CSS files go here
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
block content
// Shared JS files go here
script(src="js/jquery.min.js")
// Specific JS files go there
block extraJS
else
block content
// Specific JS files go there
block extraJS
and then page_a.jade would be :
extends layout.jade
block content
h1 Hey Welcome !
block extraJS
script(src="js/page_a.js")
If I typed localhost/page_a
in my URL bar (non-AJAX request), I would get a compiled version of :
doctype html
html(lang="fr")
head
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
h1 Hey Welcome A !
script(src="js/jquery.min.js")
script(src="js/page_a.js")
That looks good. But what would happen if I now went to page_b
using my AJAX navigation ? My page would be a compiled version of :
doctype html
html(lang="fr")
head
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
h1 Hey Welcome B !
script(src="js/page_b.js")
script(src="js/jquery.min.js")
script(src="js/page_a.js")
js/page_a.js and js/page_b.js are both loaded on the same page. What happens if there's a conflict (same variable name etc...) ? Plus, if I go back to localhost/page_a using AJAX, I would have this :
doctype html
html(lang="fr")
head
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
h1 Hey Welcome B !
script(src="js/page_a.js")
script(src="js/jquery.min.js")
script(src="js/page_a.js")
The same JavaScript file (page_a.js) is loaded twice on the same page ! Will it cause conflicts, double firing of each event ? Whether or not that's the case, I don't think it's clean code.
So you might say that specific JS files should be in my block content
so that they're removed when I go to another page. Thus, my layout.jade should be :
if (!locals.xhr)
doctype html
html(lang="fr")
head
// Shared CSS files go here
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
block content
block extraJS
// Shared JS files go here
script(src="js/jquery.min.js")
else
block content
// Specific JS files go there
block extraJS
Right ? Err....If I go to localhost/page_a
, I will get a compiled version of :
doctype html
html(lang="fr")
head
link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
body
div#main_content
h1 Hey Welcome A !
script(src="js/page_a.js")
script(src="js/jquery.min.js")
As you might have noticed, js/page_a.js is actually loaded before jQuery, so it won't work, because jQuery is not defined yet...
So I don't know what to do for this problem. I thought of handling script requests client-side, using (for example) jQuery.getScript()
, but the client would have to know the scripts' filename, see if they're already loaded, maybe remove them. I don't think it should be done client-side.
How should I do handle JavaScript files loaded via AJAX ? Server-side using a different strategy/template engine ? Client-side ?
If you've made it this far, you're a true hero, and I'm grateful, but I would be even more grateful if you could give me some advice :)
Great question. I don't have a perfect option, but I'll offer a variant of your solution #3 that I like. Same idea as solution #3 but move the jade template for the _full file into your code, since it is boilerplate and javascript can generate it when needed for a full page. disclaimer: untested, but I humbly suggest:
app.use(function (req, res, next) {
var template = "extends layout.jade\n\nblock content\n include ";
res.renderView = function (viewName, opts) {
if (req.xhr) {
var renderResult = jade.compile(template + viewName + ".jade\n", opts);
res.send(renderResult);
} else {
res.render(viewName, opts);
}
next();
};
});
And you can get more clever with this idea as your scenarios become more complicated, for example saving this template to a file with placeholders for file names.
Of course this is still not a perfect solution. You're implementing features that should really be handled by your template engine, same as your original objection to solution #3. If you end up writing more than a couple dozen of lines of code for this then try to find a way to fit the feature into Jade and send them a pull request. For example if the jade "extends" keyword took an argument that could disable extending the layout for xhr requests...
For your second problem, I'm not sure any template engine can help you. If you're using ajax navigation, you can't very well "unload" page_a.js when the navigation happens via some back end template magic. I think you have to use traditional javascript isolation techniques for this (client-side). To your specific concerns: Implement the page specific logic (and variables) in closures, for starter, and have sensible cooperation between them where necessary. Secondly, you don't need to worry too much about hooking up double event handlers. Assuming the main content gets cleared on ajax navigation and also those are the elements that had the event handlers get attached that will call get reset (of course) when the new ajax content is loaded into the DOM.