Kendo-angular: Grid Selection

Created on 9 Jun 2017  路  32Comments  路  Source: telerik/kendo-angular

This is a summary of the enhancements we're working on to support selection in the Grid.

Work is already underway, but the API and specification are not set in stone.

Basics

  • Provide a directive that implements persistent selection in memory. By default, the selected state will be stored by an unique key on the data item.

    1. Basic usage

      <kendo-grid kendoGridSelectBy="id"></kendo-grid>
      
    2. Modification through the backing store

      <kendo-grid kendoGridSelectBy="id" [(selectedKeys)]="selection"></kendo-grid>
      
      export class MyGrid {
        public selection: number[];
      
        public deselectAll(): void {
          this.selection = [];
        }
      }
      

    Note that the backing store can be used to obtain the currently selected items and to modify the selection.

    1. Usage with item key function

      <kendo-grid [kendoGridSelectBy]="itemKey"></kendo-grid>
      

      ts export class MyGrid { public itemKey = (item) => item.id; // or // public itemKey = (item, index) => index; }

  • Have a built-in checkbox column for selecting rows

  • Fire a selectionChange event when the selection is modified. Event arguments will look like:

    export interface SelectionChangeEvent {
      // Items that should be selected as a result of the current action
      selectedItems: any[];
      selectedIndexes: number[];
    
      // Items that should be deselected as a result of the current action
      deselectedItems: any[];
      deselectedIndexes: number[];
    }
    

    Note that the event is a low-level interface. The arguments contain information only about the selected items in the current view.

  • Accept a function that returns a boolean value indicating if the row should be selected:

    <kendo-grid [rowSelected]="isRowSelected"></kendo-grid>
    

    ts export class MyGrid { public isRowSelected({sender, rowIndex, dataItem}): void { return dataItem.checked; } }

    Note that this function is a low-level interface intended for more advanced scenarios.

Shortcuts

  • _Click_ (or _Space_) should toggle the selected state of the row and unselect all other rows
  • _Ctrl+Click_ (or _Ctrl+Space_) should toggle the selected state of the current row
  • _Shift+Click_ (or _Shift+Up/Down_) should extend the selected range from the currently selected item and unselect all other rows
  • _Ctrl + Up/Down/PgUp/PgDown_ should navigate between rows without altering the selection

Docs

The documentation should cover the following scenarios:

  • Basic functionality with the built-in directive
  • Advanced usage of the API that stores selected state in a data item field

Will do later

  • Drag to select

Won't do

  • Cell selection
  • Column selection
Enhancement Epic grid

Most helpful comment

Hi @tsvetomir,
Just played with @dev version and the multiselect feature.
I've tried to do a simple task - grab selected rows on button click. I cannot get current selection from SelectionChange event because it provides only newly selectedRows so I've tried to use Selection Directive and selectedKeys option but I need the whole row data but not only selected row keys. Should I get all grid data and then filter by selected keys in order to grab all selected rows data?

Do you consider adding current selection in SelectionChange Event?

{
  selectedRows: ...,
  deselectedRows: ...,
  currentRows: [{selectedRowData0}, {selectedRowData1}, ...],  // Something like this
  ctrlKey: ...,
  selected: ...
  index: ...
}

All 32 comments

In "", is selectedKeys supposed to be 2-way binding?

If the user makes a selection in the grid, is selection going to be reflected in selectedKeys?

Won't do

Cell selection

Please reconsider again. In jQuery version I have a bunch of custom shortcut keys on selected cell. I even filter as you type and for that I need to have cell selection.

is selectedKeys supposed to be 2-way binding?

Indeed, fixed the snippet.

Cell selection

We'll consider this scenario. The keyboard navigation feature might be able to cover it without implementing full-blown cell selection.

@tsvetomir

Can we confirm how selection will be impacted by data functions like grid sorting? Will selection be maintained across sorts and filters?

In today's grid, sorting seems to "silently" change the selected row (keeping the same index).

The selection will be persisted based on the unique key supplied to _kendoGridSelectBy_. You can also implement custom persistence by handling the _selectionChange_ and supplying _isRowSelected_ callback.

@tsvetomir
Just wanted to show how simple filtering can be, but I need to know which cell is selected.

2017-06-27_14-02-07

@MaklaCof would a click event for the cell suffice for this scenario?

Hey guys, do you have any update for us, maybe we can test something @dev ?

@moxival soon! We'll have something to show in a week or so.

@tsvetomir
Much better then nothing would be, but I think you can see what can be achieved with "real" cell selection.

Hey guys, i do feel like the annoying kid in the backseat but it is almost the end of the month and still no multiple selection. Any news?

@moxival it's not you, it's us. We had to do quite a bit of refactoring to get things in order.
We'll post updates and, fingers crossed, a working release by Monday.

Does "Drag to Select" mean the ability to re-order rows by drag and drop? If not, is it on the road map? I was using Dragula with just a ngFor prior to swapping in the Kendo Grid; now the entire contents of the grid "drags".

The "dev" channel of the Grid has been updated to include a preview of the selection functionality. Version number is 1.3.0-dev.*

Switch to this channel by running:

npm install --save @progress/kendo-angular-grid@dev

Optionally, update your package.json to always get the "dev" version:

"dependencies": {
  "@progress/kendo-angular-grid": "dev"
  ...
}

NOTE: The "dev" channel is suitable only for development. Do not use it for production deployments.

Adding a preview of the feature documentation below:

Selection Basics

The Grid allows the user to select single or multiple rows.

The selection is enabled through the selectable option, which accepts a boolean or SelectableSettings parameter. Using the later allows you to specify the following options:

  • enabled - determines if the selection is enabled initially. The default value is true.
  • checkboxOnly - determines if the selection is only possible through clicking a checkbox with the SelectionCheckboxDirective applied. The default value is false.
  • mode - sets the SelectableMode of the selection. The default value is multiple.
import { Component } from '@angular/core';
import { products } from './products';
import { SelectableSettings } from '@progress/kendo-angular-grid';

@Component({
    selector: 'my-app',
    template: `
        <div style="margin: 10px">
            <div class="card">
                <div class="card-block k-form">
                    <fieldset>
                    <legend>Selection Settings</legend>
                    <div class="k-form-field">
                        <span>Mode</span>
                        <input type="radio" name="mode" id="single" value="single" class="k-radio"
                            [(ngModel)]="mode" (change)="setSelectableSettings()">
                        <label class="k-radio-label" for="single">Single</label>
                        <input type="radio" name="mode" id="multiple" value="multiple" class="k-radio"
                            [(ngModel)]="mode" (change)="setSelectableSettings()">
                        <label class="k-radio-label" for="multiple">Multiple</label>
                    </div>
                    <label class="k-form-field">
                        <input type="checkbox" id="chkboxonly" class="k-checkbox" [(ngModel)]="checkboxOnly" (change)="setSelectableSettings()">
                        <label class="k-checkbox-label" for="chkboxonly">Use checkbox only selection</label>
                    </label>
                    </fieldset>
                </div>
            </div>
        </div>
        <kendo-grid [data]="gridData" [selectable]="selectableSettings" [height]="500">
            <kendo-grid-checkbox-column></kendo-grid-checkbox-column>
            <kendo-grid-column field="ProductName" title="Product Name" [width]="300"></kendo-grid-column>
            <kendo-grid-column field="UnitsInStock" title="Units In Stock"></kendo-grid-column>
            <kendo-grid-column field="UnitsOnOrder" title="Units On Order"></kendo-grid-column>
            <kendo-grid-column field="ReorderLevel" title="Reorder Level"></kendo-grid-column>
        </kendo-grid>
    `
})
export class AppComponent {
    private gridData: any[] = products;

    private setSelectableSettings() {
        this.selectableSettings = {
            checkboxOnly: this.checkboxOnly,
            mode: this.mode
        }
    }

    private checkboxOnly: boolean = false
    private mode: string = "multiple";
    private selectableSettings: SelectableSettings;

    constructor() {
        this.setSelectableSettings();
    }
}

Selection Directive

By default the selection is not persisted during dataStateChange operations (paging, sorting etc.). This behavior can be modified by applying the SelectionDirective to the Grid. Out of the box, the directive stores the items by an absolute rowIndex, but you could override this by specifying the key that will be stored instead. It could be a dataItem field set as string or a function that will accept a RowArgs parameter and return the key as a result.
Additionally specifying the selectedKeys option in a combination with the SelectionDirective will allow you define the collection that will store the selected items. This will allow full control e.g. specifying initially selected items, updating the collection manually etc.

The following example demonstrates storing the selected items by the ProductID field.

import { Component } from '@angular/core';
import { products } from './products';

@Component({
    selector: 'my-app',
    template: `
        <kendo-grid
            [data]="gridData"
            [height]="500"
            [kendoGridSelectBy]="ProductID"
            [selectedKeys]="mySelection"
            >
            <kendo-grid-column field="ProductName" title="Product Name" [width]="300"></kendo-grid-column>
            <kendo-grid-column field="UnitsInStock" title="Units In Stock"></kendo-grid-column>
            <kendo-grid-column field="UnitsOnOrder" title="Units On Order"></kendo-grid-column>
            <kendo-grid-column field="ReorderLevel" title="Reorder Level"></kendo-grid-column>
        </kendo-grid>
    `
})
export class AppComponent {
    private gridData: any[] = products;
    private mySelection: number[] = [2, 4];
}

The following example demonstrates storing the selected items by a custom key.

import { Component } from '@angular/core';
import { products } from './products';
import { RowArgs } from '@progress/kendo-angular-grid';

@Component({
    selector: 'my-app',
    template: `
        <kendo-grid
            [data]="gridData"
            [height]="500"
            [kendoGridSelectBy]="mySelectionKey"
            [selectedKeys]="mySelection"
            >
            <kendo-grid-column field="ProductName" title="Product Name" [width]="300"></kendo-grid-column>
            <kendo-grid-column field="UnitsInStock" title="Units In Stock"></kendo-grid-column>
            <kendo-grid-column field="UnitsOnOrder" title="Units On Order"></kendo-grid-column>
            <kendo-grid-column field="ReorderLevel" title="Reorder Level"></kendo-grid-column>
        </kendo-grid>
    `
})
export class AppComponent {
    private gridData: any[] = products;
    private mySelection: string[] = [];
    private mySelectionKey(context: RowArgs): string {
        return context.dataItem.ProductName + " " + context.index;
    }
}

RowSelected Function

Specifying a rowSelected function is an alternative to set the selected items. It represents a function that is executed for every data row in the Grid and determines if the row should be selected. Please note that this function will take precedence over the default selection behavior and any other selection related option set.

import { Component } from '@angular/core';
import { products } from './products';
import { RowArgs } from '@progress/kendo-angular-grid';

@Component({
    selector: 'my-app',
    template: `
        <kendo-grid
            [data]="gridData"
            [height]="500"
            [rowSelected]="isRowSelected"
            >
            <kendo-grid-column field="ProductName" title="Product Name" [width]="300"></kendo-grid-column>
            <kendo-grid-column field="UnitsInStock" title="Units In Stock"></kendo-grid-column>
            <kendo-grid-column field="UnitsOnOrder" title="Units On Order"></kendo-grid-column>
            <kendo-grid-column field="ReorderLevel" title="Reorder Level"></kendo-grid-column>
        </kendo-grid>
    `
})
export class AppComponent {
    private gridData: any[] = products;

    private isRowSelected(row: RowArgs) {
        return row.index % 2 === 0;
    }
}

SelectionChange Event

When the selection is modified the Grid will emit the selectionChange event, which will contain the following information:

  • selectedRows - Collection of RowArgs elements, representing the items that were added to the selection.
  • deselectedRows - Collection of RowArgs elements, representing the items that were removed from the selection.
  • ctrlKey - A boolean value representing the state of the ctrlKey during the selection.

  • selected - Deprecated property, representing the selected state of the affected row.

  • index - Deprecated property, representing the index of the affected row.

@cp79shark no, it's more like rectangular selection tool. Haven't tried Dragula, but will give it a shot to see if there's something we can do to play nice with it.

Now, that row selection is complete, we can talk about row->cell selection. So whey you click cell, entire row is selected, but clicked cell is selected on top of row selection.
At this point I would like to quote you, when you said that the kendoAngular grid would eventually super-pass jQuery grid.

@tsvetomir there is an angular2/4 wrapper by the people out of Valor Software for Dragula (JS only lib). ng2-dragula

I just started using the Kendo Angular Grid this evening and now I've gotten Dragula to work with the Grid, but I had to make a change to the Dragula source code. I'm not sure where the issue should go, probably Dragula for discussion.

If anyone is interested, here is how I got it to "work" (roughly, there is an issue with the column headers I'm still working on).

In your Component.ts, define some dragOptions. The moves member is optional, I use a drag handle in my code. Also use a GridDataResult, don't bind your list of data directly to the data attribute in the kendo-grid.

    private gridView: GridDataResult;

    dragOptions: any = {
        isContainer: function (el: any) {
            return el.tagName === "TBODY"; // only our kendo grid tbody should be the container
        },
        invalid: function (el: any, handle: any) {
            return el.tagName === "TR" && el.parentElement.tagName !== "TBODY"; // if it's not a tr from our tbody then it's invalid
        },
        moves: function (el: any, src: any, handle: any, sibling: any) {
            // chrome doesn't work if you click on the I element, which rightly doesn't have the dragHandle class

            return handle.className.indexOf('dragHandle') !== -1 ||
                handle.parentElement.className.indexOf('dragHandle') !== -1;
        },
        revertOnSpill: true,
    };

Assign the dragOptions to your kendo-grid, ensure your dragulaModel is the actual collection that
gridView is built from.

    <kendo-grid [data]="gridView" [dragula]="'dropzone'" [dragulaModel]="classifications" [dragulaOptions]="dragOptions">
    // ... omitted
    </kendo-grid>

The change required in Dragula is in the dragula.provider.js file. line 87 (this in the components folder of the package ng2-dragula)

sourceModel = drake.models[drake.containers.indexOf(source)];

becomes

sourceModel = drake.models[drake.containers.indexOf(source)] || drake.models[0];

The reason why I had to make this change is because the dragulaModel directive registers the "source" element as kendo-grid. I have to restrict my container to tbody to get individual row drag/drop. The check in the dragula provider code looks in its model tables and since the source is tbody and not kendo-grid it fails to find the backing model. So, for now, my very ugly patch is to just revert to the first backing model stored in case of a miss.

Hi @tsvetomir,
Just played with @dev version and the multiselect feature.
I've tried to do a simple task - grab selected rows on button click. I cannot get current selection from SelectionChange event because it provides only newly selectedRows so I've tried to use Selection Directive and selectedKeys option but I need the whole row data but not only selected row keys. Should I get all grid data and then filter by selected keys in order to grab all selected rows data?

Do you consider adding current selection in SelectionChange Event?

{
  selectedRows: ...,
  deselectedRows: ...,
  currentRows: [{selectedRowData0}, {selectedRowData1}, ...],  // Something like this
  ctrlKey: ...,
  selected: ...
  index: ...
}

Hey guys,
I am just testing the new and awesome selection :) but I have found that when the selectionChange event fires the selectedKeys is not yet updated with the new selected keys. Should we use selectedKeysChange in order to be sure that we have up-to-date data or is this something that you will fix?

Also i am interested in what @ilianiv wrote earlier, a currentSelection property will be really convenient and easy to work with.

There is a bug when selecting new row and that leads to the deselection of the previously selected row. the deselectedRows property is an array of the row data and not an array of {dataItem: any, index: number} which leads to exception in Selection.getItemKey() because of return row.dataItem[this.selectionKey];:

image

image

core.es5.js:1020 ERROR TypeError: Cannot read property 'id' of undefined
    at SelectionDirective.webpackJsonp.../../../../@progress/kendo-angular-grid/dist/es/selection/selection-default.js.Selection.getItemKey (selection-default.js:42)
    at selection-default.js:53
    at Array.forEach (<anonymous>)
    at SelectionDirective.webpackJsonp.../../../../@progress/kendo-angular-grid/dist/es/selection/selection-default.js.Selection.onSelectionChange (selection-default.js:52)
    at SafeSubscriber.schedulerFn [as _next] (core.es5.js:3647)
    at SafeSubscriber.webpackJsonp.../../../../rxjs/Subscriber.js.SafeSubscriber.__tryOrUnsub (Subscriber.js:238)
    at SafeSubscriber.webpackJsonp.../../../../rxjs/Subscriber.js.SafeSubscriber.next (Subscriber.js:185)
    at Subscriber.webpackJsonp.../../../../rxjs/Subscriber.js.Subscriber._next (Subscriber.js:125)
    at Subscriber.webpackJsonp.../../../../rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
    at EventEmitter.webpackJsonp.../../../../rxjs/Subject.js.Subject.next (Subject.js:55)

If you give us template for a plunker maybe we can report bugs and demonstrate behaviour a bit easier :)

