vuetify icon indicating copy to clipboard operation
vuetify copied to clipboard

feat(VDataTable): add slot for group header columns

Open xyven1 opened this issue 9 months ago • 20 comments

Description

This resolves #18278 by adding [`group.${string}`] slots to VDataTable.

Added a set of slots [`group.${string}`] to v-data-table much like [`item.${string}`], which will allow people to override what is rendered for the corresponding column in a group header. The slot will be forwarded the item prop from VDataTableGroupHeaderRow allowing users to take advantage of group depth, key, and all the other things which data-table-group and group-header slots have access to.

Playground:

<template>
  <v-app>
    <v-container>
      <v-chip-group v-model="groupBy" multiple filter>
        <v-chip :value="{ key: 'city', order: 'asc' }">
          City
        </v-chip>
        <v-chip :value="{ key: 'sport', order: 'asc' }">
          Sport
        </v-chip>
      </v-chip-group>
      <v-data-table
        :group-by="groupBy"
        :headers="headers"
        :items="sports"
        :items-per-page="20"
      >
        <template #[`group.active`]="{ item }">
          {{ getItemsRecursive(item).reduce((a, v) => v.raw.active + a, 0) }}
        </template>
        <template #[`group.inactive`]="{ item }">
          {{ getItemsRecursive(item).reduce((a, v) => v.raw.inactive + a, 0) }}
        </template>
      </v-data-table>
    </v-container>
  </v-app>
</template>

<script setup lang="ts">
  import { Group } from '@/labs/VDataTable/composables/group'
  import { SortItem } from '@/labs/VDataTable/composables/sort'
  import { DataTableHeader } from '@/labs/VDataTable/types'
  import { ref } from 'vue'

  const groupBy = ref<readonly SortItem[]>([{ key: 'sport', order: 'asc' }, { key: 'city', order: 'asc' }])

  type Team = {
    name: string
    city: string
    sport: string,
    active: number
    inactive: number
  }

  const getItemsRecursive = (group:Group) => {
    let items = group.items
    while (items[0].items) {
      items = items.flatMap((v:Group) => v.items)
    }
    return items
  }
  const headers: DataTableHeader[] = [
    {
      title: 'Name',
      align: 'start',
      key: 'name',
    },
    {
      title: 'City',
      align: 'start',
      key: 'city',
    },
    {
      title: 'Sport',
      align: 'start',
      key: 'sport',
    },
    { title: 'Active Players', key: 'active', align: 'end' },
    { title: 'Inactive Players', key: 'inactive', align: 'end' },
  ]
  const sports: Team[] = [
    {
      name: 'Celtics',
      city: 'Boston',
      sport: 'Basketball',
      active: 10,
      inactive: 5,
    },
    {
      name: 'Lakers',
      city: 'Los Angeles',
      sport: 'Basketball',
      active: 15,
      inactive: 3,
    },
    {
      name: '49ers',
      city: 'San Francisco',
      sport: 'Football',
      active: 12,
      inactive: 4,
    },
    {
      name: 'Giants',
      city: 'San Francisco',
      sport: 'Baseball',
      active: 16,
      inactive: 2,
    },
    {
      name: 'Warriors',
      city: 'San Francisco',
      sport: 'Basketball',
      active: 14,
      inactive: 4,
    },
    {
      name: 'Sharks',
      city: 'San Jose',
      sport: 'Hockey',
      active: 17,
      inactive: 3,
    },
    {
      name: 'Raiders',
      city: 'Las Vegas',
      sport: 'Football',
      active: 11,
      inactive: 5,
    },
    {
      name: 'Golden Knights',
      city: 'Las Vegas',
      sport: 'Hockey',
      active: 19,
      inactive: 1,
    },
    {
      name: 'Heat',
      city: 'Miami',
      sport: 'Basketball',
      active: 16,
      inactive: 2,
    },
    {
      name: 'Marlins',
      city: 'Miami',
      sport: 'Baseball',
      active: 14,
      inactive: 4,
    },
    {
      name: 'Dolphins',
      city: 'Miami',
      sport: 'Football',
      active: 12,
      inactive: 4,
    },
    {
      name: 'Lightning',
      city: 'Tampa Bay',
      sport: 'Hockey',
      active: 18,
      inactive: 2,
    },
    {
      name: 'Rays',
      city: 'Tampa Bay',
      sport: 'Baseball',
      active: 15,
      inactive: 3,
    },
    {
      name: 'Buccaneers',
      city: 'Tampa Bay',
      sport: 'Football',
      active: 14,
      inactive: 2,
    },
    {
      name: 'Timberwolves',
      city: 'Minnesota',
      sport: 'Basketball',
      active: 11,
      inactive: 5,
    },
    {
      name: 'Twins',
      city: 'Minnesota',
      sport: 'Baseball',
      active: 17,
      inactive: 1,
    },
    {
      name: 'Bruins',
      city: 'Boston',
      sport: 'Hockey',
      active: 20,
      inactive: 4,
    },
    {
      name: 'Red Sox',
      city: 'Boston',
      sport: 'Baseball',
      active: 15,
      inactive: 3,
    },
    {
      name: 'Yankees',
      city: 'New York',
      sport: 'Baseball',
      active: 17,
      inactive: 5,
    },
    {
      name: 'Mets',
      city: 'New York',
      sport: 'Baseball',
      active: 13,
      inactive: 2,
    },
    {
      name: 'Dodgers',
      city: 'Los Angeles',
      sport: 'Baseball',
      active: 18,
      inactive: 1,
    },
    {
      name: 'Angels',
      city: 'Los Angeles',
      sport: 'Baseball',
      active: 14,
      inactive: 4,
    },
  ]
