ftlabs / fruitmachine

View rendering engine
MIT License
247 stars 18 forks source link

Can I use Mustache partials in fruitmachine module templates? #63

Closed callumlocke closed 10 years ago

callumlocke commented 10 years ago

I might be being dumb, but I can't see how I can do this currently... With Hogan, you have to pass in any needed partials into a template's render method, and I don't think that's possible with something like module.render().inject(el) since I don't have direct access to the template's own render method. I wondered if a fruitmachine module's own render method might accept a list of partials, but it looks like it doesn't take any arguments).

Or should I be using some other fruitmachine functionality to achieve the same thing as partials?

matthew-andrews commented 10 years ago

Yes - definitely. Every fruitmachine module we have in the web app is hooked up to a Hogan template. I've made a worked example web app that includes approximately how we use fruitmachine Hogan in the web app here (also includes our AppCache hacks):- https://github.com/matthew-andrews/web-app/.

The main idea is you plug the template method into fruitmachine modules in via the template key when defining the module. Eg: https://github.com/matthew-andrews/web-app/blob/master/client/views/article.js#L6

(Using the browserify-hogan transform: https://github.com/matthew-andrews/web-app/blob/master/GruntFile.js#L14)

callumlocke commented 10 years ago

Ah I'm not sure if we're talking about the same thing - I meant standard Mustache partials like this: {{> mypartial}}, where one template defers to another one and passes down the current context (spec). It looks like your demo app doesn't do partials in that way, but it seems you've got another approach which I will try and follow, thanks.

matthew-andrews commented 10 years ago

Ah right - yes you should be able to do that - it's internal to Hogan. fruitmachine shouldn't interfere with the internals of Hogan.

Not sure if you can see this from OSB but we do this occasionally for some really basic UI elements (eg. progress bars) in the templates we use within fruitmachine modules.

Eg: http://git.ak.ft.com/ftlabs/ft-app/blob/v42/lib/templates/shared/fruit/layout-i.ms#L7

callumlocke commented 10 years ago

I know fruitmachine doesn't interfere with Hogan internals, but the thing is, {{>foo}}-style partials need to be rendered in a unique way - when rendering the 'parent' template, you need to pass in a second argument in addition to the context data.

For example...

list.mustache:

<ul>
  {{#items}}
    {{> item}}
  {{/items}}
</ul>

item.mustache:

<li>{{name}}</li>

...then in your JS, after compiling both templates (as listTemplate and itemTemplate), you would render a list like this:

var renderedListHTML = listTemplate.render({
  name: 'John'
}, {item: itemTemplate});

(Sorry if you knew all that already; I didn't until yesterday.)

If you define a fruitmachine "list" component class and set its template to listTemplate, then fruitmachine doesn't know to pass in the partials at render time.

The obvious workaround (which took me way too long to think of) is to define your component with a custom template function, which itself calls the Hogan template's render method and passes in the partials, then returns the result... But since fruitmachine recommends Mustache/Hogan, maybe there should be an explicit way to pass in any needed partials when defining a component? If you think that would be good, I'll do a PR for that.

BTW, since you mentioned browserify-hogan... I'm a maintainer on that package, and you might want to know: I just published an update that changes its functionality - now when you require a template through that transform, you get the whole template object, not just its render method (so it's more in line with Hogan's official approach). Since fruitmachine accepts either an object or a function as a template, things will probably just continue working the same, but if you manually call your template anywhere, that will no longer work after updating.

matthew-andrews commented 10 years ago

I wasn't aware that was how you passed in extra data for partials - cool :-).

The line where we call the template method is here: https://github.com/ftlabs/fruitmachine/blob/master/lib/module/index.js#L497. I quite like that the requirements of the template option of fruitmachine modules are so basic and ignorant of any templating library specifics (just a function that takes a data object and outputs html) so your suggested workaround seems quite appealing to me. That said I think we would definitely consider any PR that makes passing data into partials easier - as long as the current ignorance of fruitmachine to Hogan is maintained.

As a possible way of making the workaround neater you could write a little transform method:

// pseudo code - not tested!!
module.exports = function(template, partialData) {
  return function(data) {
    return template.render.call(this, data, partialData);
  };
};

Then you just need to do:

var fruitmachine = require('fruitmachine');
var template = require('../../templates/partials/apple.html').render;
var itemTemplate = require('../../templates/partials/item-template.html').render;
var addPartialData = require('./partial-data-transform');

module.exports = fruitmachine.define({
  name: 'apple',
  template: addPartialData(template, { item: { itemTemplate } })
});

If I understand your notes correctly about the browserify transform should this:

var fruitmachine = require('fruitmachine');
var template = require('../../templates/partials/apple.html');

module.exports = fruitmachine.define({
  name: 'apple',
  template: template
});

Change to this?

var fruitmachine = require('fruitmachine');
var template = require('../../templates/partials/apple.html').render;

module.exports = fruitmachine.define({
  name: 'apple',
  template: template
});
callumlocke commented 10 years ago

That addPartialData thing is nice. And yes I completely agree about keeping FM ignorant of Hogan's specifics. Better to just put something in the docs about using partials (I will PR something).


re: browserify-transform: yes you've got it – except with fruitmachine you don't need to do that, because it doesn't mind if you pass in the whole object. (FM automatically detects whether it's a callable function or an object with a render method.)

But for example, in the addPartialData snippet you just wrote, then you'd need to change that to return template.render.call(...), or yeah just do require('apple.html').render as you suggested.

callumlocke commented 10 years ago

Oh I just double-checked that and found a bug - require('apple.html').render was broken, because a Hogan template's render method uses this internally to access prototype stuff provided by the Hogan runtime library. (And apparently pulling just a single method out of a require call like that causes that function to lose its original this context - this was news to me!).

I've just fixed that bug and republished the transform as v0.1.1. Now either approach works.

matthew-andrews commented 10 years ago

Great! Did you have any other ideas about how we might tweak the way template functions are called to make partials easier or were the above approaches acceptable? If so I'll try to find some time over the next few days to improve improve the docs.

callumlocke commented 10 years ago

I'm happy to keep using the above approaches. The only way I can think of to change FM would be for define to accept a partials object as well as a template object, but I think that would make FM a bit too Hogan-aware.

matthew-andrews commented 10 years ago

Closing this issue because an alternative solution was found.