Sorry for the delay, everyone.

@MaklaCof what you describe sounds more like an "active cell". We'll revisit this concept when working on the keyboard navigation.

@cp79shark thanks for sharing your experience. We'll consider built-in drag and drop support in the future.

@ilianiv I see what you mean. The selection, from the point of view of the Grid, is stateless. Persistence is implemented in the [kendoGridSelectBy] directive or user code. For example, you can use the SelectionChange event to update a list of row references.

// Compares by index, but can be something else
const containsRow = (all, row) =>
  !!all.find(other => other.index === row.index);

...

selectionChange(e: any): void {
  this.selectedRows = this.selectedRows.filter(row =>
    !containsRow(e.deselectedRows, row)
  );

  this.selectedRows = this.selectedRows.concat(e.selectedRows);
}

This can be encapsulated in a directive and used like like [selectedRows]="selectedRows". The built-in kendoGridSelectBy directive does pretty much the same, but with a simple key.

@moxival we'll introduce a the selectedKeysChanged event, effectively making selectedKeys a two-way binding. /cc @Raisolution

Thanks for the bug report, we'll investigate. Here's a plunkr you can use as a template for the bug report. Because you asked :)

@tsvetomir Exactly that.

Since you mention keyboard navigation please think about implementing auto confirm boolean property.
In jQuery version when you navigate with keyboard, you had to confirm selection by pressing space. I am sure many would like to see that happening automatically.

