7

I'm new to Handlebars and using version 4.1.2. I'm trying to move some templates which were written in PHP to Handlebars.

The source of my data is a JSON feed and the structure is like this:

[
    {
        "regulations_label": "Europe",
        "groups_label": "Group 1",
        "filters_label: "FF1A"
    },
    {
        "regulations_label": "Europe",
        "groups_label": "Group 1",
        "filters_label: "FF1B"
    },
    {
        "regulations_label": "Europe",
        "groups_label": "Group 2",
        "filters_label: "FF2A"
    },
    {
        "regulations_label": "Asia",
        "groups_label": "Group 999",
        "filters_label: "FF999A"
    },
    {
        "regulations_label": "Asia",
        "groups_label": "Group 999",
        "filters_label: "FF999B"
    },
    {
        "regulations_label": "Americas",
        "groups_label": "Group 10000",
        "filters_label: "FF10000A"
    },
]

The output of my HTML template (in the PHP version) was as follows:

  • Europe
    • Group 1
      • FF1A
      • FF1B
    • Group 2
      • FF2A
  • Asia
    • Group 999
      • FF999A
      • FF999B
  • Americas
    • Group 10000
      • FF10000A

The way in which this was achieved - without duplicating any of the regulations_label or groups_label during output - was to use conditional logic which checked the previous array value to see if it had changed, e.g.

// $data is the JSON above. 
foreach ($data as $key => $d): 
    if ($data[$key-1]['groups_label'] !== $d['groups_label'])
        echo $d['groups_label'];
    endif;
endforeach;

The above code means that groups_label is only rendered if it is not the same as the previous value, i.e. it can only print "Group 1" once, etc.

So in Handlebars I'm wanting to apply the same principle. I've read Handlebars/Mustache - Is there a built in way to loop through the properties of an object? and understand there is a {{@index}} and {{@key}}.

The problem I'm having is that I can't apply conditional logic on these. For example there is no such thing as {{@index - 1}}

The way in which I have it set up is as follows:

