expressjs / express

Fast, unopinionated, minimalist web framework for node.
https://expressjs.com
MIT License
64.35k stars 14.63k forks source link

Transform context passed to template immediately before rendering #2802

Closed rightaway closed 8 years ago

rightaway commented 8 years ago

How can I transform the context that's passed to the template immediately before rendering? For example, there should be a way to add/remove/change objects that are passed to the template as context, while having access to req and res.

I can't tell whether I should be looking at app.engine() or app.set('view') (which isn't documented in http://expressjs.com/4x/api.html#app.settings.table so I can't tell exactly what it's for) or somewhere else.

So far I have basic integration set up using Nunjucks templating, it looks like

class NunjucksView {

  constructor(name, opts) {
    this.env = nunjucks.configure('./templates');
    this.name = name;
    this.path = name;
  }

  render(opts, cb) {
    this.env.render(this.name, opts, cb);
  }

}

app.set('view', NunjucksView);

But I don't see any req or res objects or how to modify the context passed to the template.

rightaway commented 8 years ago

If the View.render(opts, cb) were passed req and res it would make for the perfect place to hook into for any processing that needs to happen after all middleware and route code has run. What do you think?

The kinds of context modifications I'm talking about are general and apply to multiple routes and templates (if it weren't then res.locals could just be modified in that individual route).

Examples include clearing flash messages right after the template has displayed them (for example https://github.com/expressjs/flash doesn't actually do that!), internationalizing strings that haven't already been internationalized before being passed into the template layer (for example caused by using one of many libraries with poor i18n support), or otherwise massaging the context to a format more suitable for display.

Rather than delegate all of these responsibilites to each route, it's better to have the ability to do it all in one place.

Of course I could create a wrapper for res.render and call the wrapper instead of res.render in all the routes, but then all developers working on the application need to always remember to use the wrapper. It would be better to have this context modification capability happening beneath the res.render layer. Create your View once and all calls to res.render will then have exactly the context you want available to templates.

dougwilson commented 8 years ago

Of course I could create a wrapper for res.render and call the wrapper instead of res.render in all the routes, but then all developers working on the application need to always remember to use the wrapper.

Why not simply modify the render function itself? It's the whole reason we export the prototypes:

var express = require('express');

express.response.render = function render() {
  // define your render hook here
  // req = this.req
  // res = this
}
dougwilson commented 8 years ago

Otherwise, please send us a PR, but keep in mind that the View class is purposely disconnected from HTTP request/responses, as part of separation of concerns, so we wouldn't accept adding those values into that class.

rightaway commented 8 years ago

Modifying the prototype would be fine. In the custom render function, since I"m overriding the original render function what's the way to call the original render function after my hook has prepared the context?

dougwilson commented 8 years ago

Either a simple manual AOP around, or use a AOP library.

var express = require('express');

around(express.response, 'render', function (inner, args) {
  // do something to the args/contexts
  return inner.apply(this, args);
});

function around(obj, prop, func) {
  var inner = obj[prop];
  obj[prop] = outer;

  function outer() {
    func.apply(this, [inner, Array.prototype.slice.call(arguments)]);
  };
}