Handlebars - calculation based on array key within a template

Viewed 643

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.

2 Answers

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 :)

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);
    }
});
Related