Configure if columns can select row (Fixes #7)

This commit is contained in:
Zakary Timson 2018-09-17 11:21:14 -04:00
parent b8960b0eb2
commit 7345e2ed27
6 changed files with 174 additions and 165 deletions

View File

@ -4,7 +4,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 4
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true

View File

@ -1,6 +1,6 @@
{ {
"name": "@ztimson/ng-datatable", "name": "@ztimson/ng-datatable",
"version": "1.9.7", "version": "1.10.7",
"homepage": "https://github.com/ztimson/ng-datatable", "homepage": "https://github.com/ztimson/ng-datatable",
"license": "Apache-2.0", "license": "Apache-2.0",
"author": { "author": {

View File

@ -3,6 +3,7 @@ import {TemplateRef} from "@angular/core";
export interface Column { export interface Column {
aggregate?: (rows: any[]) => any; aggregate?: (rows: any[]) => any;
cssClass?: string; // CSS to add to column cssClass?: string; // CSS to add to column
canSelect?: boolean;
hide?: boolean; // Hide column hide?: boolean; // Hide column
hideMobile?: boolean; // Hide column on mobile hideMobile?: boolean; // Hide column on mobile
initialSort?: 'asc' | 'desc'; // Sort this column initially initialSort?: 'asc' | 'desc'; // Sort this column initially

View File

@ -25,16 +25,16 @@
</thead> </thead>
<tbody class="ngdt-body"> <tbody class="ngdt-body">
<ng-container *ngFor="let row of pagedData; let i = index"> <ng-container *ngFor="let row of pagedData; let i = index">
<tr class="ngdt-row" [ngClass]="{'active': selectedRows.has(i)}" (click)="updateSelected(i)"> <tr class="ngdt-row" [ngClass]="{'active': selectedRows.has(i)}">
<td *ngIf="showCheckbox && selectionMode !== null" class="ngdt-checkbox"> <td *ngIf="showCheckbox && selectionMode !== null" class="ngdt-checkbox" (click)="updateSelected(i)">
<input type="checkbox" [checked]="selectedRows.has(i)"/> <input type="checkbox" [checked]="selectedRows.has(i)"/>
</td> </td>
<td *ngIf="expandedTemplate" class="ngdt-expand"> <td *ngIf="expandedTemplate" class="ngdt-expand" (click)="updateSelected(i)">
<span *ngIf="!selectedRows.has(i)">&#9658;</span> <span *ngIf="!selectedRows.has(i)">&#9658;</span>
<span *ngIf="selectedRows.has(i)">&#9660;</span> <span *ngIf="selectedRows.has(i)">&#9660;</span>
</td> </td>
<ng-container *ngFor="let c of columns"> <ng-container *ngFor="let c of columns">
<td *ngIf="c.hide !== true && !(c.hideMobile === true && width < mobileBreakpoint)" class="ngdt-cell"> <td *ngIf="c.hide !== true && !(c.hideMobile === true && width < mobileBreakpoint)" class="ngdt-cell" (click)="updateSelected(i, c)">
<ng-template #defaultTemplate let-value="value">{{value}}</ng-template> <ng-template #defaultTemplate let-value="value">{{value}}</ng-template>
<ng-template [ngTemplateOutlet]="c.template || defaultTemplate" <ng-template [ngTemplateOutlet]="c.template || defaultTemplate"
[ngTemplateOutletContext]="{object: row, value: _dotNotation(row, c.property)}"> [ngTemplateOutletContext]="{object: row, value: _dotNotation(row, c.property)}">

View File

@ -2,171 +2,179 @@ import {Component, EventEmitter, HostListener, Input, OnInit, Output, TemplateRe
import {Column} from './column'; import {Column} from './column';
@Component({ @Component({
selector: 'ng-datatable', selector: 'ng-datatable',
templateUrl: 'ng-datatable.component.html', templateUrl: 'ng-datatable.component.html',
styles: ['.ngdt-expand {font-family: sans-serif;}'] styles: ['.ngdt-expand {font-family: sans-serif;}']
}) })
export class NgDatatableComponent implements OnInit { export class NgDatatableComponent implements OnInit {
// Inputs ============================================================================================================ // Inputs ============================================================================================================
@Input() cssClass: string; // CSS class to add to table element @Input() cssClass: string; // CSS class to add to table element
@Input() columns: Column[] = []; // Columns to display on table @Input() columns: Column[] = []; // Columns to display on table
@Input() expandedTemplate: TemplateRef<any>; // Template to use when expanding columns @Input() expandedTemplate: TemplateRef<any>; // Template to use when expanding columns
@Input() mobileBreakpoint: number = 768; // Hide mobile false columns when screen size is less than @Input() mobileBreakpoint: number = 768; // Hide mobile false columns when screen size is less than
@Input() pageLength: number = 20; // Number of rows per page @Input() pageLength: number = 20; // Number of rows per page
@Input() page: number = 1; // Current page number @Input() page: number = 1; // Current page number
@Input() paginate: boolean = true; // Should we paginate results @Input() paginate: boolean = true; // Should we paginate results
@Input() paginateCssClass: string; // CSS class to add to paginator @Input() paginateCssClass: string; // CSS class to add to paginator
@Input() selectionMode: null | 'single' | 'multi'; // Allow selecting no/single/multiple rows @Input() selectionMode: null | 'single' | 'multi'; // Allow selecting no/single/multiple rows
@Input() showCheckbox: boolean; // Selection checkboxes @Input() showCheckbox: boolean; // Selection checkboxes
@Input() tableLayout: 'auto' | 'fixed' = 'auto'; // How column widths are decided @Input() tableLayout: 'auto' | 'fixed' = 'auto'; // How column widths are decided
// Outputs =========================================================================================================== // Outputs ===========================================================================================================
@Output() filterChanged = new EventEmitter<any[]>(); // Output when filters change @Output() filterChanged = new EventEmitter<any[]>(); // Output when filters change
@Output() finished = new EventEmitter<any[]>(); // Fired after processing is finished @Output() finished = new EventEmitter<any[]>(); // Fired after processing is finished
@Output() pageChanged = new EventEmitter<number>(); // Output when page is changed @Output() pageChanged = new EventEmitter<number>(); // Output when page is changed
@Output() processing = new EventEmitter<any[]>(); // Fires when grid begins to process @Output() processing = new EventEmitter<any[]>(); // Fires when grid begins to process
@Output() selectionChanged = new EventEmitter<any[]>(); // Output when selected rows changes @Output() selectionChanged = new EventEmitter<any[]>(); // Output when selected rows changes
// Properties ======================================================================================================== // Properties ========================================================================================================
filters: ((el?: any, i?: number, arr?: any[]) => boolean)[] = []; // Array of process functions to apply to data filters: ((el?: any, i?: number, arr?: any[]) => boolean)[] = []; // Array of process functions to apply to data
pages: number[] = []; // Array of possible pages pages: number[] = []; // Array of possible pages
pagedData: any[] = []; // The data for the current pagedData: any[] = []; // The data for the current
processedData: any[] = []; // rows left after filtering processedData: any[] = []; // rows left after filtering
selectedRows = new Set<number>(); // Keep track of selected rows selectedRows = new Set<number>(); // Keep track of selected rows
sortedColumn: number; // Column currently being sorted sortedColumn: number; // Column currently being sorted
sortedDesc = false; // Is the sorted column being sorted in ascending or descending order sortedDesc = false; // Is the sorted column being sorted in ascending or descending order
width = window.innerWidth; // Width of the screen. Used for hiding mobile columns width = window.innerWidth; // Width of the screen. Used for hiding mobile columns
// Fields ============================================================================================================ // Fields ============================================================================================================
get count(): number { return this.processedData ? this.processedData.length : 0; } // Number of rows after filtering get count(): number {
private _data: any[] = []; // Original data entered into table return this.processedData ? this.processedData.length : 0;
get data(): any[] { return this.processedData; } // Return the processed data } // Number of rows after filtering
@Input() set data(data: any[]) { private _data: any[] = []; // Original data entered into table
this._data = data; get data(): any[] {
this.refresh(); return this.processedData;
} } // Return the processed data
@Input() set data(data: any[]) {
// =================================================================================================================== this._data = data;
constructor() { } this.refresh();
ngOnInit() {
// Look through columns for an initial sort
for(let i = 0; i < this.columns.length; i++) {
if(this.columns[i].initialSort) {
this.sort(i, (this.columns[i].initialSort == 'desc'));
break;
}
}
}
// Helpers ===========================================================================================================
_convertWidth(width) {
if(typeof width == 'number') return `${width}px`;
return width;
}
_dotNotation(obj: object, prop: string) {
return prop.split('.').reduce((obj, prop) => obj[prop], obj);
}
addFilter(...filters: ((row?: any, index?: number, arr?: any[]) => boolean)[]) {
this.filters = this.filters.concat(filters);
this.refresh();
this.filterChanged.emit(this.filters);
}
aggregate(col: Column) {
if(!col.aggregate) return '';
return col.aggregate(this.processedData.map(row => this._dotNotation(row, col.property)));
}
changePage(page: number) {
if(!this.paginate || page < 1 || page > this.pages.length) return;
this.page = page;
this.refresh();
this.pageChanged.emit(this.page);
}
clearFilters(update=true) {
this.filters = [];
if(update) this.refresh();
this.filterChanged.emit(this.filters);
}
clearSelected() {
let emit = this.selectedRows.size > 0;
this.selectedRows.clear();
if(emit) this.selectionChanged.emit([]);
}
@HostListener('window:resize', ['$event'])
onResize(event) { this.width = event.target.innerWidth; }
refresh() {
this.processing.emit(this.processedData);
this.clearSelected();
this.processedData = this._data;
this.filters.forEach(f => this.processedData = this.processedData.filter(f));
if(this.sortedColumn != null && this.processedData) {
if (this.columns[this.sortedColumn].sortFn) {
this.processedData = this.processedData.sort(this.columns[this.sortedColumn].sortFn);
} else {
this.processedData = this.processedData.sort((a: any, b: any) => {
if (this._dotNotation(a, this.columns[this.sortedColumn].property) > this._dotNotation(b, this.columns[this.sortedColumn].property)) return 1;
if (this._dotNotation(a, this.columns[this.sortedColumn].property) < this._dotNotation(b, this.columns[this.sortedColumn].property)) return -1;
return 0;
});
}
if (this.sortedDesc) this.processedData = this.processedData.reverse();
} }
if(this.paginate && this.processedData) { // ===================================================================================================================
this.pages = Array(Math.ceil(this.processedData.length / this.pageLength)).fill(0).map((ignore, i) => i + 1); constructor() {
if(!this.page) this.page = 1;
if(this.page > this.pages.length) this.page = this.pages.length;
this.pagedData = this.processedData.filter((ignore, i) => i >= (this.page - 1) * this.pageLength && i < this.page * this.pageLength);
} else {
this.pagedData = this.processedData;
}
this.finished.emit(this.processedData);
}
selectAll() {
this.processedData.forEach((ignore, i) => this.selectedRows.add(i));
this.selectionChanged.emit(this.processedData);
}
sort(columnIndex: number, desc?: boolean) {
let column = this.columns[columnIndex];
if (!column || column.sort === false) return; // If column is un-sortable return
// Figure out sorting direction if not supplied
if(desc === undefined) {
desc = false;
if(columnIndex == this.sortedColumn) desc = !this.sortedDesc;
}
this.sortedColumn = columnIndex;
this.sortedDesc = desc;
// Preform sort
this.refresh();
}
updateSelected(index: number) {
if (this.selectionMode == null) return;
if (this.selectionMode == 'single') {
let alreadySelected = this.selectedRows.has(index);
this.selectedRows.clear();
if(!alreadySelected) this.selectedRows.add(index);
} else {
if (this.selectedRows.has(index)) {
this.selectedRows.delete(index);
} else {
this.selectedRows.add(index);
}
} }
this.selectionChanged.emit(this.processedData.filter((row, i) => this.selectedRows.has(i))); ngOnInit() {
} // Look through columns for an initial sort
for (let i = 0; i < this.columns.length; i++) {
if (this.columns[i].initialSort) {
this.sort(i, (this.columns[i].initialSort == 'desc'));
break;
}
}
}
// Helpers ===========================================================================================================
_convertWidth(width) {
if (typeof width == 'number') return `${width}px`;
return width;
}
_dotNotation(obj: object, prop: string) {
return prop.split('.').reduce((obj, prop) => obj[prop], obj);
}
addFilter(...filters: ((row?: any, index?: number, arr?: any[]) => boolean)[]) {
this.filters = this.filters.concat(filters);
this.refresh();
this.filterChanged.emit(this.filters);
}
aggregate(col: Column) {
if (!col.aggregate) return '';
return col.aggregate(this.processedData.map(row => this._dotNotation(row, col.property)));
}
changePage(page: number) {
if (!this.paginate || page < 1 || page > this.pages.length) return;
this.page = page;
this.refresh();
this.pageChanged.emit(this.page);
}
clearFilters(update = true) {
this.filters = [];
if (update) this.refresh();
this.filterChanged.emit(this.filters);
}
clearSelected() {
let emit = this.selectedRows.size > 0;
this.selectedRows.clear();
if (emit) this.selectionChanged.emit([]);
}
@HostListener('window:resize', ['$event'])
onResize(event) {
this.width = event.target.innerWidth;
}
refresh() {
this.processing.emit(this.processedData);
this.clearSelected();
this.processedData = this._data;
this.filters.forEach(f => this.processedData = this.processedData.filter(f));
if (this.sortedColumn != null && this.processedData) {
if (this.columns[this.sortedColumn].sortFn) {
this.processedData = this.processedData.sort(this.columns[this.sortedColumn].sortFn);
} else {
this.processedData = this.processedData.sort((a: any, b: any) => {
if (this._dotNotation(a, this.columns[this.sortedColumn].property) > this._dotNotation(b, this.columns[this.sortedColumn].property)) return 1;
if (this._dotNotation(a, this.columns[this.sortedColumn].property) < this._dotNotation(b, this.columns[this.sortedColumn].property)) return -1;
return 0;
});
}
if (this.sortedDesc) this.processedData = this.processedData.reverse();
}
if (this.paginate && this.processedData) {
this.pages = Array(Math.ceil(this.processedData.length / this.pageLength)).fill(0).map((ignore, i) => i + 1);
if (!this.page) this.page = 1;
if (this.page > this.pages.length) this.page = this.pages.length;
this.pagedData = this.processedData.filter((ignore, i) => i >= (this.page - 1) * this.pageLength && i < this.page * this.pageLength);
} else {
this.pagedData = this.processedData;
}
this.finished.emit(this.processedData);
}
selectAll() {
this.processedData.forEach((ignore, i) => this.selectedRows.add(i));
this.selectionChanged.emit(this.processedData);
}
sort(columnIndex: number, desc?: boolean) {
let column = this.columns[columnIndex];
if (!column || column.sort === false) return; // If column is un-sortable return
// Figure out sorting direction if not supplied
if (desc === undefined) {
desc = false;
if (columnIndex == this.sortedColumn) desc = !this.sortedDesc;
}
this.sortedColumn = columnIndex;
this.sortedDesc = desc;
// Preform sort
this.refresh();
}
updateSelected(index: number, column?: Column) {
if (this.selectionMode == null) return;
if (column && column.canSelect === false) return;
if (this.selectionMode == 'single') {
let alreadySelected = this.selectedRows.has(index);
this.selectedRows.clear();
if (!alreadySelected) this.selectedRows.add(index);
} else {
if (this.selectedRows.has(index)) {
this.selectedRows.delete(index);
} else {
this.selectedRows.add(index);
}
}
this.selectionChanged.emit(this.processedData.filter((row, i) => this.selectedRows.has(i)));
}
} }

View File

@ -61,7 +61,7 @@ export class AppComponent implements OnInit {
return `Males: ${total.M}, Females: ${total.F}`; return `Males: ${total.M}, Females: ${total.F}`;
} }
}, },
{label: 'Age', property: 'age', initialSort: 'desc', hideMobile: true, template: this.ageTemplate} {label: 'Age', canSelect: false, property: 'age', initialSort: 'desc', hideMobile: true, template: this.ageTemplate}
]; ];
this.search.subscribe(text => { this.search.subscribe(text => {