rjn1
rjn1

Reputation: 39

Dynamically creating grouped lists using array of objects

I have a nested array of objects as follows:

result = [
           [{"country":"France","continent":"Europe","name":"street a"}],
           [{"country":"Brazil","continent":"South America", "name":"street b"},{"country":"Brazil","continent":"South America", "name":"street c"}],
           [{"country":"Germany","continent":"Europe","name":"street d"}]
         ]

Note that this was generated from an array of objects after sorting, using continent and then the country keys.

I want to dynamically generate a list as follows (continent and then countries) and add it to a div:

  1. Europe
    • France
      • street a
    • Germany
      • street d
  2. South America
    • Brazil
      • street b
      • street c

Each time the countries and the continents returned would be different but the result array would be grouped in continent,country order.

Since I am new to HTML, CSS and JavaScript, I am not able to figure out how to dynamically create this list. So far I have managed to generate a unique list of only the continents using :

for(var i=0;i<result.length;i++){
    if($('#continent-list li').filter(function(){
         return $(this).text() == result[i][0].continent;
    }).length == 0){
         $('#continent-list').append('<li>'+result[i][0].continent+'<ul></ul></li>');
}

ie., I iterate through the list and only bother to see the first element [result[i][0]) and add this to an unordered list called 'continent-list'. Each time I also check if the name already exists in the unordered list and if it does not, only then I add.

Upvotes: 3

Views: 1977

Answers (6)

Jonathan Eunice
Jonathan Eunice

Reputation: 22473

This isn't the simplest requirement / request in the world, but it's not particularly convoluted, either. It bothers me that it takes so much code to solve. I know of no libraries that make this problem trivial, but two that come in very handy:

First, your records are excessively nested. To make them convenient to render, they should be flattened. Underscore can do this in one line:

var flat = _.flatten(result);

That isn't so hard in plain JS:

var flat = [];
result.forEach(sublist => {
  sublist.forEach(item => flat.push(item));
});

But it's nice to be able to "do it in one move" and move on.

Then you want several levels of nesting. Underscore has a nice groupBy feature, but it's single-level. There are other multi-level extensions out there, but that's more fussing around. D3, however, manages the nesting very easily:

var nested = d3.nest()
               .key(d => d.continent)
               .key(d => d.country)
               .entries(flat);

Then the nested data structure needs to be rendered to HTML. You can do that to HTML directly, but that's kind of messy. Thankfully, browsers have good tools for managing the DOM, aka their internal representation of HTML. D3 is great at manipulating it.

function nested2html(e, n, level) {
  level = level || 0;
  if (typeof e === 'string') e = d3.select(e);
  var list = e.append(level ? 'ul' : 'ol')
              .attr('class', 'level' + (level+1));
  var leafNode = !n[0].key;
  n.forEach(nitem => {
    if (leafNode) {
      list.append('li').text(nitem.name);
    } else {
      var li = list.append('li');
      li.append('span').text(nitem.key);
      nested2html(li, nitem.values, level + 1);
    }
  });
}

// now add it to the document
nested2html('body', nested);

This isn't particularly complicated as recursive routines go, and it renders out into well-structured, pretty easily styled HTML:

<ol class="level1">
  <li>
    <span>Europe</span>
    <ul class="level2">
      <li>
        <span>France</span>
        <ul class="level3">
          <li>street a</li>
        </ul>
      </li>
      <li>
        <span>Germany</span>
        <ul class="level3">
          <li>street d</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <span>South America</span>
    <ul class="level2">
      <li>
        <span>Brazil</span>
        <ul class="level3">
          <li>street b</li>
          <li>street c</li>
        </ul>
      </li>
    </ul>
  </li>
</ol>

A JSFiddle with the code running, and some optional CSS showing how the result can be styled.

I only wish there were a way to do this more concisely.

Upvotes: 0

Fareed Alnamrouti
Fareed Alnamrouti

Reputation: 32174

i think most of the work can be done using the array reduce function ;)

Step 1: we will convert your array to the structure you need:

var result = [
    [{"country": "France", "continent": "Europe", "name": "street a"}],
    [{"country": "Brazil", "continent": "South America", "name": "street b"}, {
        "country": "Brazil",
        "continent": "South America",
        "name": "street c"
    }],
    [{"country": "Germany", "continent": "Europe", "name": "street d"}]
];

// fatten the arrays into single array
var continents = result.reduce(function (a, b) {
    return a.concat(b);
});

// group by continent
continents = continents.reduce(function (a, b) {
    if (a[b.continent]) {
        a[b.continent].push(b);
    } else {
        a[b.continent] = [b];
    }
    return a;
}, {});

// group by country
Object.keys(continents).forEach(function (continent) {
    continents[continent] = continents[continent].reduce(function (a, b) {
        if (a[b.country]) {
            a[b.country].push(b);
        } else {
            a[b.country] = [b];
        }
        return a;
    }, {});
});

now this is how your continents data object should look like:

{
  "Europe": {
    "France": [
      {
        "country": "France",
        "continent": "Europe",
        "name": "street a"
      }
    ],
    "Germany": [
      {
        "country": "Germany",
        "continent": "Europe",
        "name": "street d"
      }
    ]
  },
  "South America": {
    "Brazil": [
      {
        "country": "Brazil",
        "continent": "South America",
        "name": "street b"
      },
      {
        "country": "Brazil",
        "continent": "South America",
        "name": "street c"
      }
    ]
  }
}

Step 2: we will convert this object into html elements using JQuery:

