Is there a way to render just one file without rendering its parents? #1123

Closed
draivin opened this Issue May 4, 2012 · 14 comments

Comments

Projects
None yet
10 participants
@draivin

draivin commented May 4, 2012

On express 2.x I just served the content of the page to ajax requests by using { layout : false }, but I couldn't find something similar in the new version, since it doesn't support layouts.
Is there any alternative way I can accomplish this?

http.ServerResponse.prototype.view = function(file){
    if(this.req.query.ajax !== undefined){
        this.render(file, {layout: false});
    } else {
        this.render(file);        
    }
};
@tj

This comment has been minimized.

Show comment Hide comment
@tj

tj May 4, 2012

Member

it depends slightly on the engine. For example with Jade you may have a user/list.jade view for "GET /users":

for user in users
  include user/view

and user/view.jade:

h1= user.name
p= user.stuff

so then you can use that same template for the list, as well as res.render('user/show', { user: user })

Member

tj commented May 4, 2012

it depends slightly on the engine. For example with Jade you may have a user/list.jade view for "GET /users":

for user in users
  include user/view

and user/view.jade:

h1= user.name
p= user.stuff

so then you can use that same template for the list, as well as res.render('user/show', { user: user })

@draivin

This comment has been minimized.

Show comment Hide comment
@draivin

draivin May 5, 2012

I guess I haven't explained myself very well.

On the client side, I have a script that checks if the user's browser supports history.pushState, and if it does, I handle all the internal links through a script, and instead of going to the page, I make an ajax request to the address, with an additional GET parameter named ajax at the end. At the server side, I check for that parameter and serve just the content without the layout if it was present, or serve the full page if it was not(direct links or javascript disabled), and all of that seamless, with the only difference being in the function I used to serve the page(res.view instead of res.render).

So for example, if I did res.view('user/user_info') it would serve just the content if it was an ajax request, or the content with the layout if it was a normal request. But that was based around how easy it was to serve the content alone, and I can't find a way to make something like that since layouts are no longer supported.

draivin commented May 5, 2012

I guess I haven't explained myself very well.

On the client side, I have a script that checks if the user's browser supports history.pushState, and if it does, I handle all the internal links through a script, and instead of going to the page, I make an ajax request to the address, with an additional GET parameter named ajax at the end. At the server side, I check for that parameter and serve just the content without the layout if it was present, or serve the full page if it was not(direct links or javascript disabled), and all of that seamless, with the only difference being in the function I used to serve the page(res.view instead of res.render).

So for example, if I did res.view('user/user_info') it would serve just the content if it was an ajax request, or the content with the layout if it was a normal request. But that was based around how easy it was to serve the content alone, and I can't find a way to make something like that since layouts are no longer supported.

@tj

This comment has been minimized.

Show comment Hide comment
@tj

tj May 9, 2012

Member

gotcha. You could still do that by using conventions. Your res.view(name) name + "-with-layout" or something when it's not req.xhr. Jade supports multiple levels of inheritance now so it's not quite so easy to just have an on/off switch

Member

tj commented May 9, 2012

gotcha. You could still do that by using conventions. Your res.view(name) name + "-with-layout" or something when it's not req.xhr. Jade supports multiple levels of inheritance now so it's not quite so easy to just have an on/off switch

@calmdev

This comment has been minimized.

Show comment Hide comment
@calmdev

calmdev Aug 14, 2012

Did you ever find a good solution for this? Unfortunately, I'm facing the same exact issue.

calmdev commented Aug 14, 2012

Did you ever find a good solution for this? Unfortunately, I'm facing the same exact issue.

@nrako

This comment has been minimized.

Show comment Hide comment
@nrako

nrako Sep 29, 2012

@draivin are you trying to implement pjax à la Turbolinks way? I am very curious to know what you did come with to do that.

nrako commented Sep 29, 2012

@draivin are you trying to implement pjax à la Turbolinks way? I am very curious to know what you did come with to do that.

@kitwood

This comment has been minimized.

Show comment Hide comment
@kitwood

kitwood Oct 23, 2012