</script>

EDIT: Update the playground section to use better example

xyven1 avatar Sep 18 '23 23:09 xyven1

Assuming some docs are needed to pull this PR. Let me know if that is the case.

xyven1 avatar Sep 18 '23 23:09 xyven1

Assuming some docs are needed to pull this PR. Let me know if that is the case.

https://github.com/vuetifyjs/vuetify/blob/dfafe8946afd2ed340a6c2d29c88383b320d13a9/packages/api-generator/src/locale/en/VDataTable.json#L39 and probably some others, you'd need to build vuetify and api-generator, run the docs and find missing slot descriptions in the api of data table components

jacekkarczmarczyk avatar Sep 19 '23 05:09 jacekkarczmarczyk

<template #group.category="{ item }">

Is this supposed to be group.dairy? Doesn't make much sense to show dairy in the category column.

KaelWD avatar Sep 19 '23 09:09 KaelWD

This example also doesn't really work with nested groups ([{ key: 'dairy' }, { key: 'category' }]), {{ item.value }} puts each group value in the same column without extra checks. Do you have a better example of how this might be used?

KaelWD avatar Sep 19 '23 09:09 KaelWD

If it's just for showing the grouped value we probably need to be doing this by default anyway: Screenshot_20230919_191056

KaelWD avatar Sep 19 '23 09:09 KaelWD

Yeah, I have a more specific example. I'll write it up sometime today and post the code here with some screen shots

xyven1 avatar Sep 19 '23 10:09 xyven1

Here is a better example @KaelWD. Im noticing some definite pain points with the current implementation, such as not having access to previous the entire nested groupBy keys, and having to do stuff the getItemsRecursive (in Playground.vue below) image

Playground.vue

<template>
 <v-app>
   <v-container>
     <v-chip-group v-model="groupBy" multiple filter>
       <v-chip :value="{ key: 'city', order: 'asc' }">
         City
       </v-chip>
       <v-chip :value="{ key: 'sport', order: 'asc' }">
         Sport
       </v-chip>
     </v-chip-group>
     <v-data-table
       :group-by="groupBy"
       :headers="headers"
       :items="sports"
       :items-per-page="20"
     >
       <template #[`group.active`]="{ item }">
         {{ getItemsRecursive(item).reduce((a, v) => v.raw.active + a, 0) }}
       </template>
       <template #[`group.inactive`]="{ item }">
         {{ getItemsRecursive(item).reduce((a, v) => v.raw.inactive + a, 0) }}
       </template>
     </v-data-table>
   </v-container>
 </v-app>
</template>