<script id="regulations-template" type="text/x-handlebars-template">
    {{#each myregs}}
        - {{regulations_label}} <br>
        -- {{groups_label}} <br>
        ---- {{filters_label}} <br>
    {{/each}}
</script>


<script type="text/javascript">
    var regsInfo = $('#regulations-template').html();

    var template = Handlebars.compile(regsInfo);

    var regsData = template({ myregs: // JSON data shown above });

    $('#output').html(regsData);
</script>


<div id="output"></div>

The content is rendered to the #output div, but it repeats each level, e.g.

- Europe
    -- Group 1
        ---- FF1A
- Europe
    -- Group 1
        ---- FF1B

This problem is only happening because I'm unable to find a way to see what the previous array value was and do conditional logic. How can I solve this problem?

Notes for bounty:

I'm looking for a solution which uses Handlebars and takes advantage of any features it offers that can assist with this. Someone commented that it's possible to do this kind of thing in plain js/jquery. We want to use Handlebars to take advantage of the templates it offers therefore a solution which uses it fully is needed for the bounty. This sort of conditional logic is a trivial part of many other templating systems (not just limited to PHP). Therefore I can't help but think there's a way in Handlebars given templating is the main use case.

Andy
  • 4,326
  • 4
  • 36
  • 92
  • Do you have to use handlebars? You could just restructure your data with plain JS and output it much more easily. – Kobe Jul 02 '19 at 14:52
  • @Kobe - ideally we want to use it although the irony is that it was supposed to make producing HTML output from a JSON feed easy. It's been anything but that to be honest! The PHP application we had made an ajax request and the response data was (pre formatted) HTML. An improvement would just be for us to be able to take the JSON data and then have something that renders it as HTML. Appreciate there are other ways but we really do want to use a templating system like Handlebars if possible. Any advice appreciated. I'd looked at others like json2html etc. – Andy Jul 02 '19 at 14:55
  • I don't understand why you need a framework to do that? JS can render HTML. – Kobe Jul 02 '19 at 14:57
  • I understand that we could use jquery - or even vanilla Javascript - to loop through the JSON and write it to `#output`. But one of the purposes of things like Handlebars, Mustache etc, is so you can write the template syntax as HTML (see the innards of `#regulations-template` in the question) and have more flexibility. – Andy Jul 02 '19 at 14:59
  • Yeah, templating is definitely easier to read, but for something as simple as this, I think plain javascript would be sufficient. Nonetheless, that's your choice, but I've never used handlebars before so I won't be able to help you with that :P – Kobe Jul 02 '19 at 15:13
  • The question is specifically how to do it with Handlebars. Thanks for your input though. – Andy Jul 03 '19 at 09:54
  • I think I can solve your problem. The only issue here is your data structure, which can be fixed, and then you can use handlebars to render it – Kobe Jul 03 '19 at 10:28
  • Feel free to post an answer if you have one, I'll review it. Thanks – Andy Jul 03 '19 at 10:59
  • Learned a bit of handlebars to solve your problem, check my answer :) – Kobe Jul 03 '19 at 14:06
  • I know the question asks for a Handlebars solution, so I only comment: you can solve it with plain JS's template literals: https://benfrain.com/html-templating-with-vanilla-javascript-es2015-template-literals/ – Lajos Arpad Jul 23 '19 at 10:56

2 Answers2

0

I have used a recursive function (prepare for a loooong one liner) to format the data in such a way that should be easier to display. You can just map over the children, and use the labels to print:

const data = [
  { regulations_label: "Europe", groups_label: "Group 1", filters_label: "FF1A" },
  { regulations_label: "Europe", groups_label: "Group 1", filters_label: "FF1B" },
  { regulations_label: "Europe", groups_label: "Group 2", filters_label: "FF2A" },
  { regulations_label: "Asia", groups_label: "Group 999", filters_label: "FF999A" },
  { regulations_label: "Asia", groups_label: "Group 999", filters_label: "FF999B" },
  { regulations_label: "Americas", groups_label: "Group 10000", filters_label: "FF10000A" }
];

const r = (o, keys) => Object.entries(o.reduce((a, { [keys[0]]: l, ...r }) => ({ ...a, [l]: a[l] ? [...a[l], r] : [r] }), {})).map(([k, v]) => ({ label: k, children: keys.length === 2 ? v.map(o => o[keys[1]]) : r(v, keys.slice(1))}))

const myregs = r(data, ['regulations_label', 'groups_label', 'filters_label'])
const log = () => console.log(myregs)

var regsInfo = document.getElementById('regulations-template').innerHTML
var template = Handlebars.compile(regsInfo);
var regsData = template({myregs});
document.getElementById('output').innerHTML = regsData
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.2/handlebars.js"></script>
<script id="regulations-template" type="text/x-handlebars-template">
    {{#each myregs}}
      - {{this.label}} <br>
        {{#each this.children}}
          - - {{this.label}} <br>
          {{#each this.children}}
            - - - {{this}} <br>
          {{/each}}
        {{/each}}
    {{/each}}
</script>

<button onclick="log()">Log data</button>
<div id="output"></div>

Note I removed the jQuery, just because I felt it wasn't needed, but feel free to add it back in :)

Kobe
  • 5,539
  • 1
  • 9
  • 32
0

What you could do in a pure handlebars solution is to use the following helpers:

<script id="regulations-template" type="text/x-handlebars-template">
{{#each myregs}}
    {{#ifPrevRegulation}} - {{regulations_label}} <br> {{/ifPrevRegulation}}
    {{#ifPrevGroup}}-- {{groups_label}} <br>{{/ifPrevGroup}}
    {{#ifPrevFilter}} ---- {{filters_label}} <br>{{/ifPrevFilter}}
    {{setPrevContext}}
{{/each}}
</script>

Where

var prevContext = null;
Handlebars.registerHelper('setPrevContext', function(){
    prevContext = this;
});

Handlebars.registerHelper('ifPrevRegulation', function(options){
    if(!(prevContext  && this.regulations_label == prevContext.regulations_label)){
      return options.fn(this);
    }
});

Handlebars.registerHelper('ifPrevGroup', function(options){
    if(!(prevContext  && this.regulations_label == prevContext.regulations_label && this.groups_label== prevContext.groups_label)){
      return options.fn(this);
    }
});

Handlebars.registerHelper('ifPrevFilter', function(options){
    if(!(prevContext  && this.regulations_label == prevContext.regulations_label && this.groups_label== prevContext.groups_label && this.filters_label == prevContext.filters_label)){
      return options.fn(this);
    }
});
Greedo
  • 2,906
  • 1
  • 8
  • 22