New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

block helper adds extra level to parent context (../..) #196

Closed
trask opened this Issue Feb 27, 2012 · 11 comments

Comments

Projects
None yet
@trask

trask commented Feb 27, 2012

It seems you have to use ../.. to get to the parent context from inside of a block helper. I'm not sure if this is correct or not. It feels like the parent context should stay the same (..), similar to how the context stays the same (e.g. "block helper staying in the same context" in qunit_spec.js). Here's a runnable html example in case I'm using the wrong terminologies and not making sense:

<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="http://cloud.github.com/downloads/wycats/handlebars.js/handlebars-1.0.0.beta.6.js"></script>
<script id="t" type="text/x-handlebars-template">
{{#subitems}}
  name: {{name}}<br>
  parent id: {{../id}}<br>
  {{#h}}
    (works) name: {{name}}<br>
    (doesn't work) parent id: {{../id}}<br>
    (works) parent parent id: {{../../id}}<br>
  {{/h}}
{{/subitems}}
</script>
</head>
<body>
<div id="d"></div>
<script type="text/javascript">
  var f = Handlebars.compile($('#t').html())
  Handlebars.registerHelper('h', function(options) {
    return options.fn(this)
  })
  var items = { 'id': 123, 'subitems': [{'name': 'abc'}]}
  $('#d').html(f(items))
</script>
</body>
</html>

Thanks,
Trask

@krid

This comment has been minimized.

Show comment
Hide comment
@krid

krid Apr 5, 2012

This is (presumably) the same as issue #201.

krid commented Apr 5, 2012

This is (presumably) the same as issue #201.

@cj

This comment has been minimized.

Show comment
Hide comment
@cj

cj Apr 6, 2012

I have the same issue with 1.0.beta.6

cj commented Apr 6, 2012

I have the same issue with 1.0.beta.6

@rfreitas

This comment has been minimized.

Show comment
Hide comment
@rfreitas

rfreitas Jun 30, 2012

ditto what cj said.

rfreitas commented Jun 30, 2012

ditto what cj said.

@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Sep 15, 2012

Owner

This isn't really a bug. Let me try to explain:

In Handlebars, each template has a context. Blocks ({{#if foo}}) create inline templates, and the helper (if) decides what context to render the template in.

Some helpers preserve the current context (like if), while others change it in different ways (each and with, for example). Helpers can even change the context to something totally new if they want (options.fn({ zomg: true })).

The .. feature means "look this up on the context of the parent template". In some cases, that will amount to the parent in the current object (with). In other cases, it will not. In cases like if, which preserve the context, .. will happen to point to the same object, but this is just a coincidence.

In all cases, .. is lexically bound to the parent template.

Owner

wycats commented Sep 15, 2012

This isn't really a bug. Let me try to explain:

In Handlebars, each template has a context. Blocks ({{#if foo}}) create inline templates, and the helper (if) decides what context to render the template in.

Some helpers preserve the current context (like if), while others change it in different ways (each and with, for example). Helpers can even change the context to something totally new if they want (options.fn({ zomg: true })).

The .. feature means "look this up on the context of the parent template". In some cases, that will amount to the parent in the current object (with). In other cases, it will not. In cases like if, which preserve the context, .. will happen to point to the same object, but this is just a coincidence.

In all cases, .. is lexically bound to the parent template.

@sharans

This comment has been minimized.

Show comment
Hide comment
@sharans

sharans Mar 28, 2013

@wycats : Can you explain a little bit more on the resolution of parent context incase of #with:
"In some cases, that will amount to the parent in the current object (with)"
My understanding for var items = { 'id': 123, 'subitems': [{'name': 'abc'}], 'inventory': {'available': 5, 'sold':4}}
#if - does not create a new context as its just an if condition ?
#each subitems - creates a new context for each subitem and a ../id will work fine as it takes it to the parent context
{{#with inventory}}
id works here {{../id}}
{{#if available > 2}}
id does not work {{../id}}
id does work {{../../id}}
{{/if}}
{{/with}}
#with - creates a new context and ../id to refer to the parent context, but a #if within a #with behaves like a nested context since it requires a ../../id.

Ideally I would expect #if to behave the same at top level and within #with, but the context seems to be different!
So #if within any block like #each or #with behaves differently than at the top level. Why?

sharans commented Mar 28, 2013

@wycats : Can you explain a little bit more on the resolution of parent context incase of #with:
"In some cases, that will amount to the parent in the current object (with)"
My understanding for var items = { 'id': 123, 'subitems': [{'name': 'abc'}], 'inventory': {'available': 5, 'sold':4}}
#if - does not create a new context as its just an if condition ?
#each subitems - creates a new context for each subitem and a ../id will work fine as it takes it to the parent context
{{#with inventory}}
id works here {{../id}}
{{#if available > 2}}
id does not work {{../id}}
id does work {{../../id}}
{{/if}}
{{/with}}
#with - creates a new context and ../id to refer to the parent context, but a #if within a #with behaves like a nested context since it requires a ../../id.

Ideally I would expect #if to behave the same at top level and within #with, but the context seems to be different!
So #if within any block like #each or #with behaves differently than at the top level. Why?

ulriklystbaek added a commit to ulriklystbaek/handlebars.php that referenced this issue Mar 6, 2014

Update Helpers.php
Add an extra layer during if and unless. This was originally marked as an bug for handlebars, but has never been corrected, so this should be implemented to ensure consistency between the js and php version. Link to issue: wycats/handlebars.js#196

ulriklystbaek added a commit to ulriklystbaek/handlebars.php that referenced this issue Aug 28, 2014

Update IfHelper.php
To comply with handlebars.js, If adds an extra layer when need to access parents (wycats/handlebars.js#196)

ulriklystbaek added a commit to ulriklystbaek/handlebars.php that referenced this issue Aug 28, 2014

Update UnlessHelper.php
To comply with handlebars.js, If adds an extra layer when need to access parents (wycats/handlebars.js#196)
@jt000

This comment has been minimized.

Show comment
Hide comment
@jt000

jt000 Apr 11, 2015

@sharans - I'm confused about this as well. I have an SO question related to it, but haven't gotten any response. Seems {{#if}} doesn't preserve the parent context in some situations, but not sure why or when to expect it...

jt000 commented Apr 11, 2015

@sharans - I'm confused about this as well. I have an SO question related to it, but haven't gotten any response. Seems {{#if}} doesn't preserve the parent context in some situations, but not sure why or when to expect it...

@kpdecker

This comment has been minimized.

Show comment
Hide comment
@kpdecker

kpdecker Apr 14, 2015

Collaborator

@jt000 There is no preserving of the parent context. ../ means use the context of the block above me. When calling if you create a new block, thus need another ../, even if the execution context is not changed. I can't speak to the docs for other projects but you might want to speak with them if they are confusing.

Collaborator

kpdecker commented Apr 14, 2015

@jt000 There is no preserving of the parent context. ../ means use the context of the block above me. When calling if you create a new block, thus need another ../, even if the execution context is not changed. I can't speak to the docs for other projects but you might want to speak with them if they are confusing.

@jt000

This comment has been minimized.

Show comment
Hide comment
@jt000

jt000 Apr 17, 2015

@kpdecker - Thanks. That helps a lot. I just gave this a try in jsFiddle using the latest handlebars & it works as I originally expected (#if block context is same as parent block context). I'm not sure why it is not behaving the same in my code using assemble.io. I'll have to look into that further. Thanks again.

jt000 commented Apr 17, 2015

@kpdecker - Thanks. That helps a lot. I just gave this a try in jsFiddle using the latest handlebars & it works as I originally expected (#if block context is same as parent block context). I'm not sure why it is not behaving the same in my code using assemble.io. I'll have to look into that further. Thanks again.

@mgenev

This comment has been minimized.

Show comment
Hide comment
@mgenev

mgenev Aug 31, 2015

wait what you need a helper to access the scope? That's super annoying and makes me regret choosing to use handlebars if true

mgenev commented Aug 31, 2015

wait what you need a helper to access the scope? That's super annoying and makes me regret choosing to use handlebars if true

@jebrial

This comment has been minimized.

Show comment
Hide comment
@jebrial

jebrial May 5, 2016

You can just do @root.<id or other ctx item> for anyone that stumbles on this.

jebrial commented May 5, 2016

You can just do @root.<id or other ctx item> for anyone that stumbles on this.

@wolthers

This comment has been minimized.

Show comment
Hide comment
@wolthers

wolthers Jul 11, 2016

Correct me if I'm wrong, but I'm pretty sure that instead of calling options.fn or options.inverse with this, you can just call it with the root of the data that is passed to the template, so you "avoid" creating a new context.

Handlebars.registerHelper("is",   (value, test, options) => value === test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("isnt", (value, test, options) => value !=  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("gt",   (value, test, options) => value  >  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("gte",  (value, test, options) => value >=  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("lt",   (value, test, options) => value  <  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("lte",  (value, test, options) => value  <= test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("or",   (a, b, options)        =>     a ||  b    ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("isNull",   (value, options)   => value === null ? options.fn(options.data.root) : options.inverse(options.data.root));

This wouldn't work inside any other block helpers where scope is meant to be modified (e.g. #each or #with), but you get the idea.

wolthers commented Jul 11, 2016

Correct me if I'm wrong, but I'm pretty sure that instead of calling options.fn or options.inverse with this, you can just call it with the root of the data that is passed to the template, so you "avoid" creating a new context.

Handlebars.registerHelper("is",   (value, test, options) => value === test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("isnt", (value, test, options) => value !=  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("gt",   (value, test, options) => value  >  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("gte",  (value, test, options) => value >=  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("lt",   (value, test, options) => value  <  test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("lte",  (value, test, options) => value  <= test ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("or",   (a, b, options)        =>     a ||  b    ? options.fn(options.data.root) : options.inverse(options.data.root));
Handlebars.registerHelper("isNull",   (value, options)   => value === null ? options.fn(options.data.root) : options.inverse(options.data.root));

This wouldn't work inside any other block helpers where scope is meant to be modified (e.g. #each or #with), but you get the idea.

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