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 :)
Partials are basically just views that are designed to be used from within other views. They are particularly useful for reusing the same markup between different views, layouts, and even other partials. <%- partial('./partials/navbar.ejs') %>
Step 1 — Configuring with server. var express = require('express'); var app = express(); // set the view engine to ejs app. set('view engine', 'ejs'); // use res. render to load up an ejs view file // index page app. get('/', function(req, res) { res.
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.
Not sure if it's really going to help you but I solved the same problem with Express and ejs template.
my folder:
my app.js
app.get('/page', function(req, res) { if (req.xhr) { res.render('page',{ajax:1}); } else { res.render('page',{ajax:0}); } });
my page.ejs
<% if (ajax == 0) { %> <% include header %> <% } %> content <% if (ajax == 0) { %> <% include footer %><% } %>
very simple but works perfect.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With