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

3

39

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

Hyperthermia answered 5/8, 2014 at 8:24 Comment(4)
Have you figured out how to load javascript files after the partials are rendered? I'm having a similar issue now.Aleen
I wish i could +10 this questionGlennglenna
I wish i could upvote this more. currently im trying to load css and js files after i add the partial. my workaround sucks but i am using the res.render callback to convert jade template to string then send the html and the references to the other files in an object which i then handle with javascript. I'm sure this is a horrible way to do it however I dont like link tags in my bodyMaclean
Hey there, thanks for the replies. I reported a bug on Jade's git repo quite a long time ago (which, in fact, wasn't a real bug). The issue I had is not related, but check the last two posts on the topic. Forbes Lindesay provided suggestions regarding our problem.Hyperthermia
G
3

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.

Glennglenna answered 11/11, 2014 at 1:15 Comment(2)
That's an interesting answser @Segfault, thanks. I went this solution #4 for my app. Regarding the second problem, good suggestions, though I'll have to disagree with you for the events part. The events can be bound to the document (event delegation...), or to the window (resize, that's a terrible example, but you get the idea).Hyperthermia
yes, good point @WaldoJeffers you still have to worry about the events a little bit I suppose. I think that's hard to solve for the general case...Glennglenna
S
1

Not sure if it's really going to help you but I solved the same problem with Express and ejs template.

my folder:

  • app.js
  • views/header.ejs
  • views/page.ejs
  • views/footer.ejs

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.

Sayles answered 6/5, 2015 at 1:37 Comment(0)
C
0

There are several ways to do what you asked, but all of them are bad architecturally. You may not notice that now, but it's will be worse and worse with time.

Sounds weird at this point, but you don't really want to transmit you JS, CSS via AJAX.

What you want is to be able to get the markup via AJAX and to do some Javascript after that. Also, you want to be flexible and rational while doing this. Scaleability is important as well.

The common way is:

  1. Preload all the js files could be used at the particular page and its AJAX requests handlers. Use browserify if your afraid of potential names conflicts or script sideeffects. Load them asynchronously (script async) to not make user wait.

  2. Develop an architecture that enables you to do, basically, this at the client: loadPageAjax('page_a').then(...).catch(...). The point is, since you have pre-downloaded all the JS modules, to run you AJAX-page related code at the caller of the request and not at the callee.

So, your Solution #4 is nearly good. Just use it to fetch the HTML, but not scripts.

This technique gets your goal done without building some obscure architecture, but using the single robust goal-limited way.

Will happy to answer any further questions and convince you not to do what you are going to.

Cacomistle answered 7/12, 2015 at 17:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.