<script setup lang="ts">
 import { Group } from '@/labs/VDataTable/composables/group'
 import { SortItem } from '@/labs/VDataTable/composables/sort'
 import { DataTableHeader } from '@/labs/VDataTable/types'
 import { ref } from 'vue'

 const groupBy = ref<readonly SortItem[]>([{ key: 'sport', order: 'asc' }, { key: 'city', order: 'asc' }])

 type Team = {
   name: string
   city: string
   sport: string,
   active: number
   inactive: number
 }

 const getItemsRecursive = (group:Group) => {
   let items = group.items
   while (items[0].items) {
     items = items.flatMap((v:Group) => v.items)
   }
   return items
 }
 const headers: DataTableHeader[] = [
   {
     title: 'Name',
     align: 'start',
     key: 'name',
   },
   {
     title: 'City',
     align: 'start',
     key: 'city',
   },
   {
     title: 'Sport',
     align: 'start',
     key: 'sport',
   },
   { title: 'Active Players', key: 'active', align: 'end' },
   { title: 'Inactive Players', key: 'inactive', align: 'end' },
 ]
 const sports: Team[] = [
   {
     name: 'Celtics',
     city: 'Boston',
     sport: 'Basketball',
     active: 10,
     inactive: 5,
   },
   {
     name: 'Lakers',
     city: 'Los Angeles',
     sport: 'Basketball',
     active: 15,
     inactive: 3,
   },
   {
     name: '49ers',
     city: 'San Francisco',
     sport: 'Football',
     active: 12,
     inactive: 4,
   },
   {
     name: 'Giants',
     city: 'San Francisco',
     sport: 'Baseball',
     active: 16,
     inactive: 2,
   },
   {
     name: 'Warriors',
     city: 'San Francisco',
     sport: 'Basketball',
     active: 14,
     inactive: 4,
   },
   {
     name: 'Sharks',
     city: 'San Jose',
     sport: 'Hockey',
     active: 17,
     inactive: 3,
   },
   {
     name: 'Raiders',
     city: 'Las Vegas',
     sport: 'Football',
     active: 11,
     inactive: 5,
   },
   {
     name: 'Golden Knights',
     city: 'Las Vegas',
     sport: 'Hockey',
     active: 19,
     inactive: 1,
   },
   {
     name: 'Heat',
     city: 'Miami',
     sport: 'Basketball',
     active: 16,
     inactive: 2,
   },
   {
     name: 'Marlins',
     city: 'Miami',
     sport: 'Baseball',
     active: 14,
     inactive: 4,
   },
   {
     name: 'Dolphins',
     city: 'Miami',
     sport: 'Football',
     active: 12,
     inactive: 4,
   },
   {
     name: 'Lightning',
     city: 'Tampa Bay',
     sport: 'Hockey',
     active: 18,
     inactive: 2,
   },
   {
     name: 'Rays',
     city: 'Tampa Bay',
     sport: 'Baseball',
     active: 15,
     inactive: 3,
   },
   {
     name: 'Buccaneers',
     city: 'Tampa Bay',
     sport: 'Football',
     active: 14,
     inactive: 2,
   },
   {
     name: 'Timberwolves',
     city: 'Minnesota',
     sport: 'Basketball',
     active: 11,
     inactive: 5,
   },
   {
     name: 'Twins',
     city: 'Minnesota',
     sport: 'Baseball',
     active: 17,
     inactive: 1,
   },
   {
     name: 'Bruins',
     city: 'Boston',
     sport: 'Hockey',
     active: 20,
     inactive: 4,
   },
   {
     name: 'Red Sox',
     city: 'Boston',
     sport: 'Baseball',
     active: 15,
     inactive: 3,
   },
   {
     name: 'Yankees',
     city: 'New York',
     sport: 'Baseball',
     active: 17,
     inactive: 5,
   },
   {
     name: 'Mets',
     city: 'New York',
     sport: 'Baseball',
     active: 13,
     inactive: 2,
   },
   {
     name: 'Dodgers',
     city: 'Los Angeles',
     sport: 'Baseball',
     active: 18,
     inactive: 1,
   },
   {
     name: 'Angels',
     city: 'Los Angeles',
     sport: 'Baseball',
     active: 14,
     inactive: 4,
   },
 ]
</script>

xyven1 avatar Sep 19 '23 11:09 xyven1

