[ADF-1115] selection management for DT/DL components (#2100)

* row select/unselect dom events for DT

- new events for datatable
- improved unit tests for empty content placeholders

* improved selection management for DT

* selection management for document list

* fix tests
This commit is contained in:
Denys Vuika
2017-07-19 12:00:03 +01:00
committed by Eugenio Romano
parent 6bde12f770
commit 24bd860d38
13 changed files with 205 additions and 77 deletions

View File

@@ -62,6 +62,8 @@
[contextMenuActions]="true"
[contentActions]="true"
[allowDropFiles]="true"
[selectionMode]="selectionMode"
[multiselect]="multiselect"
(error)="onNavigationError($event)"
(success)="resetError()"
(preview)="showFile($event)"
@@ -137,7 +139,20 @@
<context-menu-holder></context-menu-holder>
<div class="p-10">
Selected Nodes:
<ul>
<li *ngFor="let node of documentList.selection">
{{ node.entry.name }}
</li>
</ul>
</div>
<div class="container">
<section>
<md-slide-toggle [(ngModel)]="multiselect">Multiselect (with checkboxes)</md-slide-toggle>
</section>
<section>
<md-slide-toggle [(ngModel)]="useDropdownBreadcrumb">Dropdown breadcrumb</md-slide-toggle>
</section>
@@ -204,6 +219,15 @@
</section>
</div>
<div class="p-10">
<p>For 'Multiple' selection mode use Cmd (macOS) or Ctrl (Win) to toggle selection of multiple items.</p>
<md-select placeholder="Selection Mode" [(ngModel)]="selectionMode" name="food">
<md-option *ngFor="let mode of selectionModes" [value]="mode.value">
{{mode.viewValue}}
</md-option>
</md-select>
</div>
<file-uploading-dialog #fileDialog></file-uploading-dialog>
<div *ngIf="fileShowed">

View File

@@ -41,6 +41,18 @@ export class FilesComponent implements OnInit {
useCustomToolbar = true;
useDropdownBreadcrumb = true;
selectionModes = [
{ value: 'none', viewValue: 'None' },
{ value: 'single', viewValue: 'Single' },
{ value: 'multiple', viewValue: 'Multiple' }
];
@Input()
selectionMode = 'multiple';
@Input()
multiselect = false;
@Input()
multipleFileUpload: boolean = false;

View File

@@ -161,6 +161,7 @@ export class DataTableDemo {
| allowDropFiles | boolean | false | Toggle file drop support for rows (see **ng2-alfresco-core/UploadDirective** for more details) |
| loading | boolean | false | Flag that indicate if the datable is in loading state and need to show the loading template. Read the documentation above to know how to configure a loading template |
| showHeader | boolean | true | Toggles header visibility |
| selection | DataRow[] | [] | Contains selected rows |
### DataColumn Properties
@@ -189,14 +190,17 @@ Here's the list of available properties you can define for a Data Column definit
### DataTable DOM Events
Below are the DOM events raised by DataTable component.
Below are the DOM events raised by DataTable component.
These events bubble up the component tree and can be handled by any parent component.
| Name | Description |
| --- | --- |
| row-click | Emitted when user clicks the row |
| row-dblclick | Emitted when user double-clicks the row |
| row-click | Raised when user clicks a row |
| row-dblclick | Raised when user double-clicks a row |
| row-select | Raised after user selects a row |
| row-unselect | Raised after user unselects a row |
These events are bubbled up the element tree and can be subscribed to from within parent components.
For example:
```html
<root-component (row-click)="onRowClick($event)">

View File

@@ -55,7 +55,10 @@
</td>
<td *ngIf="multiselect">
<md-checkbox [(ngModel)]="row.isSelected"></md-checkbox>
<md-checkbox
[checked]="row.isSelected"
(change)="onCheckboxChange(row, $event)">
</md-checkbox>
</td>
<td *ngFor="let col of data.getColumns()"
class="adf-data-table-cell adf-data-table-cell--{{col.type || 'text'}} {{col.cssClass}}"

View File

@@ -17,7 +17,7 @@
import {
AfterContentInit, Component, ContentChild, DoCheck, ElementRef, EventEmitter, Input,
IterableDiffers, OnChanges, Optional, Output, SimpleChange, SimpleChanges, TemplateRef
IterableDiffers, OnChanges, Output, SimpleChange, SimpleChanges, TemplateRef
} from '@angular/core';
import { MdCheckboxChange } from '@angular/material';
import { AlfrescoTranslationService, DataColumnListComponent } from 'ng2-alfresco-core';
@@ -92,10 +92,11 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
@Input()
loading: boolean = false;
public noContentTemplate: TemplateRef<any>;
public loadingTemplate: TemplateRef<any>;
noContentTemplate: TemplateRef<any>;
loadingTemplate: TemplateRef<any>;
isSelectAllChecked: boolean = false;
selection = new Array<DataRow>();
private clickObserver: Observer<DataRowEvent>;
private click$: Observable<DataRowEvent>;
@@ -108,7 +109,7 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
private multiClickStreamSub: Subscription;
constructor(translateService: AlfrescoTranslationService,
@Optional() private el: ElementRef,
private elementRef: ElementRef,
private differs: IterableDiffers) {
if (differs) {
this.differ = differs.find([]).create(null);
@@ -173,10 +174,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
this.singleClickStreamSub = singleClickStream.subscribe((obj: DataRowEvent[]) => {
let event: DataRowEvent = obj[0];
let el = obj[0].sender.el;
this.rowClick.emit(event);
if (!event.defaultPrevented && el.nativeElement) {
el.nativeElement.dispatchEvent(
if (!event.defaultPrevented) {
this.elementRef.nativeElement.dispatchEvent(
new CustomEvent('row-click', {
detail: event,
bubbles: true
@@ -192,10 +192,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
this.multiClickStreamSub = multiClickStream.subscribe((obj: DataRowEvent[]) => {
let event: DataRowEvent = obj[0];
let el = obj[0].sender.el;
this.rowDblClick.emit(event);
if (!event.defaultPrevented && el.nativeElement) {
el.nativeElement.dispatchEvent(
if (!event.defaultPrevented) {
this.elementRef.nativeElement.dispatchEvent(
new CustomEvent('row-dblclick', {
detail: event,
bubbles: true
@@ -247,21 +246,32 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
const newValue = !row.isSelected;
const rows = this.data.getRows();
const domEventName = newValue ? 'row-select' : 'row-unselect';
const domEvent = new CustomEvent(domEventName, {
detail: {
row: row,
selection: this.selection
},
bubbles: true
});
if (this.isSingleSelectionMode()) {
rows.forEach(r => r.isSelected = false);
row.isSelected = newValue;
this.resetSelection();
this.selectRow(row, newValue);
this.elementRef.nativeElement.dispatchEvent(domEvent);
}
if (this.isMultiSelectionMode()) {
const modifier = e.metaKey || e.ctrlKey;
if (!modifier) {
rows.forEach(r => r.isSelected = false);
this.resetSelection();
}
row.isSelected = newValue;
this.selectRow(row, newValue);
this.elementRef.nativeElement.dispatchEvent(domEvent);
}
}
let dataRowEvent = new DataRowEvent(row, e, this);
const dataRowEvent = new DataRowEvent(row, e, this);
this.clickObserver.next(dataRowEvent);
}
}
@@ -272,7 +282,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
if (rows && rows.length > 0) {
rows.forEach(r => r.isSelected = false);
}
this.selection.splice(0);
}
this.isSelectAllChecked = false;
}
onRowDblClick(row: DataRow, e?: Event) {
@@ -301,12 +313,29 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
let rows = this.data.getRows();
if (rows && rows.length > 0) {
for (let i = 0; i < rows.length; i++) {
rows[i].isSelected = e.checked;
this.selectRow(rows[i], e.checked);
}
}
}
}
onCheckboxChange(row: DataRow, event: MdCheckboxChange) {
const newValue = event.checked;
this.selectRow(row, newValue);
const domEventName = newValue ? 'row-select' : 'row-unselect';
const domEvent = new CustomEvent(domEventName, {
detail: {
row: row,
selection: this.selection
},
bubbles: true
});
this.elementRef.nativeElement.dispatchEvent(domEvent);
}
onImageLoadingError(event: Event) {
if (event && this.fallbackThumbnail) {
let element = <any> event.target;
@@ -384,4 +413,20 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck
return `${row.cssClass} ${this.rowStyleClass}`;
}
private selectRow(row: DataRow, value: boolean) {
if (row) {
row.isSelected = value;
const idx = this.selection.indexOf(row);
if (value) {
if (idx < 0) {
this.selection.push(row);
}
} else {
if (idx > -1) {
this.selection.splice(idx, 1);
}
}
}
}
}

View File

@@ -15,31 +15,42 @@
* limitations under the License.
*/
import { Injector } from '@angular/core';
import { getTestBed, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { DataTableCellComponent } from '../components/datatable/datatable-cell.component';
import { DataTableComponent } from '../components/datatable/datatable.component';
import { MaterialModule } from '../material.module';
import { LoadingContentTemplateDirective } from './loading-template.directive';
describe('LoadingContentTemplateDirective', () => {
let injector: Injector;
let loadingContentTemplateDirective: LoadingContentTemplateDirective;
beforeEach(() => {
let dataTable: DataTableComponent;
let directive: LoadingContentTemplateDirective;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
CoreModule.forRoot()
],
providers: [
LoadingContentTemplateDirective,
DataTableComponent
declarations: [
DataTableComponent,
DataTableCellComponent,
LoadingContentTemplateDirective
]
});
injector = getTestBed();
loadingContentTemplateDirective = injector.get(LoadingContentTemplateDirective);
}).compileComponents();
}));
beforeEach(() => {
let fixture = TestBed.createComponent(DataTableComponent);
dataTable = fixture.componentInstance;
directive = new LoadingContentTemplateDirective(dataTable);
});
it('is defined', () => {
expect(loadingContentTemplateDirective).toBeDefined();
it('applies template to the datatable', () => {
const template = {};
directive.template = template;
directive.ngAfterContentInit();
expect(dataTable.loadingTemplate).toBe(template);
});
});

View File

@@ -30,7 +30,9 @@ export class LoadingContentTemplateDirective implements AfterContentInit {
}
ngAfterContentInit() {
this.dataTable.loadingTemplate = this.template;
if (this.dataTable) {
this.dataTable.loadingTemplate = this.template;
}
}
}

View File

@@ -15,31 +15,42 @@
* limitations under the License.
*/
import { Injector } from '@angular/core';
import { getTestBed, TestBed } from '@angular/core/testing';
import { async, getTestBed, TestBed } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { DataTableCellComponent } from '../components/datatable/datatable-cell.component';
import { DataTableComponent } from '../components/datatable/datatable.component';
import { MaterialModule } from '../material.module';
import { NoContentTemplateDirective } from './no-content-template.directive';
describe('NoContentTemplateDirective', () => {
let injector: Injector;
let noContentTemplateDirective: NoContentTemplateDirective;
beforeEach(() => {
let dataTable: DataTableComponent;
let directive: NoContentTemplateDirective;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
CoreModule.forRoot()
],
providers: [
NoContentTemplateDirective,
DataTableComponent
declarations: [
DataTableComponent,
DataTableCellComponent,
NoContentTemplateDirective
]
});
injector = getTestBed();
noContentTemplateDirective = injector.get(NoContentTemplateDirective);
}).compileComponents();
}));
beforeEach(() => {
let fixture = TestBed.createComponent(DataTableComponent);
dataTable = fixture.componentInstance;
directive = new NoContentTemplateDirective(dataTable);
});
it('is defined', () => {
expect(noContentTemplateDirective).toBeDefined();
it('applies template to the datatable', () => {
const template = {};
directive.template = template;
directive.ngAfterContentInit();
expect(dataTable.noContentTemplate).toBe(template);
});
});

View File

@@ -30,6 +30,8 @@ export class NoContentTemplateDirective implements AfterContentInit {
}
ngAfterContentInit() {
this.dataTable.noContentTemplate = this.template;
if (this.dataTable) {
this.dataTable.noContentTemplate = this.template;
}
}
}

View File

@@ -89,6 +89,7 @@ The properties currentFolderId, folderNode and node are the entry initialization
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| selectionMode | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. |
| selection | Array<MinimalNodeEntity> | [] | Contains selected nodes |
| rowStyle | string | | The inline style to apply to every row, see [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples |
| rowStyleClass | string | | The CSS class to apply to every row |
| currentFolderId | string | null | Initial node ID of displayed folder. Can be `-root-`, `-shared-`, `-my-`, or a fixed node ID |
@@ -132,6 +133,8 @@ All of them are `bubbling`, meaning you can handle them in any component up the
| --- | --- |
| node-click | Raised when user clicks the node |
| node-dblclick | Raised when user double-clicks the node |
| node-select | Raised when user selects a node |
| node-unselect | Raised when user unselects a node |
Every event is represented by a [CustomEvent](https://developer.mozilla.org/en/docs/Web/API/CustomEvent) instance, having at least the following properties as part of the `Event.detail` property value:
@@ -142,6 +145,8 @@ Every event is represented by a [CustomEvent](https://developer.mozilla.org/en/d
}
```
Please refer to the DataTable documentation to find details about additional DOM events the DocumentList component bubbles up from the DataTable.
### Handling DOM events
Here's a basic example on handling DOM events in the parent elements:

View File

@@ -20,8 +20,10 @@
(showRowContextMenu)="onShowRowContextMenu($event)"
(showRowActionsMenu)="onShowRowActionsMenu($event)"
(executeRowAction)="onExecuteRowAction($event)"
(rowClick)="onRowClick($event)"
(rowDblClick)="onRowDblClick($event)">
(rowClick)="onNodeClick($event.value?.node)"
(rowDblClick)="onNodeDblClick($event.value?.node)"
(row-select)="onNodeSelect($event.detail)"
(row-unselect)="onNodeUnselect($event.detail)">
<div *ngIf="!isEmptyTemplateDefined()">
<no-content-template>
<ng-template>

View File

@@ -748,47 +748,43 @@ describe('DocumentList', () => {
it('should emit [nodeClick] event on row click', () => {
let node = new NodeMinimalEntry();
let row = new ShareDataRow(node, null, null);
let event = new DataRowEvent(row, null);
spyOn(documentList, 'onNodeClick').and.callThrough();
documentList.onRowClick(event);
documentList.onNodeClick(node);
expect(documentList.onNodeClick).toHaveBeenCalledWith(node);
});
it('should emit node-click DOM event', (done) => {
let node = new NodeMinimalEntry();
let row = new ShareDataRow(node, null, null);
let event = new DataRowEvent(row, null);
const htmlElement = fixture.debugElement.nativeElement as HTMLElement;
htmlElement.addEventListener('node-click', (e: CustomEvent) => {
done();
});
documentList.onRowClick(event);
documentList.onNodeClick(node);
});
it('should emit [nodeDblClick] event on row double-click', () => {
let node = new NodeMinimalEntry();
let row = new ShareDataRow(node, null, null);
let event = new DataRowEvent(row, null);
spyOn(documentList, 'onNodeDblClick').and.callThrough();
documentList.onRowDblClick(event);
documentList.onNodeDblClick(node);
expect(documentList.onNodeDblClick).toHaveBeenCalledWith(node);
});
it('should emit node-dblclick DOM event', (done) => {
let node = new NodeMinimalEntry();
let row = new ShareDataRow(node, null, null);
let event = new DataRowEvent(row, null);
const htmlElement = fixture.debugElement.nativeElement as HTMLElement;
htmlElement.addEventListener('node-dblclick', (e: CustomEvent) => {
done();
});
documentList.onRowDblClick(event);
documentList.onNodeDblClick(node);
});
it('should load folder by ID on init', () => {

View File

@@ -21,7 +21,7 @@ import {
} from '@angular/core';
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api';
import { AlfrescoTranslationService, DataColumnListComponent } from 'ng2-alfresco-core';
import { DataCellEvent, DataColumn, DataRowActionEvent, DataRowEvent, DataSorting, DataTableComponent, ObjectDataColumn } from 'ng2-alfresco-datatable';
import { DataCellEvent, DataColumn, DataRow, DataRowActionEvent, DataRowEvent, DataSorting, DataTableComponent, ObjectDataColumn } from 'ng2-alfresco-datatable';
import { Observable, Subject } from 'rxjs/Rx';
import { ImageResolver, RowFilter, ShareDataRow, ShareDataTableAdapter } from './../data/share-datatable-adapter';
import { ContentActionModel } from './../models/content-action.model';
@@ -98,8 +98,8 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
@Input()
loading: boolean = false;
selection = new Array<MinimalNodeEntity>();
skipCount: number = 0;
pagination: Pagination;
@Input()
@@ -166,7 +166,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
constructor(private documentListService: DocumentListService,
private ngZone: NgZone,
translateService: AlfrescoTranslationService,
private el: ElementRef) {
private elementRef: ElementRef) {
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-documentlist', 'assets/ng2-alfresco-documentlist');
@@ -390,11 +390,10 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
val => {
if (this.isCurrentPageEmpty(val, skipCount)) {
this.updateSkipCount(skipCount - maxItems);
this.loadFolderNodesByFolderNodeId(id, maxItems, skipCount - maxItems).then(() => {
resolve(true);
}, (error) => {
reject(error);
});
this.loadFolderNodesByFolderNodeId(id, maxItems, skipCount - maxItems).then(
() => resolve(true),
error => reject(error)
);
} else {
this.data.loadPage(<NodePaging> val);
this.pagination = val.list.pagination;
@@ -458,7 +457,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
},
bubbles: true
});
this.el.nativeElement.dispatchEvent(domEvent);
this.elementRef.nativeElement.dispatchEvent(domEvent);
const event = new NodeEntityEvent(node);
this.nodeClick.emit(event);
@@ -478,11 +477,6 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
}
}
onRowClick(event: DataRowEvent) {
let item = (<ShareDataRow> event.value).node;
this.onNodeClick(item);
}
onNodeDblClick(node: MinimalNodeEntity) {
const domEvent = new CustomEvent('node-dblclick', {
detail: {
@@ -491,7 +485,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
},
bubbles: true
});
this.el.nativeElement.dispatchEvent(domEvent);
this.elementRef.nativeElement.dispatchEvent(domEvent);
const event = new NodeEntityEvent(node);
this.nodeDblClick.emit(event);
@@ -511,9 +505,26 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
}
}
onRowDblClick(event?: DataRowEvent) {
let item = (<ShareDataRow> event.value).node;
this.onNodeDblClick(item);
onNodeSelect(event: { row: ShareDataRow, selection: Array<ShareDataRow> }) {
this.selection = event.selection.map(entry => entry.node);
const domEvent = new CustomEvent('node-select', {
detail: {
node: event.row.node,
selection: this.selection
}
});
this.elementRef.nativeElement.dispatchEvent(domEvent);
}
onNodeUnselect(event: { row: ShareDataRow, selection: Array<ShareDataRow> }) {
this.selection = event.selection.map(entry => entry.node);
const domEvent = new CustomEvent('node-unselect', {
detail: {
node: event.row.node,
selection: this.selection
}
});
this.elementRef.nativeElement.dispatchEvent(domEvent);
}
onShowRowContextMenu(event: DataCellEvent) {