How to properly render partial views, and load JavaScript files in AJAX using Express/Jade?

Waldo Jeffers picture Waldo Jeffers · Aug 5, 2014 · Viewed 15.8k times · Source

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 :)

Answer

Segfault picture Segfault · Nov 11, 2014

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.