Skip to content

Support Dynamic Partial Blocks #9

Open
@jstewmon

Description

@jstewmon

Currently, there is no way of defining a dynamic partial with block syntax, so that a failover can be provided if the sub expression resolves to an undefined partial.

Why is this important?

When we use a dynamic partial block, the model-building code may not be aware of which partials are registered. For example, we may register partials with names that correspond to error codes, so that we can render an error-specific template in some cases, falling back to a default template. So, it would be convenient to supply a default template to use when the dynamic partial does not resolve to a registered partial.

I found that it is fairly trivial to support this behavior if the closeBlock helperName is optional:

diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js
index 3c1a38f..f6295d8 100644
--- a/lib/handlebars/compiler/helpers.js
+++ b/lib/handlebars/compiler/helpers.js
@@ -1,9 +1,11 @@
 import Exception from '../exception';
 
 function validateClose(open, close) {
-  close = close.path ? close.path.original : close;
+  if (close.hasOwnProperty('path')) {
+    close = close.path ? close.path.original : close.path;
+  }
 
-  if (open.path.original !== close) {
+  if (close && open.path.original !== close) {
     let errorNode = {loc: open.path.loc};
 
     throw new Exception(open.path.original + " doesn't match " + close, errorNode);
diff --git a/spec/partials.js b/spec/partials.js
index 266837d..7982677 100644
--- a/spec/partials.js
+++ b/spec/partials.js
@@ -19,6 +19,20 @@ describe('partials', function() {
     shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
     shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},, false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
   });
+
+  it('dynamic partials with failover', function() {
+    var string = 'Dudes: {{#dudes}}{{#> (partial)}}Anonymous {{/}}{{/dudes}}';
+    var partial = '{{name}} ({{url}}) ';
+    var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+    var helpers = {
+      partial: function() {
+        return 'missing';
+      }
+    };
+    shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Anonymous Anonymous ');
+    shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},, false], true, 'Dudes: Anonymous Anonymous ');
+  });
+
   it('failing dynamic partials', function() {
     var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
     var partial = '{{name}} ({{url}}) ';
diff --git a/src/handlebars.yy b/src/handlebars.yy
index ce06498..1df03cf 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -79,7 +79,7 @@ inverseChain
   ;
 
 closeBlock
-  : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
+  : OPEN_ENDBLOCK helperName? CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
   ;
 
 mustache

Of course, this solution allows closing any block with {{/}}, which would be inadvisable as a regular practice, but doesn't implicitly harm anything. validateClose could include an additional check to only allow the empty close when open.type === 'SubExpression', so that only a dynamic partial block is allowed to have an empty close (I think that's the only case when the open will be of type SubExpression).

Another option is to allow partialName in the closeBlock:

diff --git a/spec/partials.js b/spec/partials.js
index 266837d..4800d3f 100644
--- a/spec/partials.js
+++ b/spec/partials.js
@@ -19,6 +19,20 @@ describe('partials', function() {
     shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
     shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},, false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
   });
+
+  it('dynamic partials with failover', function() {
+    var string = 'Dudes: {{#dudes}}{{#> (partial)}}Anonymous {{/(partial)}}{{/dudes}}';
+    var partial = '{{name}} ({{url}}) ';
+    var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+    var helpers = {
+      partial: function() {
+        return 'missing';
+      }
+    };
+    shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Anonymous Anonymous ');
+    shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},, false], true, 'Dudes: Anonymous Anonymous ');
+  });
+
   it('failing dynamic partials', function() {
     var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
     var partial = '{{name}} ({{url}}) ';
diff --git a/src/handlebars.yy b/src/handlebars.yy
index ce06498..2d63945 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -79,7 +79,7 @@ inverseChain
   ;
 
 closeBlock
-  : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
+  : OPEN_ENDBLOCK partialName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
   ;
 
 mustache

I don't think this would be a very good solution in practice because dynamic partial expressions are rarely so trivial, so matching the sub expression in the openPartialBlock and closeBlock would be quite tedious.

I wanted to find a way to label an openPartialBlock containing a sub expression, but couldn't figure out a way of doing so that is clearly disambiguated from the existing partial syntaxes. I'd love to hear other syntactical suggestions, and I'd be happy to submit a PR if there consensus on a syntax.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions