Component that groups similar items of passed property

Hi

I have written a compontent that “almost” does what the title says.

i am passing a list of objects to my component

[
{'id':1,'name':'Tom'},
{'id':2,'name':'Tom'},
{'id':3,'name':'Sue'},
{'id':4,'name':'Mae'},
{'id':5,'name':'Mae'}
]

the component should render a table with a row for each name. if 2 or more rows with the same name follow each other the should be collapsed into 1 row and a counter should be displayed (the id will be hidden).

count | name | id
2     | Tom  |
      | Sue  | 3
2     | Mae  |

if I click the cnt I want the collapsed rows to expand (example after clicking the 2 in the Tom row)

count | name | id
2     | Tom  |
      |      | 1
      |      | 2
      | Sue  | 3
2     | Mae  |

I always keep coming back to the problem that I dont know how to keep a list of those rows that are collapsed persistently.

I would reorganize the data such that

would become (e.g.)

[ 
  {'name':'Tom', ids: [1,2]},
  {'name':'Sue', ids: [3]},
  {'name':'Mae', ids: [4,5]}
]

and then create a rows-group component that receives {'name':'Tom', ids: [1,2]} and takes care of the rest has its own status of count, isCollapsed and so on and hanldes the @click.

1 Like

thanks for the feedback, some issues that I dont know will resolve with your approach

lets say I keep the status in the rows-group component.
I guess what you mean by that is that the component has its own data() where i keep said state. right?

data() { return {
                  isCollapsed: []
}}

How do you propose I connect the passed in rows with the ‘extra’ state of the component?

If one of the passed in rows is deleted on the parent component. Vuejs apparently cannot tell me what changed in the array. I dont know which row has changed and I cannot connect the passed in array with the component state properly. (note: I dont have a unique id, there might be 2 occurences of row-groups with the same name in the list)

count | name | id
2-    | Tom  |
      |      | 1
      |      | 2
      | Sue  | 3
2+    | Mae  |
2-    | Tom  |
      |      | 6
      |      | 7

I created a small repo that contains a quick hack of what I had in mind: https://github.com/fiedsch/vue-grouped-table. Hope this helps you.

hi fiedsch, I created this fiddle that should help

taking a look at your link now

Question 1: using name as identifier doesnt work because all rows with the same name are collapsed/expanded together instead of separately. what to use instead? if i use the index i cannot keep state if the rows are updated from the parent.

your repo seems empty.

Can’t reproduce. I accessed it without being logged in to github, and it worked. Did you look into src/?

You posted this link. https://github.com/fiedsch/vue-grouped-table

the repo has 1 commit named ‘init’

there is no component aside from HelloWorld which is only boilerplate generated from vue-cli.

did you forget to commit&push your hack?

Your data shoud have some attribute that identifies records that belong together. How do you tell if a record for name; 'Tom' belongs to a first Tom and another one for name; 'Tom' would belont to the forst Tom or another Tom?

For me, in your fiddle the RowsGroup component has a misleading name. It represents the complete table instead of just a group of rows.

Ok, sorry. I just updated it and also created an instance on codesandbox.io: https://codesandbox.io/s/sharp-jepsen-ymc0n which is probably easier to use.

1 Like

-) you are right with the misleading component name.

-) rows that belong together are identified by their adjacency and a common attribute. thats what I have to work with. And that works fine until vuejs updates the list and doesnt tell me what was updated.

in the latest version of https://codesandbox.io/s/romantic-elgamal-4yvdo I have a computed that creates an id for each group. but if the source list changes, that information is lost. I might be able to work around that by creating an extra datastructure that keeps the ids. But this is totally against what I am trying to do, which is to keep the state in the component.

I only have the order of the array to establish identity.
If vuejs updates the list and doesnt tell me what was updated I cannot transfer the old state to the new state.

I’m not sure if I understand what you mean by old and new state.

I was playing a bit with the code you provided and would propose this change:

if (!r.id) {
  r.id = Math.random()
          .toString(36)
          .replace(/[^a-z]+/g, "")
          .substr(0, 5);
}

i.e. genarate an id only if it does not yet exist. That would leave exsiting ids unchanged.

your change doesnt do anything because that code is inside a computed and results is built from scratch everytime the underlying rows list is changed.

old/new state
isCollapsed keeps the state of minimized/maximized rows. (old state)
if the input data is changed I want to make sure that the collapsed/expanded state is kept.

in my fiddle. open one of the collapsed rows, then click one of the add buttons. all rows will be minimized. I want to keep the old state somehow.

re my change: you are right (didn’t test it)

re: old/new state: i think you have to have some unique ID that comes from your data. If you have that, you might assign the ID of the first entry to the group of entries.
Excmple: your

  { id: 1, name: "Tom" },
  { id: 2, name: "Tom" },

