# Table Sections

**Category:** Features/Display/Table Sections

## Design

### Description

Group [`Table`](./?path=/story/base-components-collections-table-table--table) data into sections.

#### Usage

1. Create a [`Table`](./?path=/story/base-components-collections-table-table--table) collection component.
1. In the `fetchData` function of the [`useTableCollection()`](./?path=/story/base-components-collections-table-usetablecollection--usetablecollection) hook, return data grouped by the desired field for creating sections. You can use `sort` for this, or simply return the data grouped by the field in a flat array. Patterns detects this field and sections the table data by its values.
1. Define the `sections` prop on the `Table`. The prop must be a [`TableSectionsProp`](./?path=/story/common-types--tablesectionsprop) object, where the `renderSection` callback returns a [`Section`](./?path=/story/common-types--collectionsection) object with `id`, `title`, and optional `primaryAction` properties.

```tsx
import { TableSections } from '@wix/patterns';
```

### Demo

This example sorts and groups the data by `jobTitle`.

```tsx
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import { Table, TableSections, useTableCollection } from '@wix/patterns';
import { contacts } from '@wix/crm';
import { Avatar } from '@wix/design-system';
import { AddSmall } from '@wix/wix-ui-icons-common';

function TableSectionsExample() {
  const groupBy = React.useCallback(
    (item: contacts.Contact) => item.info?.jobTitle as string,
    [],
  );

  const renderSection = React.useCallback(
    (sectionId: string) => ({
      title: sectionId,
      primaryAction: {
        id: 'primary-action',
        label: 'Primary Action',
        prefixIcon: <AddSmall />,
        onClick: () => {},
      },
    }),
    [],
  );

  const table = useTableCollection<contacts.Contact>({
    queryName: 'TableSections',
    paginationMode: 'offset',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query) => {
      const { limit, offset, search } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      queryBuilder.ascending('info.jobTitle');

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          sections={{
            TableSections,
            groupBy,
            renderSection,
          }}
          state={table}
          columns={[
            {
              id: 'avatar',
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'jobTitle',
              title: 'Job Title',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'email',
              title: 'Email',
              render: (contact) => contact.primaryEmail,
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}

export default TableSectionsExample;
```

### With Badge

This example shows how to use a badge in the section header.

```tsx
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Section,
  Table,
  TableSections,
  useTableCollection,
} from '@wix/patterns';
import { contacts } from '@wix/crm';
import { Avatar } from '@wix/design-system';
import { AddSmall } from '@wix/wix-ui-icons-common';

function TableSectionsExample() {
  const groupBy = React.useCallback((item: contacts.Contact) => {
    const name = item.info?.name?.first;
    const jobTitle = item.info?.jobTitle;
    return `${jobTitle} - ${name?.slice(0, 1).toUpperCase() || 'other'}`;
  }, []);

  const renderSection = React.useCallback((sectionId: string) => {
    const firstLetter = sectionId.split(' - ')[1]?.toLowerCase();

    return {
      title: sectionId,
      primaryAction: {
        id: 'primary-action',
        label: 'Primary Action',
        prefixIcon: <AddSmall />,
        onClick: () => {},
      },
      badge: {
        visible: true,
        skin: firstLetter === 'a' ? 'danger' : 'light',
      } as Section['badge'],
    };
  }, []);

  const table = useTableCollection<contacts.Contact>({
    queryName: 'TableSections',
    paginationMode: 'offset',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query) => {
      const { limit, offset, search } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      queryBuilder.ascending('info.jobTitle');
      queryBuilder.ascending('info.name.first');
      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          sections={{
            TableSections,
            groupBy,
            renderSection,
          }}
          state={table}
          columns={[
            {
              id: 'avatar',
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'jobTitle',
              title: 'Job Title',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'locale',
              title: 'Locale',
              render: (contact) => contact.info?.locale,
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}

export default TableSectionsExample;
```

### With Summary

This example shows how to use a summary in the section header.

```tsx
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Section,
  Table,
  TableSections,
  useTableCollection,
} from '@wix/patterns';
import { contacts } from '@wix/crm';
import { Avatar } from '@wix/design-system';
import { BarSmall, DrinkSmall, AddSmall } from '@wix/wix-ui-icons-common';

function TableSectionsExample() {
  const groupBy = React.useCallback((item: contacts.Contact) => {
    return item.info?.jobTitle as string;
  }, []);

  const renderSection = React.useCallback(
    (sectionId: string, items: contacts.Contact[]) => {
      return {
        title: sectionId,
        primaryAction: {
          id: 'primary-action',
          label: 'Primary Action',
          prefixIcon: <AddSmall />,
          onClick: () => {},
        },
        badge: {
          visible: true,
          skin: 'light',
        } as Section['badge'],
        summary: [
          {
            icon: <BarSmall />,
            text: 'English',
            count: items.filter((item) => item.info?.locale === 'en-US').length,
          },
          {
            icon: <DrinkSmall />,
            text: 'French',
            count: items.filter((item) => item.info?.locale === 'fr-FR').length,
          },
        ],
      };
    },
    [],
  );

  const table = useTableCollection<contacts.Contact>({
    queryName: 'TableSections',
    paginationMode: 'offset',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query) => {
      const { limit, offset, search } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      queryBuilder.ascending('info.jobTitle');
      queryBuilder.ascending('info.name.first');
      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          sections={{
            TableSections,
            groupBy,
            renderSection,
          }}
          state={table}
          columns={[
            {
              id: 'avatar',
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'jobTitle',
              title: 'Job Title',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'locale',
              title: 'Locale',
              render: (contact) => contact.info?.locale,
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}

export default TableSectionsExample;
```

### With Collapse

This example shows how to use a collapse action in the section header.

```tsx
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import { Table, TableSections, useTableCollection } from '@wix/patterns';
import { contacts } from '@wix/crm';
import { Avatar } from '@wix/design-system';

function TableSectionsExample() {
  const [collapsedSections, setCollapsedSections] = React.useState<Set<string>>(new Set());

  const groupBy = React.useCallback(
    (contact: contacts.Contact) => contact.info?.jobTitle ?? 'Unknown',
    [],
  );
  const renderSection = React.useCallback(
    (sectionId: string) => {
      return {
        title: sectionId,
        collapsed: collapsedSections.has(sectionId),
      };
    },
    [collapsedSections],
  );

  const table = useTableCollection<contacts.Contact>({
    queryName: 'TableSections',
    paginationMode: 'offset',
    itemName: (item) => `${item.info?.name?.first} ${item.info?.name?.last}`,
    fetchData: (query) => {
      const { limit, offset, search } = query;

      let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

      if (search) {
        queryBuilder = queryBuilder.startsWith('info.name.first', search);
      }

      queryBuilder.ascending('info.jobTitle');

      return queryBuilder.find().then(({ items = [], totalCount: total }) => ({
        items,
        total,
      }));
    },
    fetchErrorMessage: ({ err }) => String(err),
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <Table
          sections={{
            TableSections,
            groupBy,
            renderSection,
            collapsible: true,
            events: {
              onToggle: (sectionId, isCollapsed) => {
                if (isCollapsed) {
                  setCollapsedSections(new Set(collapsedSections.add(sectionId)));
                } else {
                  collapsedSections.delete(sectionId);
                  setCollapsedSections(new Set(collapsedSections));
                }
              },
            },
          }}
          state={table}
          columns={[
            {
              id: 'avatar',
              title: '',
              width: '50px',
              render: (contact) => (
                <Avatar
                  name={`${contact.info?.name?.first} ${contact.info?.name?.last}`}
                />
              ),
            },
            {
              id: 'name',
              title: 'Name',
              width: '250px',
              render: (contact) =>
                `${contact.info?.name?.first} ${contact.info?.name?.last}`,
            },
            {
              id: 'jobTitle',
              title: 'Job Title',
              render: (contact) => contact.info?.jobTitle,
            },
            {
              id: 'email',
              title: 'Email',
              render: (contact) => contact.primaryEmail,
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}

export default TableSectionsExample;
```

