Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 409
Waldo Jeffers Avatar asked Aug 05 '14 08:08

Waldo Jeffers


People also ask

What is partials in Express JS?

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') %>

How to set view engine as 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.


2 Answers

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.

like image 97
Segfault Avatar answered Oct 03 '22 18:10

Segfault


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.

like image 43
lmaix Avatar answered Oct 03 '22 19:10

lmaix