would become

{ name: r.name, group_id: 1, ids: [1,2] };

and for entries like

 { id: undefined, name: "Mae" }

you should generate an ID once (for example in mounted() you could iterate over your rows and add an id if not defined).

Same for your button’s @click methods:

<button @click="rows.push({id:undefined, name:'Tom'})">add Tom</button>

might become

<button @click="addRow('Tom')">add Tom</button>

with

methods: {
  addRow(name) {
    let id = this.generateNewId()
    this.rows.push({ id:id, name:name })
  },
  generateNewId() {
    // either the value of an incremented counter or
    // something similar to your example where you use Math.random() 
  }
}
1 Like

I really appreciate for your help. Had to get that out.

your idea appears to work, I implemented it in the fiddle already.

now i need to work out deletion, shouldnt be to hard.

one thing left is how make it work without having that extra logic pollute the parent component.

I will come back and post when I made some progress,

1 Like

[SOLVED]

I finished the cleanup/refactoring.

the component is pretty clean now, the template is passed in from the parent via slots.

App.vue

<template>
  <div id="app">
    <RowsGroup ref="foo" :rows="rows">
      <template v-slot:default="slotProps">
        <button @click="slotProps.addRow({id:undefined, name:'Tom'})">add Tom</button>
        <button @click="addRow({id:undefined, name:'Sue'})">add Sue</button>
        <button @click="addRow({id:undefined, name:'Mae'})">add Mae</button>
        <button @click="this.console.log(slotProps.getRows())">getRows()</button>
        <table>
          <tr>
            <th></th>
            <th>count</th>
            <th>name</th>
            <th>id</th>
            <th>actions</th>
          </tr>
          <template v-for="row, index in slotProps.rows">
            <tr>
              <td @click="row.collapsed = !row.collapsed">{{ row.collapsed ? '+' : '-'}}</td>
              <td>{{row.children.length}}</td>
              <td>{{row.collapsed ? row.name : ''}}</td>
              <td></td>
              <td></td>
            </tr>
            <template v-if="!row.collapsed">
              <tr v-for="child,child_index in row.children">
                <td></td>
                <td></td>
                <td>{{child.name}}</td>
                <td>{{child.id}}</td>
                <td>
                  <button @click="slotProps.deleteRow(row,index,child_index)">delete</button>
                </td>
              </tr>
            </template>
          </template>
        </table>
      </template>
    </RowsGroup>
  </div>
</template>

<script>
import RowsGroup from "./components/RowsGroup";

export default {
  name: "App",
  components: {
    RowsGroup
  },
  data: function() {
    return {
      rows: [
        { id: 1, name: "Tom" },
        { id: 2, name: "Tom" },
        { id: 3, name: "Sue" },
        { id: 4, name: "Mae" },
        { id: 5, name: "Mae" },
        { id: 6, name: "Tom" },
        { id: 7, name: "Tom" },
        { id: undefined, name: "Mae" },
        { id: undefined, name: "Mae" }
      ]
    };
  }
};
</script>

<style>
</style>

components/RowsGroup.vue

<template>
  <div>
    <slot :rows="rows2" :addRow="addRow" :deleteRow="deleteRow" :getRows="getRows"></slot>
  </div>
</template>

<script>
export default {
  name: "RowsGroup",
  data() {
    return {
      rows2: []
    };
  },
  props: ["rows"],
  created() {
    let results = [];
    let current_row = { name: "", children: [] };

    this.rows.forEach(r => {
      if (r.name === current_row.name) {
        current_row.children.push(r);
      } else {
        if (current_row.children.length) {
          results.push(current_row);
        }
        current_row = this.makeRow(r);
      }
    });
    if (current_row.children.length) {
      results.push(current_row);
    }
    this.rows2 = results;
  },
  methods: {
    makeId() {
      return Math.random()
        .toString(36)
        .replace(/[^a-z]+/g, "")
        .substr(0, 5);
    },
    makeRow(row) {
      let r = {
        name: row.name,
        id: this.makeId(),
        collapsed: true,
        children: [row]
      };
      this.$set(r, "collapsed", true);
      return r;
    },
    deleteRow(row, row_index, child_index) {
      if (row.children.length === 1) {
        this.rows2.splice(row_index, 1);
      } else {
        row.children.splice(child_index, 1);
      }
    },
    addRow(row) {
      let prev_row = this.rows2[this.rows2.length - 1];
      if (row.name === prev_row.name) {
        prev_row.children.push(row);
      } else {
        this.rows2.push(this.makeRow(row));
      }
    },
    getRows() {
      return this.rows2;
    }
  }
};
</script>

<style scoped>
</style>