Skip to content
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

Add named child helpers that can be referenced by block helpers #404

Closed
jhudson8 opened this issue Jan 4, 2013 · 9 comments
Closed

Add named child helpers that can be referenced by block helpers #404

jhudson8 opened this issue Jan 4, 2013 · 9 comments
Labels
Milestone

Comments

@jhudson8
Copy link

jhudson8 commented Jan 4, 2013

It would be nice if we could define named blocks that could be referenced within a Handlebars helper. I'll use, for example, a special prefix {{@ to defined the child helper. An example would be:

Here is some template content
{{#section}}
  {{*header}}the section header{{/header}}
  {{*body}}the section body{{/body}}
  {{*footer}}the section footer{{/footer}}
{{//section}}

And, for example, the nested helpers could be referenced in this way:

Handlebars.registerHelper('section', function(options) {
  // it would also be nice to get all child blocks as an array to check for length
  // in case we wanted to do something different (like not require the usage of {{@body}}
  // if no header/footer were present (using special '__children' just as an example)

  var rtn = '<section>';
  if (!options.fn.__children.length) {
    rtn += options.fn();
  } else {
    if (options.fn.header) {
      rtn += ('<div class="header">' + options.fn.header(this) + '</div>');
    }
    rtn += ('<div class="body">' + options.fn.body(this) + '</div>');
    if (options.fn.footer) {
      rtn += ('<div class="footer">' + options.fn.footer(this) + '</div>');
    }
  }
  return new Handlebars.SafeString(rtn);
});
@dmarcotte
Copy link
Contributor

Hey @jhudson8, I haven't tried it yet, but @thejohnfreeman has a pretty promising-looking approach which should satisfy your use case.

Check out the details in his blog post and look for tips on how to implement it in the related issue.

Hope that helps!

@jhudson8
Copy link
Author

jhudson8 commented Jan 4, 2013

Thanks @dmarcotte, it certainly seems that the author is experiencing some of the pain that I am but I am looking for a less half-baked approach (no offense intended to the author). Unless I'm misunderstanding, the child block names can not be reused with this solution.

I would like to be able to do something like:

{{#thing1}}
  {{@header}}...{{/@header}}
{{/thing1}}

{{#thing2}}
  {{@header}}...{{/header}}
{{/thing2}}

and have the ability to control what happens with header in the helper definition.

Regardless, I appreciate your comment - thanks.

I am going to try to implement this but I know nothing of the handlebars internals. If I get this done, I'll add a comment with the PR branch. If you are interested in this, feel free to comment with syntax suggestions (and impl suggestions for that matter).

@jhudson8
Copy link
Author

jhudson8 commented Jan 5, 2013

Alas, I'm not going to be able to come up with a good solution as I can't even get Handlebars to build due to some issue with the 'therubyracer' dependancy. I really hope that someone with access and Handlebars skills add this type of functionality because it seems like it would be super useful.

In the meantime, I've come up with a ghetto solution.

function parseMode(mode, fn, context) {
  return fn(_.defaults({__mode: mode}, context));
}

Handlebars.registerHelper('child', function(type, options) {
  if (this.__mode === type) {
    return new Handlebars.SafeString(options.fn(this));
  }
  return '';
});

Handlebars.registerHelper('section', function(options) {
  return new Handlebars.SafeString('<section><div class="header">' +
               parseMode('header', options.fn, this) +
               '</div><div class="body">' +
               parseMode('body', options.fn, this) +
               '</div><div class="footer">' +
               parseMode('footer', options.fn, this) +
               '</div><div class="footer">');
});

with a usage of

{{#section}}
  {{#child 'header'}}...{{/child}}
  {{#child 'body'}}...{{/child}}
  {{#child 'footer'}}...{{/child}}
{{/#section}}

It has a few issues though:

  • you have to copy the context for every child parsing so you don't alter the original context - a greater issue if you are doing this for all rows in a table, for example
  • you have no insight into whether child blocks were added or not
  • not as friendly of a syntax

@thejohnfreeman
Copy link
Contributor

What is it that you want exactly? Your example seems incomplete. I think there's something implied that I'm just not understanding.

{{#section}}
  {{#child 'header'}}...{{/child}}
  {{#child 'body'}}...{{/child}}
  {{#child 'footer'}}...{{/child}}
{{/#section}}

Is this a reusable block? Is it supposed to be a stand-alone example? It looks like it would produce no output. I'm just not getting it. :/

@jhudson8
Copy link
Author

jhudson8 commented Jan 5, 2013

@thejohnfreeman

The section helper will basically call it's block function 3 times (with a specific mode attribute; "header", "body", "footer"). The child helper will not render anything unless the mode attribute matches the child helper's expected mode.

The parseMode function (called by the section helper) sets up the context with the mode attribute and calls the section helper block function (provided as param).

However, I have come up with a use case which I believe is much more applicable and can not be solved by my solution. I would like to be able to do the following:

{{#grid items=people}}
  {{*column label="First Name"}} {{firstName}} {{/column}}
  {{*column label="Last Name"}} {{lastName}} {{/column}}
{{/grid}}

With a helper like something as follows (just an initial thought)

Handlebars.registerHelper('grid', function(options) {
  var rtn = '<table><thead><tr>';

  // access a list of nested block parameters (named as "column")
  // to write out the grid headers
  _.each(options.bodyParams.column, function(column) {
    rtn += ('<td>' + column.hash.label + '</td>');
  });
  rtn += '</tr></thead><tbody>';

  var items = options.hash.items;
  _.each(items, function(item) {
    rtn += '<tr>';
    // access the body params again to write out the item data
    _.each(options.bodyParams.column, function(column) {
      rtn += ('<td>' + column.fn(item) + '</td>');
    });
    rtn += '</tr>';
  });

  rtn += '<tr></tbody></table>';
  return new Handlebars.SafeString(rtn);

   // this last parameter would indicate that this block helper accepts nested block params
   // so, for example, the if block helper wouldn't swallow params
}, true);

IMHO, if Handlebars had this functionality - it would be unbeatable.

Question: is it appropriate to rewrite the description of this issue with the new use case or close this task and create a new one for posterity?

@jhudson8
Copy link
Author

jhudson8 commented Jan 8, 2013

note: I just changed the example child parameter block identifiers from '@' to '*' as I didn't realize that {{@...}} was existing Handlebars syntax.

@eastridge
Copy link
Contributor

@jhudson8 my thought was that you could do this with the helpers hash, here is what I came up with:

<script type="text/template" data-template-name="test">
  {{#grid items=people}}
    {{#column label="First Name"}} {{firstName}} {{/column}}
    {{#column label="Last Name"}} {{lastName}} {{/column}}
  {{/grid}}
</script>
<script>

  Handlebars.registerHelper('grid', function(options) {
    var head = '';
    var body = '';
    function column(options) {
      head += '<td>' + column.hash.label + '</td>';
      body += '<tr><td>' + column.fn(item) + '</td></tr>';
    }
    _.each(options.hash.items, function(item) {
      options.fn(item, {
        helpers: {
          column: column
        }
      });
    }, this);
    return new Handlebars.SafeString(
      '<table>' +
      '<thead><tr>' + head + '</tr></thead>' +
      '<tbody>' + body + '</tbody>' +
      '</table>'
    );
  });

  var template = Handlebars.compile($('[data-template-name="test"]').text());
  console.log(template({
    people: [
      {firstName: 'Ryan', lastName: 'Eastridge'},
      {firstName: 'Joe', lastName: 'Hudson'}
    ]
  }));
</script>

But it turns out this won't work because you can't pass a helpers hash to options.fn, @wycats is there a reason options.fn and options.inverse won't accept that as an argument?

@jhudson8
Copy link
Author

jhudson8 commented Jan 9, 2013

@eastridge I like where you are going with this. This would work if you could pass a helpers hash.

@kpdecker kpdecker added this to the Backlog milestone Jul 5, 2014
@kpdecker kpdecker reopened this Feb 10, 2015
@kpdecker
Copy link
Collaborator

kpdecker commented May 5, 2015

Closing in favor of #1018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants