Documentation

Documentation versions (currently viewingVaadin 23)

You are viewing documentation for Vaadin 23. View latest documentation

Grid

Grid is a component for showing tabular data.

Open in a
new tab
@state()
private items: Person[] = [];

protected override async firstUpdated() {
  const { people } = await getPeople();
  this.items = people;
}

protected override render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-column path="firstName"></vaadin-grid-column>
      <vaadin-grid-column path="lastName"></vaadin-grid-column>
      <vaadin-grid-column path="email"></vaadin-grid-column>
      <vaadin-grid-column path="profession"></vaadin-grid-column>
    </vaadin-grid>
  `;
}

Content

A basic Grid uses plain text to display information in rows and columns. Rich content can be used to provide additional information in a more legible fashion. Components such as input fields and Button are also supported.

Open in a
new tab
@state()
private items?: Person[];

protected override async firstUpdated() {
  const { people } = await getPeople();
  this.items = people;
}

protected override render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-selection-column></vaadin-grid-selection-column>
      <vaadin-grid-column
        header="Employee"
        flex-grow="0"
        auto-width
        ${columnBodyRenderer(this.employeeRenderer, [])}
      ></vaadin-grid-column>
      <vaadin-grid-column path="profession" auto-width></vaadin-grid-column>
      <vaadin-grid-column
        header="Status"
        auto-width
        ${columnBodyRenderer(this.statusRenderer, [])}
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}

private employeeRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
  <vaadin-horizontal-layout style="align-items: center;" theme="spacing">
    <vaadin-avatar
      img="${person.pictureUrl}"
      name="${person.firstName} ${person.lastName}"
      alt="User avatar"
    ></vaadin-avatar>
    <vaadin-vertical-layout style="line-height: var(--lumo-line-height-m);">
      <span>${person.firstName} ${person.lastName}</span>
      <span style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);">
        ${person.email}
      </span>
    </vaadin-vertical-layout>
  </vaadin-horizontal-layout>
`;

private statusRenderer: GridColumnBodyLitRenderer<Person> = ({ status }) => html`
  <span theme="badge ${status === 'Available' ? 'success' : 'error'}">${status}</span>
`;

Component Renderer vs. Lit Renderer (Flow Only)

As demonstrated in the previous example, custom content can be rendered using component renderers or Lit renderers.

Component Renderer

Component renderers are easy to build, but slow to render. For a given column, they generate a component for each item in the dataset. The rendered components are fully controllable on the server side.

For each rendered cell, Grid creates a corresponding component instance on the server side. A dataset of 100 items and 10 columns using component renderer produce up to 1,000 components that need to be managed. The more components you use in a component renderer, the greater the impact on performance.

Component renderers are very flexible and easy to use, but should be used with caution. They’re better suited as editors since only a single row can be edited at a time. They can also be used for detail rows.

Lit Renderer

Lit renderers will render quickly, but require you to write HTML. To use components with Lit renderers, you need to use their HTML format. Lit templates are immutable, meaning the state of the components can’t be managed on the server side. However, the template can have different representations, depending on the state of the item.

The only data sent from the server, other than the template itself — which is sent only once — is the extra name property of each item.

Lit templates still enable event handling on the server side. However, you can’t, for example, disable or change the text of a button from the event handler. For such situations, use editors instead.

With Lit renderers, the server doesn’t keep track of the components in each cell. It only manages the state of the item in each row. The client side doesn’t have to wait for the server to send missing information about what needs to be rendered. It can use the template to render all of the cells it needs.

For more in-depth information, see Using Lit Renderers with Grid.

Wrap Cell Content

Overflowing cell content is clipped or truncated by default. This variant makes the content wrap instead.

Open in a
new tab
@customElement('grid-wrap-cell-content')
export class Example extends LitElement {
  protected override createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

  @state()
  private items: Person[] = [];

  protected override async firstUpdated() {
    const { people } = await getPeople();
    this.items = people;
  }

  protected override render() {
    return html`
      <vaadin-grid .items="${this.items}" theme="wrap-cell-content">
        <vaadin-grid-column
          header="Image"
          flex-grow="0"
          auto-width
          ${columnBodyRenderer(this.avatarRenderer, [])}
        ></vaadin-grid-column>
        <vaadin-grid-column path="firstName"></vaadin-grid-column>
        <vaadin-grid-column path="lastName"></vaadin-grid-column>
        <vaadin-grid-column
          header="Address"
          ${columnBodyRenderer(this.addressRenderer, [])}
        ></vaadin-grid-column>
      </vaadin-grid>
    `;
  }

