2

I am attempting to dynamically generate some HTML lists based on JSON data fetched from a PHP script. The returned data is in the following format:

SAMPLE JSON DATA:

[{"animal":"Dog","breed":"German Shepard","image":"germanshepard.png","ownerID":"885","Status":"Check In"},
{"animal":"Dog","breed":"Poodle","image":"poodle.png","ownerID":"131","Status":"Checked In"},
{"animal":"Dog","breed":"Labrador","image":"labrador.png","ownerID":"6","Status":"Waiting Consultation"},
{"animal":"Bird","breed":"Parrot","image":"parrot.png","ownerID":"22","Status":"Checked In"},
{"animal":"Bird","breed":"Finch","image":"finch.png","ownerID":"31","Status":"Checked In"},
{"animal":"Cat","breed":"Persian","image":"persian.png","ownerID":"19","Status":"Waiting Consultation"},
{"animal":"Cat","breed":"Sphynx","image":" sphynx.png","ownerID":"44","Status":"Allocated Kennel"}]

I'm trying to create a div for each unique animal, ie. 1 div per unique animal. eg.

<div class='dog'></div>

Within each div I am trying to append an ul for each unique breed eg.

<ul class='Poodle'></ul>

with the final result looking like so:

ul {
background: #fff;
color: #000;
border: 1 px thin solid #000;
}

/* repeat for all status classes */
.Checked In {
color: 'green';
}
<section id='container'>
<div class='Dog'>    
    <ul class='German Shepard'>
       <li><img src='germanshepard.png'/></li>
       <li class='${ownerID}'>885</li>
       <li class='${Status}'>Check In</li>
    </ul>    
    <ul class='Poodle'>
       <li><img src='poodle.png'/></li>
       <li class='${ownerID}'>131</li>
       <li class='${Status}'>Checked In</li>
    </ul>
</div>    
<div class='Bird'>    
    <ul class='Parrot'>
       <li><img src='parrot.png'/></li>
       <li class='${ownerID}'>22</li>
       <li class='${Status}'>Checked In</li>
    </ul>    
    <ul class='Finch'>
       <li><img src='finch.png'/></li>
       <li class='${ownerID}'>31</li>
       <li class='${Status}'>Checked In</li>
    </ul>
</div>
</section>

I'm returning the data like so:

function getAnimalData() {
let url = "php/getAnimalData.php";
fetch(url)
    .then((res) => res.json())
    .then((data) => {
        for (const [key, value] of Object.entries(data)) {
            byID('container').insertAdjacentHTML('afterend', 
            `<div class="${value['animal']}">
              <ul class="${value['breed']}">
               <li class="${value['image']}"></li>
               <li class="${value['owner']}"></li>
               <li class="${value['status']}"></li>
              </ul>
            </div>`);
        }
    }).catch(err => {
        return Promise.reject(err.message);
    });
}