Noticing some things immediately and wondering if I should fix. https://github.com/vuetifyjs/vuetify/blob/dfafe8946afd2ed340a6c2d29c88383b320d13a9/packages/api-generator/src/locale/en/VDataTable.json#L39 is missing "[`column.${string}`]" slot, and the description for "[`column.${string}`]" in VDataTableVirtual.json is not the clear, if I am understanding the code correctly. Looks like the column prop is for rendering a custom cell in the header, not complete custom rendering of a column, which doesn't even make sense given how data tables are oragnized.

Should I address these things in this PR or would that be extending the scope of the PR too far. Let me know

xyven1 avatar Sep 19 '23 18:09 xyven1

Would it be possible to sort the headers also based on columns first followed by grouped rows. Essentially it’s a multi sort.

subinznz avatar Sep 26 '23 10:09 subinznz

Would it be possible to sort the headers also based on columns first followed by grouped rows. Essentially it’s a multi sort.

I'm a little confused, what exactly do you mean? Data tables support multi-sort already, and sorting while rows are grouped also works quite well

xyven1 avatar Sep 26 '23 12:09 xyven1

In your example screen shot you have a sum of all the active players for each sport e.g Baseball. What I would like to know is if the groups themselves can be sorted first with this calculated value ( e.g decscending order - highest numbers of total players in the group to lowest number of total players )

subinznz avatar Sep 26 '23 12:09 subinznz

Hmm yeah if you want sorting the sum would have to be done through the groupBy object somehow, can't sort slot contents.

KaelWD avatar Sep 26 '23 13:09 KaelWD

Actually my groups sort for some reasons when i sort by clicking the headers so I was suprised when I was testing the examples that the entire group did not move. I will try reproduce on playground. I’m not sure if this is a feature or a bug. Because the functionality of being able to sort the entire group itself is what I want.

Edit: looks the reason I was able to achieve sorting by group was because of the groupBy object { key: 'sport', order: 'asc' } If I remove the order pair and only have key e.g { key: 'sport' } then the groups will reorder.

subinznz avatar Sep 26 '23 13:09 subinznz

Yeah that'll sort the groups by the grouped value (sport name in alphabetical order), there's no way to sort by count or some other aggregate though. With no defined order it'll go by order of first appearance in the data after non-group sorting.

KaelWD avatar Sep 26 '23 14:09 KaelWD

Any hint on which files I should look or how you would go about implementing this ? , I want to try experiment and get something working , a feature request and PR hopefully.

Thanks

subinznz avatar Sep 26 '23 14:09 subinznz

packages/vuetify/src/labs/VDataTable/composables/group.ts

I'd probably do it on headers actually, something like this maybe?

{
  title: 'Active Players', 
  key: 'active', 
  align: 'end',
  aggregate: ({ group }) => group.items.reduce((a, v) => v.raw.active + a, 0)
}

KaelWD avatar Sep 26 '23 14:09 KaelWD

Yeah, this makes the most sense to me. By defining an aggregate function for each column, the slot can be passed the aggregated value along with the rest of the context, and thus the slot is just for overriding default rendering like with normal columns. This addresses the awkwardness of the recursive sum that I did in my example, and so I think this is the best move. In a sense to a slightly separate issue to what this PR is addressing, is this PR is just about the slot itself, but I feel like it is related closely enough to be worth addressing in this PR as well.

On a side note, I'm going to continue working in this PR I'm going to need a little bit of direction as this is ending up requiring some knowledge of the framework's inner workings that I don't quite have

xyven1 avatar Sep 28 '23 20:09 xyven1

Until such a time as the aggregate issue is addressed this feels complete. I addressed all docs I could find, so I think this can be merged. I performed all the checks I could find in the npm scripts, but if I'm missing something I'm happy to address it.

xyven1 avatar Oct 17 '23 14:10 xyven1

Please resolve merge conflicts, ty.

johnleider avatar Mar 28 '24 19:03 johnleider

Hey, I haven't looked at this PR in a while. Like I mentioned in my previous comment, the aggregation API could in a certain way make this PR obsolete. I'm happy to update it if it's still a desired feature.

xyven1 avatar Mar 28 '24 21:03 xyven1