Skip to content
This repository

Dynamic partials and partial collections #242

Open
wants to merge 1 commit into from

4 participants

Jamie Hill Justin Hileman Devin Rhode Matthew Mirande
Jamie Hill

Hi,

This is a small patch that brings great flexibility when dealing with collections that are going to be rendered using partials. Please see the new documentation below:

With this patch, the following:

View:

{
  items: [
    { type: 'image', url: 'Some URL', is_image: true },
    { type: 'text', content: 'Some text', is_text: true }
  ]
}

Template:

base.mustache
{{#items}}
  {{#is_text}}
    {{>text}}
  {{/is_text}}
  {{#is_image}}
    {{>image}}
  {{/is_image?}}
{{/items}}

text.mustache
<p>{{content}}</p>

image.mustache
<p><img src="{{url}}"/></p>

...can be replaced with:

View:

{
  items: [
    { partial: 'text', content: 'Some text' },
    { partial: 'image', url: 'Some URL' }
  ]
}

Template:

base.mustache
{{@items}}

text.mustache
<p>{{content}}</p>

image.mustache
<p><img src="{{url}}"/></p>

...or if more fine grain control is needed:

Template:

base.mustache
{{#items}}
<p>{{>.}}</p>
{{^items}}

text.mustache
{{content}}

image.mustache
<img src="{{url}}"/>

Patch includes tests and documentation, addressing a recurring problem when using Mustache templates e.g. http://stackoverflow.com/questions/2932679/dynamically-render-partial-templates-using-mustache and #241.

I really hope the pull request is accepted as I am already utilising this in a couple of projects and it's a joy to use. I will look at implementing in the Ruby version of Mustache also if accepted.

Kind regards,

Jamie

Justin Hileman

Things like this should probably be taken up with the spec first, then added to individual implementations:

https://github.com/mustache/spec

Jamie Hill

Just created ticket here: mustache/spec#54

I do however think that it would be worth applying this patch anyway so that people can see just how useful it is. Think vendor prefixes in browsers, that's how template inheritance made it into hogan.js.

Justin Hileman

Vendor prefixes are great, but the analog in Mustache is a {{%PRAGMA}} tag. If it's a non-spec feature, it should be off-by-default and explicitly enabled via a toggle. Several great things came into the spec this way (e.g. dot notation, implicit iterators).

Justin Hileman

In fact, there's a "filters" feature which will prolly go out in the next Mustache.php release that does this too:

bobthecow/mustache.php#102

We don't need to stop implementing awesome new things, we just need to be spec-compliant by default and guard the new features with a {{%PRAGMA}} tag :)

Jamie Hill

I'm not sure how I'd implement pragma's in mustache.js ...I will investigate. In the meantime, what do you/others think of this patch?

I urge you to give it a try as I saw instantly the benefits in my projects.

Jamie Hill

I'm thinking we could do with some way of plugging in language features, that way the plugins (middleware) can be used and assessed as a plugin even if they don't make it into the core library. Something like:

Mustache.Renderer.register('@', function(name, context, options) {
  // My custom tag functionality

  return 'the output';
});

Not sure how to go about extend and existing symbol's functionality but something like before/after callbacks could work.

Devin Rhode

Your syntax:

base.mustache
{{#items}}
<p>{{>.}}</p>
{{^items}}

and

base.mustache
{{@items}}

Assumes there is a key named 'partial' and then looks up that partial. The idea of dynamic partials is great, but I don't know about assuming a certain key to be present is the most developer friendly approach and the easiest to understand quickly. It could instead by {{>.partialName}} which, when the lookup for the literal key '.partialName' fails, it resolves .partialName to 'text' and looks that up.

This is interesting but I agree with bob that it should be off by default but have an option to turn it on. Further, the {{%PRAGMA}} idea should probably be in the spec itself... @bobthecow, would you agree?

Devin Rhode

@thelucid In regards to:

Mustache.Renderer.register('@', function(name, context, options) {
  // My custom tag functionality

  return 'the output';
});

I don't know how you could think of something so terrible...

No I like the idea, I need to consider the implementation more, but I'd be interested in hearing what others think.

Jamie Hill

The reason I went with assuming a partial key is that I like the whole convention over configuration approach.

I am using this in production and the {{@some_collection}} syntax really cleans up templates, it basically says to me at a glance "render a bunch of objects with their own partials":

{
  articles: [
    { partial: 'basic_article', title: 'Title A', content: 'Some text' },
    { partial: 'image_article', title: 'Title B', src: 'image.jpg' }
  ]
}
base.mustache
<div class="articles">
{{@articles}}
</div>

basic_article.mustache
<h1>{{title}}</h1>
<p>{{content}}</p>

image_article.mustache
<h1>{{title}}</h1>
<p><img src="{{src}}"/ ></p>

I guess a common ground would be to use {{@collection}} as I have outlined and the longhand could either be:

Assuming a partial key:

{{#items}}
<p>{{>.}}</p>
{{^items}}

A partial based on my_partial_key:

{{#items}}
<p>{{>.my_partial_key}}</p>
{{^items}}

I'm not sure how I'd implement the {{%PRAGMA}} thing cleanly.

I'd arguably be more interested in the Mustache.Renderer.register functionality as it would allow app specific customisation without hacking Mustache itself. Not sure how you'd use this to add additional functionality to an existing symbol.

Jamie Hill

I would appreciate peoples opinions over at mustache/spec#54 if you get a sec as there have been a couple of suggestions that highlight further why a clean solution to "dynamic partials" is needed... namely a pretty ugly workaround using functions that return html (mustache/spec#54 (comment)).

Matthew Mirande

annnnd i think i'm all caught up on the history of this one (>_<)

so... what's the verdict on this dynamic partials stuff? it sure would be helpful to me.

at the least, the lamba-based approach shown here mustache/spec#54 (comment) and here #304 (comment)

Jamie Hill

@busticated I came up with a Ruby solution that I am happy with in my Mustache implementation (https://github.com/thelucid/tache) which allows for this type of behaviour without a change to the spec but not sure how it will translate to the Javascript version. See the tests starting with ~ for how it's achieved: https://github.com/thelucid/tache/tree/master/test/fixtures

Jamie Hill thelucid referenced this pull request April 17, 2014
Closed

Dynamic Partials #241

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

Showing 1 unique commit by 1 author.

Aug 06, 2012
Jamie Hill Dynamic partials and partial collections 472773f
This page is out of date. Refresh to see the latest.
76  README.md
Source Rendered
@@ -328,6 +328,82 @@ In mustache.js an object of partials may be passed as the third argument to
328 328
 `Mustache.render`. The object should be keyed by the name of the partial, and
329 329
 its value should be the partial text.
330 330
 
  331
+### Dynamic Partials
  332
+
  333
+It is quite common to want to render a partial for each item in a collection.
  334
+Sometimes this will be a common partial but often it will be dependent on each
  335
+item in the collection.
  336
+
  337
+The implicit partial notation of `{{>.}}` renders a partial who's name comes
  338
+from the `partial` key of the current item. The current item becomes the
  339
+context of the partial.
  340
+
  341
+View:
  342
+
  343
+    {
  344
+      "beatles": [
  345
+        { "name": "John", "partial": "dead" },
  346
+        { "name": "Paul", "partial": "alive" },
  347
+        { "name": "George", "partial": "dead" },
  348
+        { "name": "Ringo", "partial": "alive" }
  349
+      ]
  350
+    }
  351
+
  352
+Template:
  353
+
  354
+    base.mustache
  355
+    {{#beatles}}
  356
+    {{>.}}
  357
+    {{/beatles}}
  358
+    
  359
+    alive.mustache
  360
+    * Keep it up {{name}}
  361
+    
  362
+    dead.mustache
  363
+    * Rest in peace {{name}}
  364
+
  365
+Output:
  366
+
  367
+    * Rest in peace John
  368
+    * Keep it up Paul
  369
+    * Rest in peace George
  370
+    * Keep it up Ringo
  371
+
  372
+#### Partial Collections
  373
+    
  374
+As this is such a common pattern, there is a shortcut that renders the named
  375
+partial for each item in a collection. The same example can therefore also be
  376
+accomplished with the following:
  377
+
  378
+View:
  379
+
  380
+    {
  381
+      "beatles": [
  382
+        { "name": "John", "partial": "dead" },
  383
+        { "name": "Paul", "partial": "alive" },
  384
+        { "name": "George", "partial": "dead" },
  385
+        { "name": "Ringo", "partial": "alive" }
  386
+      ]
  387
+    }
  388
+
  389
+Template:
  390
+
  391
+    base.mustache
  392
+    {{@beatles}}
  393
+    
  394
+    alive.mustache
  395
+    * Keep it up {{name}}
  396
+    
  397
+    dead.mustache
  398
+    * Rest in peace {{name}}
  399
+
  400
+Output:
  401
+
  402
+    * Rest in peace John
  403
+    * Keep it up Paul
  404
+    * Rest in peace George
  405
+    * Keep it up Ringo
  406
+
331 407
 ### Set Delimiter
332 408
 
333 409
 Set Delimiter tags start with an equals sign and change the tag delimiters from
24  mustache.js
@@ -48,7 +48,7 @@ var Mustache;
48 48
   var nonSpaceRe = /\S/;
49 49
   var eqRe = /\s*=/;
50 50
   var curlyRe = /\s*\}/;
51  
-  var tagRe = /#|\^|\/|>|\{|&|=|!/;
  51
+  var tagRe = /@|#|\^|\/|>|\{|&|=|!/;
52 52
 
53 53
   // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
54 54
   // See https://github.com/janl/mustache.js/issues/189
@@ -300,7 +300,10 @@ var Mustache;
300 300
   };
301 301
 
302 302
   Renderer.prototype._partial = function (name, context) {
303  
-    var fn = this._partialCache[name];
  303
+    // If the partial name is a dot, render based on the 'partial' key of
  304
+    // the current context, otherwise use default behaviour.
  305
+    var cache = this._partialCache,
  306
+        fn = (name === '.' ? cache[context.lookup("partial")] : cache[name]);
304 307
 
305 308
     if (fn) {
306 309
       return fn(context);
@@ -309,6 +312,20 @@ var Mustache;
309 312
     return "";
310 313
   };
311 314
 
  315
+  Renderer.prototype._partialCollection = function (name, context) {
  316
+    var value = context.lookup(name), buffer = "";
  317
+
  318
+    // If we have an array, render the partial defined by the 'partial'
  319
+    // key for each item in the array.
  320
+    if (isArray(value)) {
  321
+      for (var i = 0, len = value.length; i < len; ++i) {
  322
+        buffer += this._partial('.', context.push(value[i]));
  323
+      }
  324
+    }
  325
+
  326
+    return buffer;
  327
+  };
  328
+
312 329
   Renderer.prototype._name = function (name, context, escape) {
313 330
     var value = context.lookup(name);
314 331
 
@@ -355,6 +372,9 @@ var Mustache;
355 372
       case ">":
356 373
         body.push("r._partial(" + quote(token.value) + ", c)");
357 374
         break;
  375
+      case "@":
  376
+        body.push("r._partialCollection(" + quote(token.value) + ", c)");
  377
+        break;
358 378
       case "text":
359 379
         body.push(quote(token.value));
360 380
         break;
6  test/_files/partial_dynamic_collection.js
... ...
@@ -0,0 +1,6 @@
  1
+({
  2
+  items: [
  3
+    { content: 'Hello', partial: 'partial' },
  4
+    { content: 'Hello', partial: 'partial2' }
  5
+  ]
  6
+})
1  test/_files/partial_dynamic_collection.mustache
... ...
@@ -0,0 +1 @@
  1
+{{@items}}
1  test/_files/partial_dynamic_collection.partial
... ...
@@ -0,0 +1 @@
  1
+I am partial 1
1  test/_files/partial_dynamic_collection.partial2
... ...
@@ -0,0 +1 @@
  1
+I am partial 2
2  test/_files/partial_dynamic_collection.txt
... ...
@@ -0,0 +1,2 @@
  1
+I am partial 1
  2
+I am partial 2
7  test/_files/partial_dynamic_collection_implicit.js
... ...
@@ -0,0 +1,7 @@
  1
+({
  2
+  title: 'Dynamic partial collection',
  3
+  items: [
  4
+    { content: 'Hello', partial: 'partial' },
  5
+    { content: 'Hello', partial: 'partial2' }
  6
+  ]
  7
+})
7  test/_files/partial_dynamic_collection_implicit.mustache
... ...
@@ -0,0 +1,7 @@
  1
+Header
  2
+{{#items}}
  3
+Before
  4
+{{>.}}
  5
+After
  6
+{{/items}}
  7
+Footer
1  test/_files/partial_dynamic_collection_implicit.partial
... ...
@@ -0,0 +1 @@
  1
+I am partial 1
1  test/_files/partial_dynamic_collection_implicit.partial2
... ...
@@ -0,0 +1 @@
  1
+I am partial 2
8  test/_files/partial_dynamic_collection_implicit.txt
... ...
@@ -0,0 +1,8 @@
  1
+Header
  2
+Before
  3
+I am partial 1
  4
+After
  5
+Before
  6
+I am partial 2
  7
+After
  8
+Footer
4  test/parse_test.js
@@ -49,7 +49,9 @@ var expectations = {
49 49
   "{{#a}}{{/a}}hi{{#b}}{{/b}}\n"            : [ { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'hi' }, { type: '#', value: 'b', tokens: [] }, { type: 'text', value: '\n' } ],
50 50
   "{{a}}\n{{b}}\n\n{{#c}}\n{{/c}}\n"        : [ { type: 'name', value: 'a' }, { type: 'text', value: '\n' }, { type: 'name', value: 'b' }, { type: 'text', value: '\n\n' }, { type: '#', value: 'c', tokens: [] } ],
51 51
   "{{#foo}}\n  {{#a}}\n    {{b}}\n  {{/a}}\n{{/foo}}\n"
52  
-                                            : [ { type: "#", value: "foo", tokens: [ { type: "#", value: "a", tokens: [ { type: "text", value: "    " }, { type: "name", value: "b" }, { type: "text", value: "\n" } ] } ] } ]
  52
+                                            : [ { type: "#", value: "foo", tokens: [ { type: "#", value: "a", tokens: [ { type: "text", value: "    " }, { type: "name", value: "b" }, { type: "text", value: "\n" } ] } ] } ],
  53
+  "{{>.}}"                                  : [ { type: '>', value: '.' } ],
  54
+  "{{@collection}}"                         : [ { type: '@', value: 'collection' } ]
53 55
 };
54 56
 
55 57
 var spec = {};
11  test/render_test.js
@@ -43,13 +43,18 @@ testNames.forEach(function (testName) {
43 43
   var template = getContents(testName, "mustache");
44 44
   var expect = getContents(testName, "txt");
45 45
   var partial = getContents(testName, "partial");
  46
+  var partial2 = getContents(testName, "partial2");
46 47
 
47 48
   spec["knows how to render " + testName] = function () {
48 49
     Mustache.clearCache();
49 50
 
50  
-    var output;
51  
-    if (partial) {
52  
-      output = Mustache.render(template, view, {partial: partial});
  51
+    var output, partials = {};
  52
+  
  53
+    if (partial) { partials['partial'] = partial; }
  54
+    if (partial2) { partials['partial2'] = partial2; }
  55
+
  56
+    if (partial || partial2) {
  57
+      output = Mustache.render(template, view, partials);
53 58
     } else {
54 59
       output = Mustache.render(template, view);
55 60
     }
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.