@tsvetomir can you shed some light on what was updated with version 1.3.0-dev.201708071516?

I cannot reproduce the bug in plunker that i mentioned earlier that's why there is no bug report :(

Hi guys,

Sorry for the late input. If you would like to persist the selected dataItems directly, another option would be specify the key to be stored as a function, which returns the dataItem itself.

E.g.

    <kendo-grid [kendoGridSelectBy]="mySelectionKey">
    private mySelectionKey(context: RowArgs): Object {
        return context.dataItem;
    }

This way the selectedKeys collection will contain the selected dataItems directly. @ilianiv @moxival, let me know if this suits your needs.

@moxival, I was unable to reproduce this on our end too. Keep us updated if you notice the incorrect behavior again.

@moxival nothing changed in regards to the selection... at least I can't see anything in the commit log. You can clone the full repo along with tags if you really need to pinpoint a specific change. See Obtaining Source Code.

I'm happy to let you know that Grid Selection is shipped in v1.3.1 of the Grid.

Hi @tsvetomir
I am facing a problem with the Grid selection. The property that maps to selectedKeys is, in fact, storing all the rows selected, however, when moving from one page to another (remote paging), the Grid is not selecting any row that corresponds to those values stored in the selectedKeys property.

I can tell it is more of a refresh issue. Is there anything I could do to trace this issue?

