Pein
Pein

Reputation: 443

Angular 7 Grouping Data Using *ngfor

Right now i have data coming in like this in my angular application

https://stackblitz.com/edit/angular-ymmt4d

enter image description here

How can i group and order the data by State and County and display like below table

enter image description here

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  data = [
    { state: 'MN', county: '1', item: 0.297 },
    { state: 'MN', county: '1', item: 0.04 },
      { state: 'CA', county: '2', item: 0.019 },
    { state: 'MN', county: '1', item: 0.0374 }, 
    { state: 'CA', county: '2', item: 0.037 }
]
}
 <table >
        <tr>
          <th>State</th>
          <th>County</th>
          <th>Item</th>
        </tr>
        <ng-container *ngFor="let dataitem of data">
        <tr>
          <td>{{dataitem.state}}</td>
          <td>{{dataitem.county}}</td>
          <td>{{dataitem.item}}</td>
        </tr>
        </ng-container>
    </table>

Upvotes: 4

Views: 13659

Answers (5)

jo_va
jo_va

Reputation: 13964

Here is a fully working solution with a Stackblitz Demo with merging of similar cells.

You have to calculate a row span for each item and bind it to the rowspan attribute of the td. You also have to conditionally render the td to display it only for the first item for each state.

For this, you can create a preprocessed array, ordered by state and by county, with added span properties for states and counties.

To set the span properties, you can count the number of children for each state and each county for the state by filtering the original array.

The goal is to get a array like this:

[
  {state: "CA", county: "2", item: 0.019, stateSpan: 3, countySpan: 2},
  {state: "CA", county: "2", item: 0.037, stateSpan: 0, countySpan: 0},
  {state: "CA", county: "3", item: 0.14, stateSpan: 0, countySpan: 1},
  {state: "MN", county: "1", item: 0.297, stateSpan: 4, countySpan: 3},
  {state: "MN", county: "1", item: 0.04, stateSpan: 0, countySpan: 0},
  {state: "MN", county: "1", item: 0.0374, stateSpan: 0, countySpan: 0},
  {state: "MN", county: "3", item: 0.14, stateSpan: 0, countySpan: 1}
]

Here is the code:

 <table>
    <tr>
      <th>State</th>
      <th>County</th>
      <th>Item</th>
    </tr>
    <tr *ngFor="let item of dataExt">
      <td [attr.rowspan]="item.stateSpan" *ngIf="item.stateSpan">{{ item.state }}</td>
      <td [attr.rowspan]="item.countySpan" *ngIf="item.countySpan">{{ item.county }}</td>
      <td>{{ item.item }}</td>
    </tr>
</table>
export class AppComponent  {
  data = [
    { state: 'MN', county: '1', item: 0.297 },
    { state: 'MN', county: '1', item: 0.04 },
    { state: 'MN', county: '3', item: 0.14 },
    { state: 'CA', county: '2', item: 0.019 },
    { state: 'MN', county: '1', item: 0.0374 }, 
    { state: 'CA', county: '2', item: 0.037 },
    { state: 'CA', county: '3', item: 0.14 }
  ];

  dataExt: any[] = [];

  constructor() {
    this.processData();
  }

  private processData() {
    const statesSeen = {};
    const countiesSeen = {};

    this.dataExt = this.data.sort((a, b) => {
      const stateComp = a.state.localeCompare(b.state);
      return stateComp ? stateComp : a.county.localeCompare(b.county);
    }).map(x => {
      const stateSpan = statesSeen[x.state] ? 0 :
        this.data.filter(y => y.state === x.state).length;

      statesSeen[x.state] = true;

      const countySpan = countiesSeen[x.state] && countiesSeen[x.state][x.county] ? 0 :
        this.data.filter(y => y.state === x.state && y.county === x.county).length;

      countiesSeen[x.state] = countiesSeen[x.state] || {};
      countiesSeen[x.state][x.county] = true;

      return { ...x, stateSpan, countySpan };
    });
  }
}

Here is the resulting table:

enter image description here

Upvotes: 1

Richard Francis
Richard Francis

Reputation: 491

Perhaps something like this:

interface Item {
  state?: string;
  county?: string;
  item: number;
}

private processData(data: any[]): Item[] {
  // Sort alphabetically
  const sorted = data.sort((a, b) => {
    if(a.state < b.state) { return -1; }
    if(a.state > b.state) { return 1; }
    return 0;
  });

  // Return original object if state value is different to
  // previous object (or if first iteration). Otherwise
  // return object with sole `item` property.
  return sorted.map((el, i) => {
    if (i === 0 || sorted[i-1].state !== el.state) {
      return el;
    } else {
      return {
        item: el.item
      };
    }
  });
}

That should be O(log n) complexity and the result will look like this:

[
  { state: 'CA', county: '2', item: 0.019 },
  { item: 0.037 },
  { state: 'MN', county: '1', item: 0.297 },
  ...
]

Upvotes: 0

Eliseo
Eliseo

Reputation: 57929

if you has the data ordered you can use let i=index and check if the before data is eauql using a conditional operator

<ng-container *ngFor="let dataitem of data;let i=index">
<tr>
  <td>{{i>0 && data[i-1].state==dataitem.state?'':dataitem.state}}</td>
  <td>{{i>0 && data[i-1].county==dataitem.county?'':dataitem.county}}</td>
  <td>{{dataitem.item}}</td>
</tr>
</ng-container>

Upvotes: 3

Leon Radley
Leon Radley

Reputation: 7672

transform your data into multiple arrays using lodash.

https://lodash.com/docs/4.17.11#groupBy

import { groupBy } from 'lodash-es'
const states = groupBy(data, 'state')

in your table you can have multiple tbodies

<tbody *ngFor="let state of states">...</tbody>

Upvotes: 2

user10747134
user10747134

Reputation:

Using this answer as a guide, you can groom your data ahead of time (or use a pipe) to create nested objects.

Something along the lines of (pulled roughly from the linked example)

key = 'state';
data = data.reduce((data, x) => {
    (data[x[key]] = data[x[key]] || []).push(x);
    return data;
  }, {});

should produce something similar to

data = [
    { 'MN': [
        { county: '1', item: 0.297 }
        ....
    ]}
]

Alternatively you can use fancy *ngIf and index logic if your list is sorted. Read more here. Something along the lines of:

<div *ngFor="let item of list; let i = index;">
  <div *ngIf="i > 0 && item.state !== list[i-1].state">{{item.state}}</div>
</div>

Upvotes: 1

Related Questions