From 24bd860d38b965d20147696851d544b06ccb8ae7 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 19 Jul 2017 12:00:03 +0100 Subject: [PATCH] [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 --- .../app/components/files/files.component.html | 24 ++++++ .../app/components/files/files.component.ts | 12 +++ .../ng2-alfresco-datatable/README.md | 12 ++- .../datatable/datatable.component.html | 5 +- .../datatable/datatable.component.ts | 77 +++++++++++++++---- .../loading-template.directive.spec.ts | 37 +++++---- .../directives/loading-template.directive.ts | 4 +- .../no-content-template.directive.spec.ts | 37 +++++---- .../no-content-template.directive.ts | 4 +- .../ng2-alfresco-documentlist/README.md | 5 ++ .../components/document-list.component.html | 6 +- .../document-list.component.spec.ts | 12 +-- .../src/components/document-list.component.ts | 47 ++++++----- 13 files changed, 205 insertions(+), 77 deletions(-) diff --git a/demo-shell-ng2/app/components/files/files.component.html b/demo-shell-ng2/app/components/files/files.component.html index 5e58b21a8d..deeecb3c98 100644 --- a/demo-shell-ng2/app/components/files/files.component.html +++ b/demo-shell-ng2/app/components/files/files.component.html @@ -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 @@ +
+ Selected Nodes: + +
+
+
+ Multiselect (with checkboxes) +
+
Dropdown breadcrumb
@@ -204,6 +219,15 @@
+
+

For 'Multiple' selection mode use Cmd (macOS) or Ctrl (Win) to toggle selection of multiple items.

+ + + {{mode.viewValue}} + + +
+
diff --git a/demo-shell-ng2/app/components/files/files.component.ts b/demo-shell-ng2/app/components/files/files.component.ts index ffd4d3f177..b01780e294 100644 --- a/demo-shell-ng2/app/components/files/files.component.ts +++ b/demo-shell-ng2/app/components/files/files.component.ts @@ -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; diff --git a/ng2-components/ng2-alfresco-datatable/README.md b/ng2-components/ng2-alfresco-datatable/README.md index 468e3117ff..22c0c562d1 100644 --- a/ng2-components/ng2-alfresco-datatable/README.md +++ b/ng2-components/ng2-alfresco-datatable/README.md @@ -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 diff --git a/ng2-components/ng2-alfresco-datatable/src/components/datatable/datatable.component.html b/ng2-components/ng2-alfresco-datatable/src/components/datatable/datatable.component.html index 5b22d92ace..a5c3ffcd1f 100644 --- a/ng2-components/ng2-alfresco-datatable/src/components/datatable/datatable.component.html +++ b/ng2-components/ng2-alfresco-datatable/src/components/datatable/datatable.component.html @@ -55,7 +55,10 @@ - + + ; - public loadingTemplate: TemplateRef; + noContentTemplate: TemplateRef; + loadingTemplate: TemplateRef; isSelectAllChecked: boolean = false; + selection = new Array(); private clickObserver: Observer; private click$: Observable; @@ -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 = 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); + } + } + } + } } diff --git a/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.spec.ts b/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.spec.ts index c815b33e4f..d377e2622c 100644 --- a/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.spec.ts +++ b/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.spec.ts @@ -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); }); }); diff --git a/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.ts b/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.ts index 242f7864aa..720c7739ae 100644 --- a/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.ts +++ b/ng2-components/ng2-alfresco-datatable/src/directives/loading-template.directive.ts @@ -30,7 +30,9 @@ export class LoadingContentTemplateDirective implements AfterContentInit { } ngAfterContentInit() { - this.dataTable.loadingTemplate = this.template; + if (this.dataTable) { + this.dataTable.loadingTemplate = this.template; + } } } diff --git a/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.spec.ts b/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.spec.ts index be28055e8b..d53a282a2f 100644 --- a/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.spec.ts +++ b/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.spec.ts @@ -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); }); }); diff --git a/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.ts b/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.ts index 539ed82490..2645bcf47b 100644 --- a/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.ts +++ b/ng2-components/ng2-alfresco-datatable/src/directives/no-content-template.directive.ts @@ -30,6 +30,8 @@ export class NoContentTemplateDirective implements AfterContentInit { } ngAfterContentInit() { - this.dataTable.noContentTemplate = this.template; + if (this.dataTable) { + this.dataTable.noContentTemplate = this.template; + } } } diff --git a/ng2-components/ng2-alfresco-documentlist/README.md b/ng2-components/ng2-alfresco-documentlist/README.md index 136caf64b4..44a1044e5f 100644 --- a/ng2-components/ng2-alfresco-documentlist/README.md +++ b/ng2-components/ng2-alfresco-documentlist/README.md @@ -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 | [] | 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: diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.html b/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.html index 119ec59589..4c4b76b02d 100644 --- a/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.html +++ b/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.html @@ -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)">
diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.spec.ts b/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.spec.ts index 3855f8bd6f..faa77a2028 100644 --- a/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.spec.ts +++ b/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.spec.ts @@ -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', () => { diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.ts b/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.ts index 6c684c8429..d71cf93b89 100644 --- a/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.ts +++ b/ng2-components/ng2-alfresco-documentlist/src/components/document-list.component.ts @@ -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(); 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( 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 = ( 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 = ( event.value).node; - this.onNodeDblClick(item); + onNodeSelect(event: { row: ShareDataRow, selection: Array }) { + 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 }) { + 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) {