  private avatarRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
    <vaadin-avatar
      img="${person.pictureUrl}"
      name="${person.firstName} ${person.lastName}"
      alt="User avatar"
    ></vaadin-avatar>
  `;

  private addressRenderer: GridColumnBodyLitRenderer<Person> = ({ address }) => html`
    <span>${address.street} ${address.city} ${address.zip} ${address.state}</span>
  `;
}

Tooltips can also be used to display content that doesn’t fit into the cell.

Dynamic Height

Grid has a default height of 400 pixels. It becomes scrollable when its items overflow the allocated space.

In addition to setting any fixed or relative value, the height of a grid can be set by the number of items in the dataset. The grid grows and shrinks based on the row count. This disables scrolling and shouldn’t be used for large data sets to avoid performance issues.

Open in a
new tab
<vaadin-grid .items="${this.invitedPeople}" all-rows-visible>
  <vaadin-grid-column header="Name" path="displayName" auto-width></vaadin-grid-column>
  <vaadin-grid-column path="email"></vaadin-grid-column>
  <vaadin-grid-column path="address.phone"></vaadin-grid-column>
  <vaadin-grid-column
    header="Manage"
    ${columnBodyRenderer(this.manageRenderer, [])}
  ></vaadin-grid-column>
</vaadin-grid>

Selection

Selection isn’t enabled by default. Grid supports single- and multi-select. Single-select allows the user to select only one item, while multi-select permits multiple items to be selected.

Single-Selection Mode

In single-selection mode, the user can select and deselect rows by clicking anywhere on the row.

Open in a
new tab
@state()
private items: Person[] = [];

@state()
private selectedItems: Person[] = [];

protected override async firstUpdated() {
  const { people } = await getPeople();
  this.items = people;
}

protected override render() {
  return html`
    <vaadin-grid
      .items="${this.items}"
      .selectedItems="${this.selectedItems}"
      @active-item-changed="${(e: GridActiveItemChangedEvent<Person>) => {
        const item = e.detail.value;
        this.selectedItems = item ? [item] : [];
      }}"
    >
      <vaadin-grid-column path="firstName"></vaadin-grid-column>
      <vaadin-grid-column path="lastName"></vaadin-grid-column>
      <vaadin-grid-column path="email"></vaadin-grid-column>
    </vaadin-grid>
  `;
}

Multi-Select Mode

In multi-select mode, the user can use a checkbox column to select and deselect rows.

Open in a
new tab
@customElement('grid-multi-select-mode')
export class Example extends LitElement {
  protected override createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

  @state()
  private items: Person[] = [];

  protected override async firstUpdated() {
    const { people } = await getPeople();
    this.items = people;
  }

  protected override render() {
    return html`
      <vaadin-grid .items="${this.items}">
        <vaadin-grid-selection-column></vaadin-grid-selection-column>
        <vaadin-grid-column path="firstName"></vaadin-grid-column>
        <vaadin-grid-column path="lastName"></vaadin-grid-column>
        <vaadin-grid-column path="email"></vaadin-grid-column>
      </vaadin-grid>
    `;
  }
}

Columns

Column alignment, freezing (i.e., fixed positioning), grouping, headers and footers, visibility, and width can be configured. Users can be permitted to resize and reorder columns.

Column Alignment

Three different column alignments are supported: left (default), center, and right.

Right alignment is useful when comparing numeric values, as it helps with readability and scannability. Tabular numbers — if the font offers them — or a monospace font could be used to further improve digit alignment.

Open in a
new tab
<vaadin-grid .items="${this.items}">
  <vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
  <vaadin-grid-column
    header="Due"
    ${columnBodyRenderer(() => html`<span>${this.randomDate()}</span>`, [])}
  ></vaadin-grid-column>
  <vaadin-grid-column
    header="Amount"
    text-align="end"
    ${columnBodyRenderer(
      () => html`
        <span style="font-variant-numeric: tabular-nums">${this.randomAmount()}</span>
      `,
      []
    )}
  ></vaadin-grid-column>
</vaadin-grid>

Column Freezing

Columns and column groups can be frozen — made sticky — to exclude them from scrolling a grid, horizontally. This can be useful for keeping the most important columns always visible in a grid with many columns. Freezing columns at the end of the grid is useful, for example, for keeping row actions always visible.

Open in a
new tab
<vaadin-grid-column
  frozen
  header="Name"
  auto-width
  flex-grow="0"
  ${columnBodyRenderer<Person>(
    (person) => html`${person.firstName} ${person.lastName}`,
    []
  )}
></vaadin-grid-column>
<vaadin-grid-column
  frozen-to-end
  auto-width
  flex-grow="0"
  ${columnBodyRenderer(
    () => html`<vaadin-button theme="tertiary-inline">Edit</vaadin-button>`,
    []
  )}
></vaadin-grid-column>

Although it’s technically possible to freeze any column, it should be used primarily to freeze columns at the start or end of the grid, leaving the remaining columns unfrozen.

Column Grouping

It’s possible to group columns together. Grouped columns share a common header and footer. Use this feature to better visualize and organize related or hierarchical data.

Open in a
new tab
@customElement('grid-column-grouping')
export class Example extends LitElement {
  protected override createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }

  @state()
  private items: Person[] = [];

  protected override async firstUpdated() {
    const { people } = await getPeople();
    this.items = people;
  }

  protected override render() {
    return html`
      <vaadin-grid .items="${this.items}">
        <vaadin-grid-column-group header="Name">
          <vaadin-grid-column path="firstName"></vaadin-grid-column>
          <vaadin-grid-column path="lastName"></vaadin-grid-column>
        </vaadin-grid-column-group>
        <vaadin-grid-column-group header="Address">
          <vaadin-grid-column path="address.street"></vaadin-grid-column>
          <vaadin-grid-column path="address.city"></vaadin-grid-column>
          <vaadin-grid-column path="address.zip"></vaadin-grid-column>
          <vaadin-grid-column path="address.state"></vaadin-grid-column>
        </vaadin-grid-column-group>
      </vaadin-grid>
    `;
  }
}

Column Headers & Footers

Each column has its own customizable header and footer. A basic column header shows the name in plain text. Footers are empty and thus hidden by default. Both can contain rich content and components.

Open in a
new tab
@state()
private items: Person[] = [];

protected override async firstUpdated() {
  const { people } = await getPeople();
  this.items = people.map((person) => ({
    ...person,
    displayName: `${person.firstName} ${person.lastName}`,
  }));
}

protected override render() {
  return html`
    <vaadin-grid .items="${this.items}">
      <vaadin-grid-column
        header="Name"
        path="displayName"
        ${columnFooterRenderer(() => html`<span>200 total members</span>`, [])}
      ></vaadin-grid-column>
      <vaadin-grid-column
        ${columnHeaderRenderer(this.subscriberHeaderRenderer, [])}
        ${columnBodyRenderer(this.subscriberRenderer, [])}
        ${columnFooterRenderer(() => html`<span>102 subscribers</span>`, [])}
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="membership"
        ${columnHeaderRenderer(this.membershipHeaderRenderer, [])}
        ${columnFooterRenderer(() => html`<span>103 regular, 71 premium , 66 VIP</span>`, [])}
      ></vaadin-grid-column>
    </vaadin-grid>
  `;
}

private subscriberHeaderRenderer = () => html`
  <vaadin-horizontal-layout style="align-items: center;">
    <span>Subscriber</span>
    <vaadin-icon
      icon="vaadin:info-circle"
      title="Subscribers are paying customers"
      style="height: var(--lumo-font-size-m); color: var(--lumo-contrast-70pct);"
    ></vaadin-icon>
  </vaadin-horizontal-layout>
`;

private subscriberRenderer: GridColumnBodyLitRenderer<Person> = (person) =>
  html`<span>${person.subscriber ? 'Yes' : 'No'}</span>`;

private membershipHeaderRenderer = () => html`
  <vaadin-horizontal-layout style="align-items: center;">
    <span>Membership</span>
    <vaadin-icon
      icon="vaadin:info-circle"
      title="Membership levels determines which features a client has access to"
      style="height: var(--lumo-font-size-m); color: var(--lumo-contrast-70pct);"
    ></vaadin-icon>
  </vaadin-horizontal-layout>
`;

Column Visibility

Columns and column groups can be hidden. You can provide the user with a menu for toggling column visibilities, for example using Menu Bar.

Allowing the user to hide columns is useful when only a subset of the columns is relevant to their task, and if there are plenty of columns.

Open in a
new tab
@state()
private items: Person[] = [];

@state()
private contextMenuItems: Array<ContextMenuItem & { key: string }> = [
  { text: 'First name', checked: true, key: 'firstName' },
  { text: 'Last name', checked: true, key: 'lastName' },
  { text: 'Email', checked: true, key: 'email' },
  { text: 'Phone', checked: true, key: 'phone' },
  { text: 'Profession', checked: true, key: 'profession' },
];

protected override async firstUpdated() {
  const { people } = await getPeople();
  this.items = people;
}

protected override render() {
  return html`
    <vaadin-horizontal-layout style="align-items: baseline">
      <strong style="flex: 1;">Employees</strong>
      <vaadin-context-menu
        open-on="click"
        .items="${this.contextMenuItems}"
        @item-selected="${(e: ContextMenuItemSelectedEvent) => {
          const value = e.detail.value as ContextMenuItem & { key: string };
          this.contextMenuItems = this.contextMenuItems.map((item) =>
            item.key === value.key ? { ...item, checked: !value.checked } : item
          );
        }}"
      >
        <vaadin-button theme="tertiary">Show/Hide Columns</vaadin-button>
      </vaadin-context-menu>
    </vaadin-horizontal-layout>

    <vaadin-grid .items="${this.items}">
      <vaadin-grid-column
        path="firstName"
        .hidden="${!this.contextMenuItems[0].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="lastName"
        .hidden="${!this.contextMenuItems[1].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="email"
        .hidden="${!this.contextMenuItems[2].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="address.phone"
        .hidden="${!this.contextMenuItems[3].checked}"
      ></vaadin-grid-column>
      <vaadin-grid-column
        path="profession"
        .hidden="${!this.contextMenuItems[4