Skip to content

Commit

Permalink
feat(editor): add new Dual Input Editor & extract all Editor Validato…
Browse files Browse the repository at this point in the history
…rs (#446)

Co-authored-by: Ghislain Beaulac <ghislain.beaulac@se.com>
  • Loading branch information
ghiscoding and ghiscoding-SE authored May 5, 2020
1 parent 96e2973 commit 06f5dc9
Show file tree
Hide file tree
Showing 27 changed files with 1,722 additions and 281 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@
"dependencies": {
"@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0",
"dompurify": "^2.0.8",
"excel-builder-webpacker": "^1.0.4",
"dompurify": "^2.0.10",
"excel-builder-webpacker": "^1.0.5",
"flatpickr": ">=4.5.0",
"font-awesome": "^4.7.0",
"jquery": "^3.4.1",
Expand Down Expand Up @@ -112,9 +112,9 @@
"@types/dompurify": "^2.0.1",
"@types/flatpickr": "^3.1.2",
"@types/jest": "^24.0.25",
"@types/jquery": "^3.3.33",
"@types/jquery": "^3.3.37",
"@types/moment": "^2.13.0",
"@types/node": "^13.1.4",
"@types/node": "^13.13.4",
"@types/text-encoding-utf-8": "^1.0.1",
"babel-jest": "^24.9.0",
"bootstrap": "3.4.1",
Expand All @@ -123,7 +123,7 @@
"conventional-changelog": "^3.1.18",
"copyfiles": "^2.2.0",
"core-js": "^2.6.11",
"cross-env": "^7.0.0",
"cross-env": "^7.0.2",
"custom-event-polyfill": "^1.0.7",
"del": "^5.1.0",
"del-cli": "^3.0.0",
Expand All @@ -137,7 +137,7 @@
"jest-preset-angular": "^6.0.1",
"ng-packagr": "~5.3.0",
"ngx-bootstrap": "^4.3.0",
"node-sass": "^4.13.1",
"node-sass": "4.14.0",
"npm-run-all": "^4.1.5",
"postcss-cli": "^7.1.0",
"require-dir": "^1.2.0",
Expand Down
2 changes: 1 addition & 1 deletion src/app/examples/grid-editor.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ <h2>{{title}}</h2>
<angular-slickgrid gridId="grid2" (onAngularGridCreated)="angularGridReady($event)"
(sgOnCellChange)="onCellChanged($event.detail.eventData, $event.detail.args)"
(sgOnClick)="onCellClicked($event.detail.eventData, $event.detail.args)"
(sgOnValidationError)="onCellValidation($event.detail.eventData, $event.detail.args)"
(sgOnValidationError)="onValidationError($event.detail.eventData, $event.detail.args)"
[columnDefinitions]="columnDefinitions" [gridOptions]="gridOptions" [dataset]="dataset">
</angular-slickgrid>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/examples/grid-editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ export class GridEditorComponent implements OnInit {
}
}

onCellValidation(e, args) {
onValidationError(e, args) {
alert(args.validationResults.msg);
}

Expand Down
71 changes: 36 additions & 35 deletions src/app/examples/grid-frozen.component.html
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
<div class="container-fluid">
<h2>{{title}}</h2>
<div class="subtitle" [innerHTML]="subTitle"></div>
<h2>{{title}}</h2>
<div class="subtitle" [innerHTML]="subTitle"></div>

<br>
<br>

<div class="row col-sm-12">
<span>
<label for="">Pinned Rows: </label>
<input type="number" [(ngModel)]="frozenRowCount">
<button class="btn btn-default btn-xs" (click)="changeFrozenRowCount()">
Set
</button>
</span>
<span style="margin-left: 10px">
<label for="">Pinned Columns: </label>
<input type="number" [(ngModel)]="frozenColumnCount">
<button class="btn btn-default btn-xs" (click)="changeFrozenColumnCount()">
Set
</button>
</span>
<span style="margin-left: 15px">
<button class="btn btn-default btn-sm" (click)="toggleFrozenBottomRows()">
<i class="fa fa-random fa-lg"></i> Toggle Pinned Rows
</button>
<span style="font-weight: bold;">: {{ isFrozenBottom ? 'Bottom' : 'Top' }}</span>
</span>
</div>
<div class="row col-sm-12">
<span>
<label for="">Pinned Rows: </label>
<input type="number" [(ngModel)]="frozenRowCount">
<button class="btn btn-default btn-xs" (click)="changeFrozenRowCount()">
Set
</button>
</span>
<span style="margin-left: 10px">
<label for="">Pinned Columns: </label>
<input type="number" [(ngModel)]="frozenColumnCount">
<button class="btn btn-default btn-xs" (click)="changeFrozenColumnCount()">
Set
</button>
</span>
<span style="margin-left: 15px">
<button class="btn btn-default btn-sm" (click)="toggleFrozenBottomRows()">
<i class="fa fa-random fa-lg"></i> Toggle Pinned Rows
</button>
<span style="font-weight: bold;">: {{ isFrozenBottom ? 'Bottom' : 'Top' }}</span>
</span>
</div>

<div class="col-sm-12">
<hr>
</div>
<div class="col-sm-12">
<hr>
</div>

<angular-slickgrid gridId="grid20"
gridWidth="875"
[columnDefinitions]="columnDefinitions"
[gridOptions]="gridOptions"
[dataset]="dataset"
(onAngularGridCreated)="angularGridReady($event)">
</angular-slickgrid>
<angular-slickgrid gridId="grid20"
gridWidth="875"
[columnDefinitions]="columnDefinitions"
[gridOptions]="gridOptions"
[dataset]="dataset"
(sgOnValidationError)="onValidationError($event.detail.eventData, $event.detail.args)"
(onAngularGridCreated)="angularGridReady($event)">
</angular-slickgrid>
</div>
118 changes: 104 additions & 14 deletions src/app/examples/grid-frozen.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { AngularGridInstance, Column, FieldType, Formatters, Filters, GridOption } from './../modules/angular-slickgrid';
import { AngularGridInstance, Column, ColumnEditorDualInput, Editors, FieldType, formatNumber, Formatters, Filters, GridOption } from './../modules/angular-slickgrid';

@Component({
templateUrl: './grid-frozen.component.html',
Expand Down Expand Up @@ -64,12 +64,6 @@ export class GridFrozenComponent implements OnInit {
filterable: true,
sortable: true
},
{
id: 'duration', name: 'Duration', field: 'duration',
minWidth: 100, width: 120,
filterable: true,
sortable: true
},
{
id: 'percentComplete', name: '% Complete', field: 'percentComplete',
resizable: false,
Expand All @@ -92,6 +86,78 @@ export class GridFrozenComponent implements OnInit {
filterable: true,
sortable: true
},
{
id: 'cost', name: 'Cost | Duration', field: 'cost',
formatter: this.costDurationFormatter.bind(this),
minWidth: 150, width: 170,
sortable: true,
// filterable: true,
filter: {
model: Filters.compoundSlider,
},
editor: {
model: Editors.dualInput,
// the DualInputEditor is of Type ColumnEditorDualInput and MUST include (leftInput/rightInput) in its params object
// in each of these 2 properties, you can pass any regular properties of a column editor
// and they will be executed following the options defined in each
params: {
leftInput: {
field: 'cost',
type: 'float',
decimal: 2,
minValue: 0,
maxValue: 50000,
placeholder: '< 50K',
errorMessage: 'Cost must be positive and below $50K.',
},
rightInput: {
field: 'duration',
type: 'float', // you could have 2 different input type as well
minValue: 0,
maxValue: 100,
title: 'make sure Duration is withing its range of 0 to 100',
errorMessage: 'Duration must be between 0 and 100.',

// Validator Option #1
// You could also optionally define a custom validator in 1 or both inputs
/*
validator: (value, args) => {
let isValid = true;
let errorMsg = '';
if (value < 0 || value > 120) {
isValid = false;
errorMsg = 'Duration MUST be between 0 and 120.';
}
return { valid: isValid, msg: errorMsg };
}
*/
},
} as ColumnEditorDualInput,

// Validator Option #2 (shared Validator) - this is the last alternative, option #1 (independent Validators) is still the recommended way
// You can also optionally use a common Validator (if you do then you cannot use the leftInput/rightInput validators at same time)
// to compare both values at the same time.
/*
validator: (values, args) => {
let isValid = true;
let errorMsg = '';
if (values.cost < 0 || values.cost > 50000) {
isValid = false;
errorMsg = 'Cost MUST be between 0 and 50k.';
}
if (values.duration < 0 || values.duration > 120) {
isValid = false;
errorMsg = 'Duration MUST be between 0 and 120.';
}
if (values.cost < values.duration) {
isValid = false;
errorMsg = 'Cost can never be lower than its Duration.';
}
return { valid: isValid, msg: errorMsg };
}
*/
}
},
{
id: 'effortDriven', name: 'Effort Driven', field: 'effortDriven',
minWidth: 100, width: 120,
Expand Down Expand Up @@ -137,6 +203,8 @@ export class GridFrozenComponent implements OnInit {
alwaysShowVerticalScroll: false, // disable scroll since we don't want it to show on the left pinned columns
enableExcelCopyBuffer: true,
enableCellNavigation: true,
editable: true,
autoEdit: true,
asyncEditorLoading: true,
frozenColumn: this.frozenColumnCount,
frozenRow: this.frozenRowCount,
Expand All @@ -151,18 +219,23 @@ export class GridFrozenComponent implements OnInit {
// Set up some test columns.
const mockDataset = [];
for (let i = 0; i < 500; i++) {
const randomYear = 2000 + Math.floor(Math.random() * 10);
const randomMonth = Math.floor(Math.random() * 11);
const randomDay = Math.floor((Math.random() * 29));

mockDataset[i] = {
id: i,
title: 'Task ' + i,
duration: Math.round(Math.random() * 25) + ' days',
cost: (i % 33 === 0) ? null : Math.random() * 10000,
duration: i % 8 ? (Math.round(Math.random() * 100) + '') : null,
percentComplete: Math.round(Math.random() * 100),
start: '01/01/2009',
finish: '01/05/2009',
start: new Date(randomYear, randomMonth, randomDay),
finish: new Date(randomYear, (randomMonth + 1), randomDay),
effortDriven: (i % 5 === 0),
title1: Math.round(Math.random() * 25),
title2: Math.round(Math.random() * 25),
title3: Math.round(Math.random() * 25),
title4: Math.round(Math.random() * 25),
title1: `Some Text ${Math.round(Math.random() * 25)}`,
title2: `Some Text ${Math.round(Math.random() * 25)}`,
title3: `Some Text ${Math.round(Math.random() * 25)}`,
title4: `Some Text ${Math.round(Math.random() * 25)}`,
};
}
return mockDataset;
Expand All @@ -186,6 +259,23 @@ export class GridFrozenComponent implements OnInit {
}
}

costDurationFormatter(row, cell, value, columnDef, dataContext) {
const costText = this.isNullUndefinedOrEmpty(dataContext.cost) ? 'n/a' : formatNumber(dataContext.cost, 0, 2, false, '$', '', '.', ',');
let durationText = 'n/a';
if (!this.isNullUndefinedOrEmpty(dataContext.duration) && dataContext.duration >= 0) {
durationText = `${dataContext.duration} ${dataContext.duration > 1 ? 'days' : 'day'}`;
}
return `<b>${costText}</b> | ${durationText}`;
}

isNullUndefinedOrEmpty(data: any) {
return (data === '' || data === null || data === undefined);
}

onValidationError(e, args) {
alert(args.validationResults.msg);
}

/** toggle dynamically, through slickgrid "setOptions()" the top/bottom pinned location */
toggleFrozenBottomRows() {
if (this.gridObj && this.gridObj.setOptions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Constants } from '../constants';
import { EditorValidatorOutput } from '../models/editorValidatorOutput.interface';
import { EditorValidator } from '../models/editorValidator.interface';

interface FloatValidatorOptions {
editorArgs: any;
decimal?: number;
errorMessage?: string;
minValue?: string | number;
maxValue?: string | number;
operatorConditionalType?: 'inclusive' | 'exclusive';
required?: boolean;
validator?: EditorValidator;
}

export function floatValidator(inputValue: any, options: FloatValidatorOptions): EditorValidatorOutput {
const floatNumber = !isNaN(inputValue as number) ? parseFloat(inputValue) : null;
const decPlaces = options.decimal || 0;
const isRequired = options.required;
const minValue = options.minValue;
const maxValue = options.maxValue;
const operatorConditionalType = options.operatorConditionalType || 'inclusive';
const errorMsg = options.errorMessage;
const mapValidation = {
'{{minValue}}': minValue,
'{{maxValue}}': maxValue,
'{{minDecimal}}': 0,
'{{maxDecimal}}': decPlaces
};
let isValid = true;
let outputMsg = '';

if (typeof options.validator === 'function') {
return options.validator(inputValue, options.editorArgs);
} else if (isRequired && inputValue === '') {
isValid = false;
outputMsg = errorMsg || Constants.VALIDATION_REQUIRED_FIELD;
} else if (inputValue !== '' && (isNaN(inputValue as number) || (decPlaces === 0 && !/^[-+]?(\d+(\.)?(\d)*)$/.test(inputValue)))) {
// when decimal value is 0 (which is the default), we accept 0 or more decimal values
isValid = false;
outputMsg = errorMsg || Constants.VALIDATION_EDITOR_VALID_NUMBER;
} else if (minValue !== undefined && maxValue !== undefined && floatNumber !== null && ((operatorConditionalType === 'exclusive' && (floatNumber <= minValue || floatNumber >= maxValue)) || (operatorConditionalType === 'inclusive' && (floatNumber < minValue || floatNumber > maxValue)))) {
// MIN & MAX Values provided
// when decimal value is bigger than 0, we only accept the decimal values as that value set
// for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals
isValid = false;
outputMsg = errorMsg || Constants.VALIDATION_EDITOR_NUMBER_BETWEEN.replace(/{{minValue}}|{{maxValue}}/gi, (matched) => mapValidation[matched]);
} else if (minValue !== undefined && floatNumber !== null && ((operatorConditionalType === 'exclusive' && floatNumber <= minValue) || (operatorConditionalType === 'inclusive' && floatNumber < minValue))) {
// MIN VALUE ONLY
// when decimal value is bigger than 0, we only accept the decimal values as that value set
// for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals
isValid = false;
outputMsg = errorMsg || Constants.VALIDATION_EDITOR_NUMBER_MIN.replace(/{{minValue}}/gi, (matched) => mapValidation[matched]);
} else if (maxValue !== undefined && floatNumber !== null && ((operatorConditionalType === 'exclusive' && floatNumber >= maxValue) || (operatorConditionalType === 'inclusive' && floatNumber > maxValue))) {
// MAX VALUE ONLY
// when decimal value is bigger than 0, we only accept the decimal values as that value set
// for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals
isValid = false;
outputMsg = errorMsg || Constants.VALIDATION_EDITOR_NUMBER_MAX.replace(/{{maxValue}}/gi, (matched) => mapValidation[matched]);
} else if ((decPlaces > 0 && !new RegExp(`^[-+]?(\\d*(\\.)?(\\d){0,${decPlaces}})$`).test(inputValue))) {
// when decimal value is bigger than 0, we only accept the decimal values as that value set
// for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals
isValid = false;
outputMsg = errorMsg || Constants.VALIDATION_EDITOR_DECIMAL_BETWEEN.replace(/{{minDecimal}}|{{maxDecimal}}/gi, (matched) => mapValidation[matched]);
}

return { valid: isValid, msg: outputMsg };
}
4 changes: 4 additions & 0 deletions src/app/modules/angular-slickgrid/editorValidators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './floatValidator';
export * from './integerValidator';
export * from './sliderValidator';
export * from './textValidator';
Loading

0 comments on commit 06f5dc9

Please sign in to comment.