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
- What's the cleanest and fastest way to render fragments of views for an AJAX navigation with Express + Jade ?
- How should JavaScript files relative to each view be loaded ?
Requirements
- My Web App needs to be compatible with users who have disabled
JavaScript - If JavaScript is enabled, only the page's own content (and not the whole layout) should be sent from the server to the client
- The app needs to be fast, and load as few bytes as possible
Problem #1 : what I've tried
Solution 1 : having different files for AJAX & non-AJAX requests
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 :
- As you probably guessed, I have to edit my views twice every time I need to change something. Quite frustrating.
Solution 2 : same technique with `include`
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 :
- Now my content is only defined once, that's better.
Drawbacks :
- I still need two files for one page.
- I have to use the same technique in Express on every route. I just moved my code repetition problem from Jade to Express. Snap.
Solution 3 : same as Solution 2 but fixing the code repetition problem.
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
- It solved the code repetition problem
Drawbacks
- I still need two files for one page.
- Defining my own
renderView
method feels a litle dirty. After all, I expect my template engine/framework to handle this for me.
Solution 4 : Moving the logic to Jade
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 :
- Now, I have only one file per page
- I don't have to repeat the
req.xhr
test in every route or in every view
Disadvantages :
- There is logic in my template. Not good
Summary
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 ?
Problem #2
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 :)