Thanks
Bilal H.

Hi Bilal,

The problem is not reproducible in the following example. Please use our support channels for issues, which are not bug reports. Thanks in advance.

I cannot get the [selectedKeys] or the [kendoGridSelectedBy] working at all. Error is can't bind to 'selectedKeys' since it isn't a known property of 'kendo-grid'

There's are no known issues with the selection feature. Can it be that you're using an older version from the retired Progress NPM registry? See #712

If that's not the case I'd like to ask you to submit a support ticket with more details about your scenario.

Hi the team,
Great work for this feature, we're planning now to migrate our custom solution for multiple selection to this mecanism.
I have one question about that :

  • Is it possible to create our own CustomSelectionDirective, that inherit from your SelectionDirective to manage the selection logic for the grid ?
  • Is it possible to change de width / height of the checkboxes without tricky CSS ?

Hi @ChrisProlls,

Yes, you could achieve both out of the box.

  • It is possible to extend the SelectionDirective and manually handle the selection. At the moment we don't have specific example in the documentation, because we haven't met a scenario that requires it. Most custom selection scenarios could be implemented by setting the rowSelected function of the Grid and/or subscribe to the selection related events. You could open a support ticket and describe the specific business requirements and we will suggest the most suitable approach for implementation.
  • The SelectionCheckboxDirective (which is used internally in the CheckboxColumn) could be applied to any input element, turning it to a selection checkbox. This way, the styling is up to you and your specific requirements, while the checkbox will still handle the selection.

Hi !

  • For the moment I use a dedicated injectable service for handling all selection-things that my grid needs : it worked without any problem ;
  • I've found this solution into the documentation and I feel this is a little heavy for my need : I resolved my problem with a specific CSS rule, and that worked. I will keep this solution.

Thanks for your prompt response !

Was this page helpful?
0 / 5 - 0 ratings