Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: explain how to implement Grid range selection #4026

Merged
merged 25 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions articles/components/grid/selection.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,40 @@ include::{root}/frontend/demo/component/grid/react/grid-multi-select-mode.tsx[re
endif::[]
--

In addition to selecting rows individually, a range of rows can be selected by dragging from one selection checkbox to another, if enabled:
=== Range Selection

In addition to selecting rows individually, you may want to allow users to select or deselect a range of rows using kbd:[Shift] + Click. Since there are a variety ways for range selection, Grid provides the necessary event, `item-toggle` to create your own implementation. This event provides information about the toggled row, its selection state, and whether the user was holding kbd:[Shift] during the toggle.

The example below demonstrates a possible implementation of range selection using `item-toggle`. In this implementation, the first clicked row is stored as an anchor point. When the user holds kbd:[Shift] and clicks another row, the selection state of all rows between the anchor and the newly clicked row is updated to match the clicked row's state. The second clicked row then becomes the new anchor point for future selections.

[.example]
--
ifdef::lit[]
[source,typescript]
----
include::{root}/frontend/demo/component/grid/grid-range-selection.ts[render,tags=snippet,indent=0,group=Lit]
----
endif::[]

ifdef::flow[]
[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/component/grid/GridRangeSelection.java[render,tags=snippet,indent=0,group=Flow]
----
endif::[]

ifdef::react[]
[source,tsx]
----
include::{root}/frontend/demo/component/grid/react/grid-range-selection.tsx[render,tags=snippet,indent=0,group=React]
----
endif::[]
--

A range of rows can also be selected by dragging from one selection checkbox to another, if that's enabled:

[.example]
--
[source,typescript]
----
<source-info group="Lit"></source-info>
Expand All @@ -97,9 +126,9 @@ selectionModel.setDragSelect(true);
...
</Grid>
----

--


== Selection Modes in Flow

Each selection mode is represented by a [classname]`GridSelectionModel`, accessible through the [methodname]`getSelectionModel()` method, which can be cast that to the specific selection model type, [classname]`SingleSelectionModel` or [classname]`MultiSelectionModel`. These interfaces provide selection mode specific APIs for configuration and selection events.
Expand Down
80 changes: 80 additions & 0 deletions frontend/demo/component/grid/grid-range-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'Frontend/demo/init'; // hidden-source-line
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-selection-column.js';
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type { GridItemToggleEvent } from '@vaadin/grid';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';

// tag::snippet[]
@customElement('grid-range-selection')
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[] = [];

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

private rangeStartItem?: Person;

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

handleItemToggle(event: GridItemToggleEvent<Person>) {
const { item, selected, shiftKey } = event.detail;

// If the anchor point isn't set, set it to the current item
this.rangeStartItem ??= item;

if (shiftKey) {
// Calculcate the range of items between the anchor point and
// the current item
const [rangeStart, rangeEnd] = [this.rangeStartItem, item]
.map((i) => this.items.indexOf(i))
.sort((a, b) => a - b);
const rangeItems = this.items.slice(rangeStart, rangeEnd + 1);

// Update the selection state of items within the range
// based on the state of the current item
const newSelectedItems = new Set(this.selectedItems);
rangeItems.forEach((rangeItem) => {
if (selected) {
newSelectedItems.add(rangeItem);
} else {
newSelectedItems.delete(rangeItem);
}
});
this.selectedItems = [...newSelectedItems];
}

// Update the anchor point to the current item
this.rangeStartItem = item;
}

protected override render() {
return html`
<vaadin-grid
.items="${this.items}"
.selectedItems="${this.selectedItems}"
@item-toggle="${this.handleItemToggle}"
>
<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>
`;
}
}
// end::snippet[]
66 changes: 66 additions & 0 deletions frontend/demo/component/grid/react/grid-range-selection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line
import React, { useCallback, useEffect, useRef } from 'react';
import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line
import { useSignal } from '@vaadin/hilla-react-signals';
import { Grid, GridItemToggleEvent } from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { GridSelectionColumn } from '@vaadin/react-components/GridSelectionColumn.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

function Example() {
useSignals(); // hidden-source-line
const items = useSignal<Person[]>([]);
const selectedItems = useSignal<Person[]>([]);
const rangeStartItem = useRef<Person>();

useEffect(() => {
getPeople().then(({ people }) => {
items.value = people;
});
}, []);

const handleItemToggle = (event: GridItemToggleEvent<Person>) => {
const { item, selected, shiftKey } = event.detail;

// If the anchor point isn't set, set it to the current item
rangeStartItem.current ??= item;

if (shiftKey) {
// Calculcate the range of items between the anchor point and
// the current item
const [rangeStart, rangeEnd] = [rangeStartItem.current, item]
.map((i) => items.value.indexOf(i))
.sort((a, b) => a - b);
const rangeItems = items.value.slice(rangeStart, rangeEnd + 1);

// Update the selection state of items within the range
// based on the state of the current item
const newSelectedItems = new Set(selectedItems.value);
rangeItems.forEach((rangeItem) => {
if (selected) {
newSelectedItems.add(rangeItem);
} else {
newSelectedItems.delete(rangeItem);
}
});
selectedItems.value = [...newSelectedItems];
}

// Update the anchor point to the current item
rangeStartItem.current = item;
};

return (
// tag::snippet[]
<Grid items={items.value} selectedItems={selectedItems.value} onItemToggle={handleItemToggle}>
<GridSelectionColumn />
<GridColumn path="firstName" />
<GridColumn path="lastName" />
<GridColumn path="email" />
</Grid>
// end::snippet[]
);
}

export default reactExample(Example); // hidden-source-line
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.vaadin.demo.component.grid;

import java.util.List;

import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridMultiSelectionModel;
import com.vaadin.flow.component.grid.dataview.GridListDataView;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.router.Route;
import com.vaadin.demo.DemoExporter; // hidden-source-line
import com.vaadin.demo.domain.DataService;

@Route("grid-range-selection")
public class GridRangeSelection extends Div {

private Person rangeStartItem;

public GridRangeSelection() {
// tag::snippet[]
Grid<Person> grid = new Grid<>(Person.class, false);
grid.addColumn(Person::getFirstName).setHeader("First name");
grid.addColumn(Person::getLastName).setHeader("Last name");
grid.addColumn(Person::getEmail).setHeader("Email");

List<Person> people = DataService.getPeople();
grid.setItems(people);

GridMultiSelectionModel<Person> selectionModel = (GridMultiSelectionModel<Person>) grid
.setSelectionMode(Grid.SelectionMode.MULTI);

selectionModel.addClientItemToggleListener(event -> {
Person item = event.getItem();

// If the anchor point isn't set, set it to the current item
if (rangeStartItem == null) {
rangeStartItem = item;
}

if (event.isShiftKey()) {
// Calculcate the range of items between the anchor
// point and the current item
GridListDataView<Person> dataView = grid.getListDataView();
int rangeStart = dataView.getItemIndex(rangeStartItem).get();
int rangeEnd = dataView.getItemIndex(item).get();
Person[] rangeItems = dataView.getItems()
.skip(Math.min(rangeStart, rangeEnd))
.limit(Math.abs(rangeStart - rangeEnd) + 1)
.toArray(Person[]::new);

// Update the selection state of items within the range
// based on the state of the current item
if (event.isSelected()) {
selectionModel.selectItems(rangeItems);
} else {
selectionModel.deselectItems(rangeItems);
}
}

// Update the anchor point to the current item
rangeStartItem = item;
});
// end::snippet[]

add(grid);
}

public static class Exporter // hidden-source-line
extends DemoExporter<GridRangeSelection> { // hidden-source-line
} // hidden-source-line
}
Loading