// create continents list
var $list = $('<ol id="continent-list"/>');
Object.keys(continents).forEach(function (continentName) {
    // create continent
    var $continent = $('<li class="continent"/>').append($("<span/>").text(continentName));

    // add countries
    var $countries = $('<ul class="country-list"/>');
    var continent = continents[continentName];
    Object.keys(continent).forEach(function (countryName) {
        var $country = $('<li/>').append($("<span/>").text(countryName));
        // add streets
        var $streets = $('<ul class="street-list"/>');
        var country = continent[countryName];
        country.forEach(function (street) {
            var $street = $('<li/>').text(street.name);
            $streets.append($street);
        });
        $country.append($streets);
        $countries.append($country);
    });
    $continent.append($countries);

    // add continent to the list
    $list.append($continent);
});

now this is how $list should look like:

<ol id="continent-list">
    <li class="continent"><span>Europe</span>
        <ul class="country-list">
            <li><span>France</span>
                <ul class="street-list">
                    <li>street a</li>
                </ul>
            </li>
            <li><span>Germany</span>
                <ul class="street-list">
                    <li>street d</li>
                </ul>
            </li>
        </ul>
    </li>
    <li class="continent"><span>South America</span>
        <ul class="country-list">
            <li><span>Brazil</span>
                <ul class="street-list">
                    <li>street b</li>
                    <li>street c</li>
                </ul>
            </li>
        </ul>
    </li>
</ol>

Step 3 Just append it to the document, e.g:

$("body").append($list);

Upvotes: 0

Nina Scholz
Nina Scholz

Reputation: 386680

You could flat the array, sort by properties continent, country and street and build all new lists when changing the groups.

var result = [[{ "country": "France", "continent": "Europe", "name": "street a" }], [{ "country": "Brazil", "continent": "South America", "name": "street b" }, { "country": "Brazil", "continent": "South America", "name": "street c" }], [{ "country": "Germany", "continent": "Europe", "name": "street d" }]],
    flat = result.reduce(function (r, a) {
        return r.concat(a);
    }, []),
    ol = document.createElement('ol'),
    ul1, ul2;

flat.sort(function (a, b) {
    return a.continent.localeCompare(b.continent) || a.country.localeCompare(b.country) || a.name.localeCompare(b.name);
});

flat.forEach(function (a, i, aa) {
    var li;
    if (!i || aa[i - 1].continent !== a.continent) {
        li = document.createElement('li');
        li.appendChild(document.createTextNode(a.continent));
        ol.appendChild(li);
        ul1 = document.createElement('ul');
        ol.appendChild(ul1);
    }
    if (!i || aa[i - 1].country !== a.country) {
        li = document.createElement('li');
        li.appendChild(document.createTextNode(a.country));
        ul1.appendChild(li);
        ul2 = document.createElement('ul');
        ul1.appendChild(ul2);
    }
    li = document.createElement('li');
    li.appendChild(document.createTextNode(a.name));
    ul2.appendChild(li);
});

document.getElementById('out').appendChild(ol);
<div id="out"></div>

Upvotes: 0

RobertFrenette
RobertFrenette

Reputation: 627

I'll let you handle the duplicates, but this should give you the general idea:

jsFiddle

function displayData(continent, country, street) {
  console.log(continent + ', ' + country + ', ' + street);
  var output = $('#output');

  var continentLI = document.createElement('li');
  var continentTextNode = document.createTextNode(continent);

  continentLI.appendChild(continentTextNode);

  var countryUL = document.createElement('ul');
  var countryLI = document.createElement('li');
  var countryTextNode = document.createTextNode(country);
  countryLI.appendChild(countryTextNode);
  countryUL.appendChild(countryLI);

  var streetUL = document.createElement('ul');
  var streetLI = document.createElement('li');
  var streetTextNode = document.createTextNode(street);

  streetLI.appendChild(streetTextNode);
  streetUL.appendChild(streetLI);
  countryLI.appendChild(streetUL);

  continentLI.appendChild(countryUL);
  output.append(continentLI);
};

$.each(result, function(resultKey, resultValue) {
  var country = resultValue;
  $.each(country, function(countryKey, countryValue) {
    var continent = countryValue.continent;
    var country = countryValue.country;
    var street = countryValue.name;
    displayData(continent, country, street);
  });
});

Upvotes: 0

vijay
vijay

Reputation: 631

var countries =[[{"country":"France","continent":"Europe","name":"street a"}], [{"country":"Brazil","continent":"South America", "name":"street b"}{"country":"Brazil","continent":"South America", "name":"street c"}], [{"country":"Germany","continent":"Europe","name":"street d"}]];

       var modified = {};

        for(var i =0; i<countries.length; i++){
          var continent = countries[i];
          for(var j =0;j<continent.length;j++){
              var country = continent[j];
              if(!modified[country.continent]){
                modified[country.continent] ={}
              }
               if(!modified[country.continent][country.country]){
                modified[country.continent][country.country] =[]
              }
              modified[country.continent][country.country].push(country);
         }
    }
    var liText = "<ul>";
    for (var key in modified) {
    liText += "<li>"+key + "<ul>";
    for (var country in  modified[key]){
      liText += "<li>"+country + "<ul>";
      for(var i=0;i< modified[key][country].length;i++){
        liText += "<li>"+modified[key][country][i].name + "</ll>";
      }
      liText += "</ul></li>";
    };
   liText += "</ul></li>";
      };
        liText += "</ul>"

console.log(liText);

document.getElementById('continent-list').innerHTML = liText;

Upvotes: 0

David Adegoke
David Adegoke

Reputation: 100

I think one way to do this is to loop through the objects in the array and create divs for the continents available while making sure no continent is created more than once then still using the same loop you can use something like

for(i=0;i<=results.length;i++){
  if (result[i].continent == continent) //here talking about the present continent in the main loop{
     //code to create new div for the current country....then same procedure for the street part....
    }
}

Sorry i'm not sounding too jqueryish :D but really i hope you get the scope

Upvotes: 2

Related Questions