I worked out a way around this following some inspiration from @visionmedia.

  1. Given the express-pjax code, change the pjax.js to this:

    module.exports = function() {
      return function(req, res, next) {
    
        if (req.header('X-PJAX')) {
          req.pjax = true;
        }
    
        res.renderPjax = function(view, options, fn) {
          if (req.pjax) {
            view = 'pjax/' + view;
          }
    
          res.render(view, options, fn);
        };
    
        next();
      };
    };
    
  2. Then, in your Express 3.x-modified templates (modified from express-pjax-demo /views/aliens.jade), do something like:

    extends layout
    
    block mainContent
        include pjax/aliens
    
  3. In your layout.jade, make sure to replace the != body with block mainContent

  4. Then move/copy the original content of your template files (dinosaur.jade, aliens.jade, index.jade, etc.) into a new subdirectory "pjax" under "views":

    /public
    /routes
    /views
        /pjax
            aliens.jade
            dinosaurs.jade
            ...
        aliens.jade
        dinosaurs.jade
        ...
        layout.jade
    

This way, navigating directly to /aliens will utilize the 'extended' *.jade files, which in turn use the 'included' pjax/*.jade files, while the pjax request will skip to the pjax/*.jade files.

Hope that made sense...

kitwood commented Oct 23, 2012

I worked out a way around this following some inspiration from @visionmedia.

  1. Given the express-pjax code, change the pjax.js to this:

    module.exports = function() {
      return function(req, res, next) {
    
        if (req.header('X-PJAX')) {
          req.pjax = true;
        }
    
        res.renderPjax = function(view, options, fn) {
          if (req.pjax) {
            view = 'pjax/' + view;
          }
    
          res.render(view, options, fn);
        };
    
        next();
      };
    };
    
  2. Then, in your Express 3.x-modified templates (modified from express-pjax-demo /views/aliens.jade), do something like:

    extends layout
    
    block mainContent
        include pjax/aliens
    
  3. In your layout.jade, make sure to replace the != body with block mainContent

  4. Then move/copy the original content of your template files (dinosaur.jade, aliens.jade, index.jade, etc.) into a new subdirectory "pjax" under "views":

    /public
    /routes
    /views
        /pjax
            aliens.jade
            dinosaurs.jade
            ...
        aliens.jade
        dinosaurs.jade
        ...
        layout.jade
    

This way, navigating directly to /aliens will utilize the 'extended' *.jade files, which in turn use the 'included' pjax/*.jade files, while the pjax request will skip to the pjax/*.jade files.

Hope that made sense...

@draivin

This comment has been minimized.

Show comment Hide comment
@draivin

draivin Oct 27, 2012

I ended up using a completely different approach, but if someone still wants to do it like this, I found a somewhat hackish solution to accomplish this that does not involves creating more files:

  1. Open jade's parser.js file, located at jade/lib/parser.js and find this function in the file:
  parse: function(){
    var block = new nodes.Block, parser;
    block.line = this.line();

    while ('eos' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }

    if (parser = this.extending) {
      this.context(parser);
      var ast = parser.parse();
      this.context();
      // hoist mixins
      for (var name in this.mixins)
        ast.unshift(this.mixins[name]);
      return ast;
    }

    return block;
  }
  1. Replace it with this slightly modified version:
  parse: function(){
    var block = new nodes.Block, parser;
    block.line = this.line();

    while ('eos' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }

    //Edit starts here
    if (this.options.block) {    
      if (this.blocks[this.options.block]) {
        return this.blocks[this.options.block];
      } else {
        throw new Error('expected block "' + this.options.block + '", but it did not exist');
      }
    }
    //Edit ends here

    if (parser = this.extending) {
      this.context(parser);
      var ast = parser.parse();
      this.context();
      // hoist mixins
      for (var name in this.mixins)
        ast.unshift(this.mixins[name]);
      return ast;
    }

    return block;
  }
  1. Now if you pass an option named block to the parser, it will render only the named block, then you can do something like this:
    if (req.xhr) {
        res.render(file, {block: 'content', title: 'Express'});
    } else {
        res.render(file, {title: 'Express'});        
    }

And your content files can stay like they where before:

 extends layout

 block content
        p Welcome to #{title}

Be wary of the following:

  1. As the code adds a variant to the render code, the built-in jade cache becomes totally unreliable for files where you use the block option (Fix below) .
  2. If you try to render a single block, and the block does not exists, it will throw an error.

If you want the built-in jade cache to work in conjunction with the block option you should do the following:

  1. Open jade's jade.js located at jade/lib/jade.js and find the following function:
exports.render = function(str, options, fn){
  // swap args
  if ('function' == typeof options) {
    fn = options, options = {};
  }

  // cache requires .filename
  if (options.cache && !options.filename) {
    return fn(new Error('the "filename" option is required for caching'));
  }

  try {
    var path = options.filename;
    var tmpl = options.cache
      ? exports.cache[path] || (exports.cache[path] = exports.compile(str, options))
      : exports.compile(str, options);
    fn(null, tmpl(options));
  } catch (err) {
    fn(err);
  }
};
  1. Replace it with this slightly modified version:
exports.render = function(str, options, fn){
  // swap args
  if ('function' == typeof options) {
    fn = options, options = {};
  }

  // cache requires .filename
  if (options.cache && !options.filename) {
    return fn(new Error('the "filename" option is required for caching'));
  }

  try {
    var key = options.filename;

    if(options.cache && options.block)
      key += ':block#' + options.block;

    var tmpl = options.cache
      ? exports.cache[key] || (exports.cache[key] = exports.compile(str, options))
      : exports.compile(str, options);
    fn(null, tmpl(options));
  } catch (err) {
    fn(err);
  }
};

That's it :)

PS: I did not test the cache fix, but it should work.

draivin commented Oct 27, 2012

I ended up using a completely different approach, but if someone still wants to do it like this, I found a somewhat hackish solution to accomplish this that does not involves creating more files:

  1. Open jade's parser.js file, located at jade/lib/parser.js and find this function in the file:
  parse: function(){
    var block = new nodes.Block, parser;
    block.line = this.line();

    while ('eos' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }

    if (parser = this.extending) {
      this.context(parser);
      var ast = parser.parse();
      this.context();
      // hoist mixins
      for (var name in this.mixins)
        ast.unshift(this.mixins[name]);
      return ast;
    }

    return block;
  }
  1. Replace it with this slightly modified version:
  parse: function(){
    var block = new nodes.Block, parser;
    block.line = this.line();

    while ('eos' != this.peek().type) {
      if ('newline' == this.peek().type) {
        this.advance();
      } else {
        block.push(this.parseExpr());
      }
    }

    //Edit starts here
    if (this.options.block) {    
      if (this.blocks[this.options.block]) {
        return this.blocks[this.options.block];
      } else {
        throw new Error('expected block "' + this.options.block + '", but it did not exist');
      }
    }
    //Edit ends here

    if (parser = this.extending) {
      this.context(parser);
      var ast = parser.parse();
      this.context();
      // hoist mixins
      for (var name in this.mixins)
        ast.unshift(this.mixins[name]);
      return ast;
    }

    return block;
  }
  1. Now if you pass an option named block to the parser, it will render only the named block, then you can do something like this:
    if (req.xhr) {
        res.render(file, {block: 'content', title: 'Express'});
    } else {
        res.render(file, {title: 'Express'});        
    }

And your content files can stay like they where before:

 extends layout

 block content
        p Welcome to #{title}

Be wary of the following:

  1. As the code adds a variant to the render code, the built-in jade cache becomes totally unreliable for files where you use the block option (Fix below) .
  2. If you try to render a single block, and the block does not exists, it will throw an error.

If you want the built-in jade cache to work in conjunction with the block option you should do the following:

  1. Open jade's jade.js located at jade/lib/jade.js and find the following function:
exports.render = function(str, options, fn){
  // swap args
  if ('function' == typeof options) {
    fn = options, options = {};
  }

  // cache requires .filename
  if (options.cache && !options.filename) {
    return fn(new Error('the "filename" option is required for caching'));
  }

  try {
    var path = options.filename;
    var tmpl = options.cache
      ? exports.cache[path] || (exports.cache[path] = exports.compile(str, options))
      : exports.compile(str, options);
    fn(null, tmpl(options));
  } catch (err) {
    fn(err);
  }
};
  1. Replace it with this slightly modified version:
exports.render = function(str, options, fn){
  // swap args
  if ('function' == typeof options) {
    fn = options, options = {};
  }

  // cache requires .filename
  if (options.cache && !options.filename) {
    return fn(new Error('the "filename" option is required for caching'));
  }

  try {
    var key = options.filename;

    if(options.cache && options.block)
      key += ':block#' + options.block;

    var tmpl = options.cache
      ? exports.cache[key] || (exports.cache[key] = exports.compile(str, options))
      : exports.compile(str, options);
    fn(null, tmpl(options));
  } catch (err) {
    fn(err);
  }
};

That's it :)

PS: I did not test the cache fix, but it should work.

@draivin draivin closed this Oct 27, 2012

@optikfluffel optikfluffel referenced this issue in dakatsuka/express-pjax Mar 10, 2013

Open

Not working with express 3.0 #3

@inca

This comment has been minimized.

Show comment Hide comment
@inca

inca Jun 24, 2013

+1 to @draivin, I'd really like the "render-just-a-single-block" to be a part of the core library.

inca commented Jun 24, 2013

+1 to @draivin, I'd really like the "render-just-a-single-block" to be a part of the core library.

@Amberlamps

This comment has been minimized.

Show comment Hide comment
@Amberlamps

Amberlamps Nov 25, 2013

I am using Express 3.x and I get 200 HTTP codes when using your block fix, when I was expecting 304. Or am I not understanding Jade caching correctly?

I am using Express 3.x and I get 200 HTTP codes when using your block fix, when I was expecting 304. Or am I not understanding Jade caching correctly?

@draivin

This comment has been minimized.

Show comment Hide comment
@draivin

draivin Nov 25, 2013

Seeing as this was made over a year ago I doubt the code hasn't changed enough to still be compatible.

draivin commented Nov 25, 2013

Seeing as this was made over a year ago I doubt the code hasn't changed enough to still be compatible.

@hacksparrow

This comment has been minimized.

Show comment Hide comment
@hacksparrow

hacksparrow Nov 25, 2013

Member

Things have changed since the original question was posted. The view-to-be rendered decides whether it wants to inherit from a parent or not.

If you have a view, you can just do res.render(view_name) to send its content - whatever you put in it.

Member

hacksparrow commented Nov 25, 2013

Things have changed since the original question was posted. The view-to-be rendered decides whether it wants to inherit from a parent or not.

If you have a view, you can just do res.render(view_name) to send its content - whatever you put in it.

@EvanCarroll

This comment has been minimized.

Show comment Hide comment
@EvanCarroll

EvanCarroll Dec 24, 2013

@hacksparrow what does that have to do with anything? If view_name has extends view_parent it's still going to be wrapped. This feature request is so that this does not happen. That said, let me say that this duplicate view idea submitted by @draivin and others is ugly. Perhaps set a variable in a middleware, app.set( function(req,res,next) { res.locals.xhrreq = req.xhr; next(); } ) and then simply put the extends foo in a conditional in the view,

- if ( ! xhrreq ) {
  extends layout.tt
- }

@hacksparrow what does that have to do with anything? If view_name has extends view_parent it's still going to be wrapped. This feature request is so that this does not happen. That said, let me say that this duplicate view idea submitted by @draivin and others is ugly. Perhaps set a variable in a middleware, app.set( function(req,res,next) { res.locals.xhrreq = req.xhr; next(); } ) and then simply put the extends foo in a conditional in the view,

- if ( ! xhrreq ) {
  extends layout.tt
- }
@inca

This comment has been minimized.

Show comment Hide comment
@inca

inca Dec 25, 2013

I came up with a working solution by defining a common block named viewport for partial content. Since I prefer to “stack” layouts (basically, a layout.jade-per-directory), my outer layout looks like this:

if (xhr)
  block viewport
else
  doctype html
  html
    head
      // etc.
    body
      // etc.
      block content

Then the most specific layout (e.g. mgmt/users/layout.jade if we're going for /mgmt/users) looks like this:

extends ../layout

block content
  // Some content
  block viewport

Finally the mgmt/users/index.jade looks like this:

extends layout

block viewport
  h2 Users
  // Show them

Now if the request is XHR, then only the payload markup is rendered, and the full HTML document otherwise.

inca commented Dec 25, 2013

I came up with a working solution by defining a common block named viewport for partial content. Since I prefer to “stack” layouts (basically, a layout.jade-per-directory), my outer layout looks like this:

if (xhr)
  block viewport
else
  doctype html
  html
    head
      // etc.
    body
      // etc.
      block content

Then the most specific layout (e.g. mgmt/users/layout.jade if we're going for /mgmt/users) looks like this:

extends ../layout

block content
  // Some content
  block viewport

Finally the mgmt/users/index.jade looks like this:

extends layout

block viewport
  h2 Users
  // Show them

Now if the request is XHR, then only the payload markup is rendered, and the full HTML document otherwise.

@vendethiel

This comment has been minimized.

Show comment Hide comment
@vendethiel

vendethiel Dec 25, 2013

seems like a good one @inca

seems like a good one @inca

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment