Nesting v-for Loops with Unique Headers (sub-summary)

Overview

This example is useful if you want to show a unique header only once in your v-for list.

For instance, if you had an array of objects:

    "procedures": [{
            "category": "Lips",
            "procedure": "Fat Burner"
        }, {
            "category": "Botulinum Toxin",
            "procedure": "1 Area"
        }, {
            "category": "Botulinum Toxin",
            "procedure": "2 Areas"
        }, {
            "category": "Botulinum Toxin",
            "procedure": "3 Areas"
        }, {
            "category": "Dermal Filler",
            "procedure": "Chin"
        }, {
            "category": "Dermal Filler",
            "procedure": "Nasolabial"}]

and you wanted to have unique headers for each category such as:

Solution

This solution requires some JavaScript to work.

I thought about nesting v-for loops, but needed a new array with unique categories for my outer loop.
I utilized a lodash function called _.uniqWith to only grab the unique categories.

In my onFormLoad:

var result = _.uniqWith(model.procedures, function(arrVal, othVal) {
  return arrVal.category === othVal.category;
});

model.uniqCat = result

Remember to instantiate uniqCat in your data model

v-for

Now that we have our new array, we can use v-for to loop through each category, and then an additional v-for to loop through each procedure in that category.

<div v-for="header in model.uniqCat">

{{header.category}}

    <div v-for="line in model.procedures" v-if="line.category == header.category">
    
        {{line.procedure}}
    
    
    </div>
    
</div>

My outer loop loops through each category, and my inner loop loops through each procedure. It has a v-if to check if the procedure matches the header category of the outer loop so that every procedure doesn’t get rendered 3 times.

This will end up looking like:

image

With some additional styling:

<div v-for="header in model.uniqCat">
<div class="bg-blue-100 text-black font-bold">{{header.category}}</div>

<div v-for="(line, index) in model.procedures" 
v-if="line.category == header.category"
 class="ml-8 h-8 align-middle text-black font-semibold"
 :class="index % 2 == 0 ? 'bg-gray-50' : ''">
    
    {{line.procedure}}
    
    <button class="btn bg-sky-700 text-white p-1 pull-right mb-0">Delete</button>
    
    </div>
    
</div>

We can get pretty close to our desired result:

Conclusion

Because of the nested loop, I wonder if there may be performance issues if the array is large. There may be other solutions rather than creating a new array, however I found this to be relatively simple. Hope this helps!

2 Likes

@Andrew

when you say “in my onFormLoad” … where exactly does the below code go?

var result = _.uniqWith(model.procedures, function(arrVal, othVal) {
  return arrVal.category === othVal.category;
});

model.uniqCat = result

and then instantiating uniqCat in the data model… how/where does the below go?

   model.uniqCat = result

In your page info named actions:

That just means to add a default variable in your data model, something like "uniqCat" = []

I have this in my data model:

    "uniqCat": [],

this in my development data model:

"music": [{
        "setNumber": "Set 1",
        "songName": "Slow Hands"
    }, {
        "setNumber": "Set 1",
        "songName": "I Wish"
    }, {
        "setNumber": "Set 1",
        "songName": "Fever"
    }, {
        "setNumber": "Set 2",
        "songName": "Rock With You"
    }, {
        "setNumber": "Set 3",
        "songName": "I'm So Excited"
    }, {
        "setNumber": "Bullpen",
        "songName": "Stand By Me"
    }],

this in my html:

<div v-for="header in model.uniqCat">
<div class="bg-blue-100 text-black font-bold">{{header.setNumber}}</div>

<div v-for="(line, index) in model.music" 
v-if="line.setNumber == header.setNumber"
 class="ml-8 h-8 align-middle text-black font-semibold"
 :class="index % 2 == 0 ? 'bg-gray-50' : ''">
    
    {{line.songName}}
    
    </div>

This in my page info:

"namedActions": {
        "onFormLoad": [{
            "action": "function",
            "function": "var result = _.uniqWith(model.music, function(arrVal, othVal) {return arrVal.setNumber === othVal.setNumber;});"
        }],
        "runFunc": [{
            "action": "function",
            "function": "console.log(action.options.row)"
        }]
    },

Am I missing something? Nothing is rendering in the html editor preview :frowning:

In your onFormLoad function, you’ll need model.uniqCat = result as well. What that line does is takes the result that was returned by the lodash function and sets it in the model as uniqCat.

On a side note, you don’t need runFunc, just the onFormLoad.

OK. I amended my onFormLoad:

 "namedActions": {
        "onFormLoad": [{
            "action": "function",
            "function": "var result = _.uniqWith(model.music, function(arrVal, othVal) {return arrVal.setNumber === othVal.setNumber;});model.uniqCat = result"
        }]
    }

No rendering yet :frowning:

Odd, I copied your code and seems correct:

hmmm…I don’t see anything…

oh wait. i see it on the page. just not in the edit preview

Right, because uniqCat isn’t evaluated until the onFormLoad runs.