As you can tell this doesn't filter unique animal and creates a div for each animal with each breed beneath it. I have attempted a few solutions on stackoverflow (1 for example) - most of which involve creating an array of distinct animal names then looping through that to create the multiple ul elements and then append the li by again looping over an array of unique breeds and appending appropriately. Unfortunately, I haven't found a solution that works and anything that comes close to working is using jQuery, knockout, etc (i'd very much like not to use jQuery).

I'm open to suggestions on how to achieve this using Vanilla JavaScript - or is this a case of going back to the drawing board and formatting the JSON data differently.

ted
  • 13,596
  • 9
  • 65
  • 107
edwardinchains
  • 119
  • 1
  • 12
  • 1
    You should reformat the data. You can easily do this with forEach attaching all the records to an object that has the animal as key – Jonathan Feb 24 '20 at 15:16

5 Answers5

1

You can use this groupBy function

function groupBy(list, props) {
  return list.reduce((a, b) => {
     (a[b[props]] = a[b[props]] || []).push(b);
     return a;
  }, {});
}
//https://gist.github.com/ramsunvtech/102ac0267d33c2cc1ccdf9158d0f7fca

usage: groupBy([{
  id: 1,
  group: 'user',
  name: 'User #1',
}, {
  id: 2,
  group: 'admin',
  name: 'User #2',
}, {
  id: 3,
  group: 'moderator',
  name: 'User #3',
}], 'group');

Then loop throught the grouped list with Object.entriesto fill your template

Gatoyu
  • 642
  • 1
  • 6
  • 22
1

First, manipulate your data:

fetch(url)
    .then((res) => res.json())
    .then((data) => { // assuming data is an array of items
        const hash = {}
        for (let item of data) {
           if (!hash[item.animal])
              hash[item.animal] = []
           hash[item.animal].push(item)

This will result with an object that has nested values for each animal kind

Next step: Populate titles only

for (let kind in hash) {
     byID('container').insertAdjacentHTML('afterend', `<div>${kind}</div>`)
}

Final Step: add relevant controls under each title

for (let kind in hash) {
   byID('container').insertAdjacentHTML('afterend', `<div>${kind}</div>`)
   for (const value of hash[kind])) {
        byID('container').insertAdjacentHTML('afterend', 
        `<div class="${value['animal']}">
          <ul class="${value['breed']}">
           <li class="${value['image']}"></li>
           <li class="${value['owner']}"></li>
           <li class="${value['status']}"></li>
          </ul>
        </div>`);
    }

}

ymz
  • 6,602
  • 1
  • 20
  • 39
  • using your suggestion I'm getting the following error in the dev console: index.html:1 Uncaught (in promise) Cannot read property 'insertAdjacentHTML' of null – edwardinchains Feb 24 '20 at 16:02
  • it should be placed right after the code in the first section.. note that others used `byID` as well in their answers according to the code you used in your question – ymz Feb 24 '20 at 16:12
  • byID is just a shorthand (library) for document.getElementById. https://codepen.io/edwardinchains/pen/YzXNrzv – edwardinchains Feb 24 '20 at 17:02
1

You need group the data you are receiving

Your code should be updated like

function getAnimalData() {
let url = "php/getAnimalData.php";
fetch(url)
    .then((res) => res.json())
    .then((data) => {
        const groupedData = data.reduce((a,obj) => {
        if (a[obj.animal] === undefined) a[obj.animal] = [obj]
        else a[obj.animal].push(obj)
        return a;
        }, {})
        groupedData.forEach((animals) => {
          animals.forEach((animal) => {
          for (const [key, value] of Object.entries(animal)) {
            byID('container').insertAdjacentHTML('afterend', 
            `<div class="${value['animal']}">
              <ul class="${value['breed']}">
               <li class="${value['image']}"></li>
               <li class="${value['owner']}"></li>
               <li class="${value['status']}"></li>
              </ul>
            </div>`);
          }
        })
      })
    }).catch(err => {
        return Promise.reject(err.message);
    });
}

Notice that I have grouped animals and then looking over each.

Cheers

igauravsehrawat
  • 3,696
  • 3
  • 33
  • 46
1

The best way I can think of is by using .reduce() method. Here's JS implementation. You can define a global variable and use methods to append/replace DOM elements, instead of object approach.

const array = [
    { "animal": "Dog", "breed": "German Shepard", "image": "germanshepard.png", "ownerID": "885", "Status": "Check In" },
    { "animal": "Dog", "breed": "Poodle", "image": "poodle.png", "ownerID": "131", "Status": "Checked In" },
    { "animal": "Dog", "breed": "Labrador", "image": "labrador.png", "ownerID": "6", "Status": "Waiting Consultation" },
    { "animal": "Bird", "breed": "Parrot", "image": "parrot.png", "ownerID": "22", "Status": "Checked In" },
    { "animal": "Bird", "breed": "Finch", "image": "finch.png", "ownerID": "31", "Status": "Checked In" },
    { "animal": "Cat", "breed": "Persian", "image": "persian.png", "ownerID": "19", "Status": "Waiting Consultation" },
    { "animal": "Cat", "breed": "Sphynx", "image": " sphynx.png", "ownerID": "44", "Status": "Allocated Kennel" }
]

const result = array.reduce((prev, curr) => ({
    ...prev,
    [curr.animal]: {
        ...prev[curr.animal],
        [curr.breed]: {
            ...(prev[curr.animal] || {})[curr.breed] || {},
            image: curr.image,
            owner: curr.ownerID,
            status: curr.Status
        }
    }
}), {})

console.log(result)
Max Zavodniuk
  • 79
  • 1
  • 7
1

This solution first collates the data by animal. This collated object is then enumerated and the HTML computed.

const data = [{"animal":"Dog","breed":"German Shepard","image":"germanshepard.png","ownerID":"885","status":"Check In"},{"animal":"Dog","breed":"Poodle","image":"poodle.png","ownerID":"131","status":"Checked In"},{"animal":"Dog","breed":"Labrador","image":"labrador.png","ownerID":"6","status":"Waiting Consultation"},{"animal":"Bird","breed":"Parrot","image":"parrot.png","ownerID":"22","status":"Checked In"},{"animal":"Bird","breed":"Finch","image":"finch.png","ownerID":"31","status":"Checked In"},{"animal":"Cat","breed":"Persian","image":"persian.png","ownerID":"19","status":"Waiting Consultation"},{"animal":"Cat","breed":"Sphynx","image":"sphynx.png","ownerID":"44","status":"Allocated Kennel"}]

const collate = (data) => data.reduce((acc, {animal, breed, image, ownerID, status}) => {
    acc[animal] = acc[animal] || {}
    acc[animal][breed] = acc[animal][breed] || {}
    acc[animal][breed] = { image, ownerID, status }
    return acc    
}, {})

const toHTML = (data) => `<section id='container'>${Object.entries(collate(data)).reduce((str, e) => str += animalHTML(e), '')}
</section>`

const animalHTML = ([animal, breeds]) => `
<div class='${animal}'>
${Object.entries(breeds).reduce((str, b) => str += breedHTML(b), '')}</div>`


const breedHTML = ([breed, {image, ownerID, status}]) =>
`<ul class='${breed}'>
    <li><img src='${image}'/></li>
    <li class='${ownerID}'></li>
    <li class='${status}'></li>
</ul>
`

console.log(toHTML(data))
Ben Aston
  • 53,718
  • 65
  • 205
  • 331