Skip to content
This repository

Access to root context in partials and helpers #392

Merged
merged 1 commit into from 3 months ago
Dominykas Blyžė

There is already a related bug about getting parent context inside a partial called from within a block helper: issue #182. However, tracking parent context is probably not enough - add in another block helper (e.g. an if inside an each) and things quickly get out of hand.

The same story with helpers - it's easy enough to get current context via this, it's easy enough to pass in parent context via ".." - but once again, very quickly it becomes "../../../.." There is a hack proposed in issue #245 but I had mixed fortune with it.

The above two problems seriously affect code re-use - one must always keep track of context and nesting. There are pull requests for parentContext by introducing helpers and passing explicit parent context. While I find that useful, I also need the global context - just a couple of sample use cases: localization (must know which language to use), changing some item layout/details based on user status (essentially access check, although some might argue this is "logic" and should thus not be done inside a template).

It would be great to have access to global/root context at all times - inside an {{#each}}, inside an {{#if}}, inside any helper that I create myself and naturally inside any partials that may be nested inside block helpers.

Dominykas Blyžė

This is just one example of how badly "../" can get out of hands: http://jsfiddle.net/gsVLy/1/

I realize this is probably correct behavior - there's no "rootItem" inside the each, so "../rootItem" does not exist inside the if inside the each, but if one tried to add a nested loop, and an if in there... you get the point.

brikkelt

Another reason to support root access might be Handlebars' claim that 'Mustache templates are compatible with Handlebars, so you can take a Mustache template, import it into Handlebars'. The Mustache enginges for both php and js seem to support access to root context (see http://jsfiddle.net/brikkelt/A3gqF/ for js version), so one might expect Handlebars to do so as well.

However, the strongest reasons for support are the use cases you describe above + the pain of keeping track of parent depth.

+1!

Ash Heskes

Mustache.js has a nice way of dealing with this. It essentially looks for the named property in the current context, if it can't find it, it will traverse back up the context tree all the way to the root checking each parent context for the named property, and uses the first one it finds.

e.g.

Template:

<ul>
{{ #items }}
    <li>Item {{.}} for {{user}}</li>
{{ /items }}
</ul>

Context:

{
    items: [1,2,3,4,5],
    user: 'Ash'
}

Output:

<ul>
    <li>Item 1 for Ash</li>
    <li>Item 2 for Ash</li>
    <li>Item 3 for Ash</li>
    <li>Item 4 for Ash</li>
    <li>Item 5 for Ash</li>
</ul>   

I think this would be a great addition to Handlebars and would bring it more inline with Mustache.js

Of course in the case where you are looking a property on the root which shares a name with a property somewhere in your context tree it will only return the closest property to you.

It might be useful to have a special keyword tag e.g {{ root }}. That always references the root Object. That way you can call out the {{ root.someProperty.SomeChild }} anywhere in your template. similarly using it in a section will switch the context of that section back to the root context.

e.g.

{{# outerProp }}
    {{# innerProp }}
        {{#root }}
            {{# outerProp.innerProp }}
                {{.}}
            {{/ outerProp.innerProp }}
        {{#root}}
    {{/ innerProp }}
{{/outerProp}}

Although I feel like this would be a rarer case than the first solution, which comes in handy quite a lot. Where as I have yet to find a situation where I have nested properties with the same names.

William Coates

I opened a pull request which makes it simple to access the root context in custom helpers:

#525

Jon Schlinkert

Yeah we just had an issue on Assemble related to this, assemble/assemble#164.

Charles Lavery clavery referenced this pull request in clavery/grunt-generator
Open

Retrieving global options from helpers.js #6

Snugug
Snugug commented

:+1: for this as well. It's recently bit me not being able to access root.

Simon Donaldson

:thumbsup: I would like to request this too. I have an each statement which iterates over a 2D array and using "../" doesn't work. I have a nested if statement so have tried "../../" and that too doesn't work. If there isn't performance overhead to adding a root keyword then it's a quick win.

Jon Schlinkert

Would love to see some kind of acknowledgement of this issue. It has been an ongoing problem for us with assemble.

Kevin Decker
Collaborator

This is a non-trivial implementation that none of the core team has had a time to look at. It has not been closed as it is something that we would like to do. Pull requests are welcome.

Kevin Decker
Collaborator

Note that there are many issues to consider with this such as arbitrary nesting vs. blanket root access vs. work-around such as using @ data var (as well as the similar issue of how/if data vars should support traversal.

Jon Schlinkert

Note that there are many issues to consider with this such as arbitrary nesting vs. blanket root access vs. work-around such as using @ data var

@kpdecker perhaps you could put an issue up with some bullets to discuss and socialize with the community? then we can try to systematically attack the problem and gain consensus on each point.

Kevin Decker
Collaborator

@jonschlinkert working through all of the issues is one of the major tasks here. I'll try to prioritize this higher but as a volunteer on an OSS project with only so many hours in the day its tough.

Jon Schlinkert

:+1: I hear you there

Sergei Dorogin

I can see two points here:

  • a feature for easy access root context in any nested helper/block/etc - this would be a very helpful, because traversing with "../" is error-prone.
  • issues with current behaviors of block helpers - i.e. when we have to add ".." to go out current context of ANY block helper. So N nested "if will create N nested contexts. Such behavior was very confusing for me when I encountered with it even after more than a year of using HB. I really didn't understand that even "if creates nested context. I even created a bug #601 which makes no sense indeed (BTW: it's very long way to build and run tests for HB because of need to install node, ruby, ruby devtools (!), rake and other gems). So I'd say that this's a bug (even if it's by design). Wouldn't it make sense to not nest a block's context if it hasn't changed? Not every helper can change context.
Jon Schlinkert jonschlinkert referenced this pull request in assemble/handlebars-helpers
Closed

Get page variable in nested block helpers #62

Jon Schlinkert

@kpdecker, could subexpressions be used to achieve this? Looks like it should might, but I haven't had a chance to test. thanks!

Kevin Decker
Collaborator

@jonschlinkert strawman? I think the simplest solution here is to just add @root variable to the data object but I'm hesitant to do that until the depthed question is answered sufficiently.

Jon Schlinkert

Strawman implies that I was putting forth some kind of argument, which I'm assuming is a result of me using the word "should" instead of "might". If that's the case, sorry for the confusion. I wasn't stating that "subexpressions ought to be used to achieve this", just asking "if they could be".

Also, now that I've looked more closely at subexpressions I think my initial (incorrect) interpretation was the inverse of how they actually work. Regardless, the feature still looks awesome, a great addition to Handlebars. Anyway, sorry about the confusion.

Kevin Decker
Collaborator

@jonschlinkert no worries. I think something like @root.foo or similar is going to be the best route for this, which can actually be implemented outside of the library itself it people want to play around with that.

Brian Woodward

@kpdecker we have this pending PR on assemble-handlebars, and I'm wondering if this looks like it could solve the issue.

I think it would allow doing... {{root.foo}} from any place, but I haven't had a chance to test it yet.

Jon Schlinkert

@kpdecker I know you're super busy, but would you mind just glancing at the PR to give us your insight as to whether or not this is a good approach? nothing detailed.

Kevin Decker
Collaborator

@doowb @jonschlinkert Why not just do @root.foo? I'm not a fan of that solution as it could conflict with context variables named root and creates a lookup that could surprise people if they don't know to expect this.

Jon Schlinkert

@kpdecker makes sense thanks. so just use createFrame?

Jon Schlinkert

ah, nice. I was actually asking if createFrame is what I should use, I need to take more time on my replies! sorry

Kevin Decker kpdecker merged commit cb80f46 into from
Kevin Decker kpdecker closed this
Kevin Decker kpdecker deleted the branch
Eric Ferraiuolo

Sweet! Glad to see this merged.

Mike Griffin

Can somebody please show us how the API works?

If it's @root then how do we use it to access data?

I'm hoping it's {{root.SomeData}} ?

jessehouchins

Does this get passed down to partials somehow?

Kevin Decker
Collaborator

@BrewDawg {{@root.someData}} see the tests.
@jessehouchins it should. If it's not I would consider that a bug

Brian Woodward

@kpdecker Thanks for merging this. I'm glad to see this in the main branch instead of us trying to do something that would cause conflicts.That's exactly why I was asking for your advice on the other PR.

Jesper Hills

@kpdecker Good stuff. Do you have an estimate on when this might make an appearance in the next npm release for those of us using grunt?

jezell

+1 this is awesome. push it!

Kevin Decker
Collaborator

Released in v2.0.0-alpha.1

Jesper Hills

Lovely!

jezell

yay!

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.

Jan 15, 2014
Kevin Decker Expose the initial context via @root f650b0d
This page is out of date. Refresh to see the latest.
2  lib/handlebars/compiler/javascript-compiler.js
@@ -121,7 +121,7 @@ JavaScriptCompiler.prototype = {
121 121
 
122 122
       var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
123 123
       if (this.environment.usePartial) { copies = copies + " partials = this.merge(partials, " + namespace + ".partials);"; }
124  
-      if (this.options.data) { copies = copies + " data = data || {};"; }
  124
+      if (this.options.data) { copies = copies + " data = this.initData(depth0, data);"; }
125 125
       out.push(copies);
126 126
     } else {
127 127
       out.push('');
9  lib/handlebars/runtime.js
... ...
@@ -1,6 +1,6 @@
1 1
 module Utils from "./utils";
2 2
 import Exception from "./exception";
3  
-import { COMPILER_REVISION, REVISION_CHANGES } from "./base";
  3
+import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from "./base";
4 4
 
5 5
 export function checkRevision(compilerInfo) {
6 6
   var compilerRevision = compilerInfo && compilerInfo[0] || 1,
@@ -56,6 +56,13 @@ export function template(templateSpec, env) {
56 56
       }
57 57
       return programWrapper;
58 58
     },
  59
+    initData: function(context, data) {
  60
+      data = data ? createFrame(data) : {};
  61
+      if (!('root' in data)) {
  62
+        data.root = context;
  63
+      }
  64
+      return data;
  65
+    },
59 66
     merge: function(param, common) {
60 67
       var ret = param || common;
61 68
 
15  spec/data.js
@@ -236,4 +236,19 @@ describe('data', function() {
236 236
     equals("sad world?", result, "Overriden data output by helper");
237 237
   });
238 238
 
  239
+  describe('@root', function() {
  240
+    it('the root context can be looked up via @root', function() {
  241
+      var template = CompilerContext.compile('{{@root.foo}}');
  242
+      var result = template({foo: 'hello'}, { data: {} });
  243
+      equals('hello', result);
  244
+
  245
+      result = template({foo: 'hello'}, {});
  246
+      equals('hello', result);
  247
+    });
  248
+    it('passed root values take priority', function() {
  249
+      var template = CompilerContext.compile('{{@root.foo}}');
  250
+      var result = template({}, { data: {root: {foo: 'hello'} } });
  251
+      equals('hello', result);
  252
+    });
  253
+  });
239 254
 });
2  spec/expected/empty.amd.js
@@ -2,7 +2,7 @@ define(['handlebars.runtime'], function(Handlebars) {
2 2
   Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
3 3
 return templates['empty'] = template(function (Handlebars,depth0,helpers,partials,data) {
4 4
   this.compilerInfo = [4,'>= 1.0.0'];
5  
-helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
  5
+helpers = this.merge(helpers, Handlebars.helpers); data = this.initData(depth0, data);
6 6
   var buffer = "";
7 7
 
8 8
 
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.