<div v-for="(line, index) in model.music" v-if="line.setNumber == header.setNumber" class="ml-8 h-8 align-middle text-black font-semibold" :class="index % 2 == 0 ? 'bg-gray-50' : ''">

            {{line.songName}} {{line.originalArtist}}

        </div>

Let’s say I wanted to add some more details to the line item but wanted the text to be styled differently?

Would I need to add another <div> for {{line.originalArtist}}?

Yep you can do that. Then give the new div the desired text classes.

@Andrew

In FM, sometimes I’ll name my fields "fieldName_cu" … with a _cu at the end to let me know it’s an UNSTORED CALCULATION.

Is there a chance that it’s messing up my JavaScript below?

var result = _.uniqWith(model.music, function(arrVal, othVal) {return arrVal.SetNumber_cu === othVal.SetNumber_cu;});model.uniqCat = result

I’ve had to updates some names and now my Table/List isn’t rendering.

In my Dev Tool…my JSON is parsing out correctly.

"uniqCat": [{
        "ID": "2AE464EC-FC69-4EE8-80E0-30C2A9ACC09E",
        "KeySignature_cu": "F",
        "OriginalArtist_cu": "Nat King Cole",
        "SetNotes_cu": "Anilee - Duo",
        "SetNumber_cu": "Set 1",
        "TempoSong_cu": "122",
        "TimeSignature_cu": "4/4",
        "songName_cu": "L-O-V-E"

It’s just not showing in my HTML.

I don’t see any immediate issues with the JavaScript, perhaps the HTML has something causing it?

Would you mind giving a glance at this?

HTML:

<!-- This example requires Tailwind CSS v2.0+ -->
<div class="bg-white bg-white shadow rounded-lg px-4 py-5 border-b border-gray-200 sm:px-6">
    <h3 class="text-lg leading-6 font-medium text-gray-900">Music</h3>
    <div class="border-t border-gray-200 px-4 py-5 sm:p-0">
        <div class="bg-white shadow overflow-hidden sm:rounded-lg mt-4">
            <div v-for="header in model.uniqCat">
                <div class="bg-[#822447] text-white font-bold indent-8">{{header.SetNumber_cu}}</div>

                <div v-for="(line, index) in model.music" v-if="line.SetNumber_cu == header.SetNumber_cu" class="ml-8 h-8 align-middle text-black font-semibold" :class="index % 2 == 0 ? 'bg-gray-50' : ''">

                    {{line.songName_cu}} - {{line.SetNotes_cu}}

                    <div class="text-sm text-black font-normal p-1 pull-right mb-0 mr-8" :class="index % 2 == 0 ? 'bg-gray-50' : ''">
                        {{line.OriginalArtist_cu}} ({{line.KeySignature_cu}} {{line.TimeSignature_cu}} {{line.TempoSong_cu}})
                    </div>
                </div>
            </div>
        </div>
    </div>
    <br><br>
    <iframe style="border-radius:12px" src="https://open.spotify.com/embed/playlist/37i9dQZF1DWVJ0TKGKfzgP?utm_source=generator" width="100%" height="380" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"></iframe>
</div>

Copied the code and it works for me.

My test data:

"music": [{
        "ID": "2AE464EC-FC69-4EE8-80E0-30C2A9ACC09E",
        "KeySignature_cu": "F",
        "OriginalArtist_cu": "Nat King Cole",
        "SetNotes_cu": "Anilee - Duo",
        "SetNumber_cu": "Set 1",
        "TempoSong_cu": "122",
        "TimeSignature_cu": "4/4",
        "songName_cu": "L-O-V-E"
    }, {
        "ID": "2AE464EC-FC69-4EE8-80E0-30C2A9ACC09E",
        "KeySignature_cu": "F",
        "OriginalArtist_cu": "Nat King Cole",
        "SetNotes_cu": "Anilee - Duo",
        "SetNumber_cu": "Set 1",
        "TempoSong_cu": "122",
        "TimeSignature_cu": "4/4",
        "songName_cu": "L-O-V-E"
    }, {
        "ID": "2AE464EC-FC69-4EE8-80E0-30C2A9ACC09E",
        "KeySignature_cu": "F",
        "OriginalArtist_cu": "Nat King Cole",
        "SetNotes_cu": "Anilee - Duo",
        "SetNumber_cu": "Set 2",
        "TempoSong_cu": "122",
        "TimeSignature_cu": "4/4",
        "songName_cu": "L-O-V-E"
    }, {
        "ID": "2AE464EC-FC69-4EE8-80E0-30C2A9ACC09E",
        "KeySignature_cu": "F",
        "OriginalArtist_cu": "Nat King Cole",
        "SetNotes_cu": "Anilee - Duo",
        "SetNumber_cu": "Set 3",
        "TempoSong_cu": "122",
        "TimeSignature_cu": "4/4",
        "songName_cu": "L-O-V-E"
    }]

hmm. OK. I can’t get it to work for me.

I tried to put it back the way it was before and still no dice.

I have my Enable Development data model checked but my page dev tools is showing data from FM.

The onFormRequest takes precedent, if you want to use the Dev data model you can uncheck Enable onFormRequest Hook in the Integration Tab