svogv
Version:
A decorator based approach for model driven forms, including an advanced DataGrid and a TreeView component.
1,315 lines (1,290 loc) • 88.3 kB
JavaScript
import { Injectable, Inject, EventEmitter, Component, Input, Output, ViewChild, ContentChild, Pipe, Injector, ElementRef, Renderer2, NgModule } from '@angular/core';
import { Validators, FormBuilder, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
/**
* This decorator is for validation of mandatory fields.
* The default message is 'The field {keyName} is required'.
*
* @param msg The error message shown in case of error. A default value is being provided if omitted.
*
*/
function Required(msg) {
function requiredInternalSetup(target, key) {
Object.defineProperty(target, `__isRequired__${key}`, {
get() { return true; },
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errRequired__${key}`, {
value: msg || `The field ${key} is required`,
enumerable: false,
configurable: false
});
}
// the original decorator
function requiredInternal(target, property) {
requiredInternalSetup(target, property.toString());
}
// return the decorator
return requiredInternal;
}
/**
* The maxlength decorator assures that a string field contains not more than a number of characters.
*
* @param len: the maximum length.
* @param msg: A custom message.
*
*/
function MaxLength(len, msg) {
function maxLengthInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasMaxLength__${key}`, {
value: len,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errMaxLength__${key}`, {
value: msg || `The field ${key} has max length of ${len} characters`,
enumerable: false,
configurable: false
});
}
// the original decorator
function maxLengthInternal(target, property) {
maxLengthInternalSetup(target, property.toString());
}
// return the decorator
return maxLengthInternal;
}
/**
* The minlength decorator assures that a string field contains at least a number of characters.
*
* @param len: the required length.
* @param msg: A custom message.
*
*/
function MinLength(len, msg) {
function minLengthInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasMinLength__${key}`, {
value: len,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errMinLength__${key}`, {
value: msg || `The field ${key} needs at least ${len} characters`,
enumerable: false,
configurable: false
});
}
// the original decorator
function minLengthInternal(target, property) {
minLengthInternalSetup(target, property.toString());
}
// return the decorator
return minLengthInternal;
}
/**
* The decorator that assures that a string field contains at least a number of characters and a minimum number, too.
* The default message is 'The field {fieldname} needs at least {minlength} characters'.
*
* @param min: The required length.
* @param max: The maximum length.
* @param msg: Optionally a custom message.
*
*/
function StringLength(min, max, msg) {
function stringLengthInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasMaxLength__${key}`, {
value: max,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errMaxLength__${key}`, {
value: msg || `The field ${key} has max length of ${max} characters`,
enumerable: false,
configurable: false
});
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasMinLength__${key}`, {
value: min,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errMinLength__${key}`, {
value: msg || `The field ${key} needs at least ${min} characters`,
enumerable: false,
configurable: false
});
}
// the original decorator
function stringLengthInternal(target, property) {
stringLengthInternalSetup(target, property.toString());
}
// return the decorator
return stringLengthInternal;
}
/**
* The decorator assures that a string field fullfilles a regular expression pattern.
*
* @param pattern: The expression as RegExp.
* @param msg: A custom message.
*
*/
function Pattern(pattern, msg) {
function patternInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasPattern__${key}`, {
value: true,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errPattern__${key}`, {
value: msg || `The field ${key} must fullfill the pattern ${pattern}`,
enumerable: false,
configurable: false
});
}
// the original decorator
function patternInternal(target, property) {
patternInternalSetup(target, property.toString());
}
// return the decorator
return patternInternal;
}
/**
* Validates a field against an email pattern.
* Based on "pattern", so in form one must use `hasError('pattern')` to get validation results.
*
* @param msg A custom message. If not provided "The field ffff must contain a valid e-mail address."
* will be generated, while ffff is the property name.
*
*/
function Email(msg) {
function emailInternalSetup(target, key) {
// tslint:disable-next-line:max-line-length
const pattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasPattern__${key}`, {
value: pattern,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errPattern__${key}`, {
value: msg || `The field ${key} must contain a valid e-mail address.`,
enumerable: false,
configurable: false
});
}
// the original decorator
function emailInternal(target, property) {
emailInternalSetup(target, property.toString());
}
// return the decorator
return emailInternal;
}
/**
* Validates a field against an range. Applies to numerical values or dates.
*
* The range's values are included in the valid range.
*
* @param from The minimum value (included) as number or Date
* @param to The maximum value (included) as number or Date
* @param msg A custom message. If not provided "The field [field] does not fall into the range from [from] to [to]"
* will be generated, while [field] is the propertie's name.
*/
function Range(from, to, msg) {
function rangeInternalSetup(target, key) {
// property value
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasRangeFrom__${key}`, {
value: from,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__hasRangeTo__${key}`, {
value: to,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errRange__${key}`, {
value: msg
|| `The field ${key} does not fall into the range from ${from} to ${to}`,
enumerable: false,
configurable: false
});
}
// the original decorator
function rangeInternal(target, property) {
rangeInternalSetup(target, property.toString());
}
// return the decorator
return rangeInternal;
}
/**
* The compare decorator compares two field's values and
* shows an error message on the decorated field. The other field (compared to) does
* not has a decorator nor receives a message.
*
* @param withProperty A string that represents the compared field's name.
* @param msg A custom message.
*
*/
function Compare(withProperty, msg) {
function compareInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__hasCompareProperty__${key}`, {
value: true,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__withCompare__${key}`, {
value: withProperty,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__errCompareProperty__${key}`, {
value: msg
|| `The field ${key} must have the same value as field ${withProperty}`,
enumerable: false,
configurable: false
});
}
// the original decorator
function compareInternal(target, property) {
compareInternalSetup(target, property.toString());
}
// return the decorator
return compareInternal;
}
const displayName = 'displayName';
const displayOrder = 'displayOrder';
const displayDesc = 'displayDesc';
/**
* The Display decorator.
*
* This decorator can be used on fields. It's being used to create label in {@link EditorComponent} and
* headers in the {@link DataGridComponent}. Additional parameters are provided to refine forms further.
*
* @param name The Name or Label that appears in forms or as header in grids.
* @param order If one uses `AutoFormComponent` to create a whole form from a model, this controls the element's order.
* @param description A tooltip, which can be used optionally.
*/
function Display(name, order = 0, description) {
function displayInternalSetup(target, key) {
order = parseInt(order.toString(), 10);
// create a helper property to transport a meta data value
Object.defineProperty(target, `__${displayName}__${key}`, {
value: name,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__${displayOrder}__${key}`, {
value: order,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__${displayDesc}__${key}`, {
value: description,
enumerable: false,
configurable: false
});
}
// the original decorator
function displayInternal(target, property) {
displayInternalSetup(target, property.toString());
}
// return the decorator
return displayInternal;
}
/**
* Internal access to the provided meta data value for the name property.
*/
Display.Name = (target, key, def) => target[`__${displayName}__${key}`] || def;
/**
* Internal access to the provided meta data value for the order property.
*/
Display.Order = (target, key, def) => target[`__${displayOrder}__${key}`] || def;
/**
* Internal access to the provided meta data value for the description property.
*/
Display.Desc = (target, key, def) => target[`__${displayDesc}__${key}`] || def;
const uiHint = 'uiHint';
/**
* The UiHint decorator.
* Currently it can contain any set of style rules that apply to the <th> element that forms the grid's table header cells.
* The application makes use of the [ngStyle] directive. The object's structure must be made in a way [ngStyle] can handle it.
*
* @param hide The style definition.
*/
function UiHint(uiHintRule) {
function uiHintInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__${uiHint}__${key}`, {
value: uiHintRule,
enumerable: false,
configurable: false
});
}
// the original decorator
function uiHintInternal(target, property) {
uiHintInternalSetup(target, property.toString());
}
// return the decorator
return uiHintInternal;
}
UiHint.HintRule = (target, key, def) => target[`__${uiHint}__${key}`] || def;
/**
* The DisplayGroup decorator. Groups fields in auto forms; see {@link AutoFormComponent}.
* Just define a name (that appears as the group's name) and
* put the very same name on all members of the group.
*
* @param name The Name or Label that appears in forms as the groups legend.
* @param order If one uses {@link AutoFormComponent} to create a whole form from a model, this controls the groups order.
* @param description A tooltip, which can be used optionally.
*/
function DisplayGroup(name, order = 0, description) {
function displayGroupInternalSetup(target, key) {
order = parseInt(order.toString(), 10);
// create a helper property to transport a meta data value
Object.defineProperty(target, `__isGrouped__${key}`, {
value: true,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__groupName__${key}`, {
value: name,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__groupOrder__${key}`, {
value: order,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__groupDesc__${key}`, {
value: description,
enumerable: false,
configurable: false
});
}
// the original decorator
function displayGroupInternal(target, property) {
displayGroupInternalSetup(target, property.toString());
}
// return the decorator
return displayGroupInternal;
}
/**
* The Placeholder decorator.
*
* The placeholder adds the given text as a watermark to any input fields.
* There is no function in the {@link DataGridComponent}.
*
* @param name The Name that appears in form fields as a watermark.
*/
function Placeholder(name) {
function placeholderInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__watermark__${key}`, {
value: name,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__hasWatermark__${key}`, {
value: true,
enumerable: false,
configurable: false
});
}
// the original decorator
function placeholderInternal(target, property) {
placeholderInternalSetup(target, property.toString());
}
// return the decorator
return placeholderInternal;
}
/**
* The TemplateHint decorator.
*
* One can define the way a property gets rendered.
* Currently supported:
* - TextArea
* - Calendar
* - Range
* - Number
* - Text
*
* The Calendar creates Date-field. However, in casde of a datatype Date the date field will be created anyway.
*
* @param template The Name that appears in form fields as a watermark.
* @param params Depending of template some additional values as a dictionary.
*/
function TemplateHint(template, params) {
function templateHintInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__templatehint__${key}`, {
value: template,
enumerable: false,
configurable: false
});
if (params) {
Object.defineProperty(target, `__templatehintParams__${key}`, {
value: params,
enumerable: false,
configurable: false
});
}
Object.defineProperty(target, `__hasTemplateHint__${key}`, {
value: true,
enumerable: false,
configurable: false
});
}
// the original decorator
function templateHintInternal(target, name) {
templateHintInternalSetup(target, name);
}
// return the decorator
return templateHintInternal;
}
const isHidden = 'isHidden';
/**
* The Hidden decorator.
*
* The {@link DataGrid} does not show columns for properties tagged with {@link `Hidden`} decorator.
* Fields in forms that render automatically
* using the {@link `EditorComponent`} will render as `<input type="hidden">`.
*
* @param hide Optional, default is `true`.
*/
function Hidden(hide = true) {
function hiddenInternalSetup(target, key, hide) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__${isHidden}__${key}`, {
value: hide,
enumerable: false,
configurable: false
});
}
// the original decorator
function hiddenInternal(target, property) {
hiddenInternalSetup(target, property.toString(), hide);
}
// return the decorator
return hiddenInternal;
}
Hidden.IsHidden = (target, key, def = false) => target[`__${isHidden}__${key}`] || def;
const isSortable = 'isSortable';
const hasSortCallback = 'sortCallback';
/**
* The Sortable decorator.
*
* The {@link `DataGrid` does not sort columns for properties tagged with}`@Sortable(false)`.
* The default is that all columsn are sortable. Either avoid this decorator or use `@Sortable(true)`.
* Additionally, if the decorator is provided, you can add a sort function callback as second parameter.
*
* @param canSort Suppress or allow sorting.
* @param sortCallback An optional callback that provides a sort instruction. If omitted, `Array.prototype.sort` is being used.
*/
function Sortable(canSort, sortCallback) {
function sortableInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__${isSortable}__${key}`, {
value: canSort,
enumerable: false,
configurable: false
});
Object.defineProperty(target, `__${hasSortCallback}__${key}`, {
value: sortCallback,
enumerable: false,
configurable: false
});
}
// the original decorator
function sortableInternal(target, property) {
sortableInternalSetup(target, property.toString());
}
// return the decorator
return sortableInternal;
}
Sortable.IsSortable = (target, key, def) => target[`__${isSortable}__${key}`] || def;
Sortable.SortCallback = (target, key, def) => target[`__${hasSortCallback}__${key}`] || def;
/**
* The Readonly decorator. The field is readonly in the form. It just renders grayed out
* and handles the internals using default HTML5 techniques.
*
*
* @param readonly Optional, default is true.
* @param description A tooltip that can be used optionally.
*/
function Readonly(readonly = true) {
function readonlyInternalSetup(target, key) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__isReadonly__${key}`, {
value: readonly,
enumerable: false,
configurable: false
});
}
// the original decorator
function readonlyInternal(target, property) {
readonlyInternalSetup(target, property.toString());
}
// return the decorator
return readonlyInternal;
}
/**
* The FormatPipe decorator. Provide the name of a Pipe that's being used by the
* dynamic pipe formatter. Hence, the form does not need to apply forms manually.
* The reason is that you may create forms automatically and hence can't write
* actual Pipes somewhere. This applies especially if you create a table and loop
* through properties.
*
* @param pipe The name of the pipe's type.
* @param pipeParams The custom pipe's parameters. This is optional and can be omitted.
*
* @example
* @FormatPipe(SomePipe)
* public string formattedProperty = '';
*/
function FormatPipe(pipe, pipeParams = null) {
function formatInternalSetup(target, key, innerPipe, innerPipeParams = null) {
// create a helper property to transport a meta data value
Object.defineProperty(target, `__uipipe__${key}`, {
value: innerPipe,
enumerable: false,
configurable: false
});
if (innerPipeParams && innerPipeParams.length) {
Object.defineProperty(target, `__pipeparams__${key}`, {
value: innerPipeParams,
enumerable: false,
configurable: false
});
}
}
// the original decorator
function formatInternal(target, property) {
formatInternalSetup(target, property.toString(), pipe, pipeParams);
}
// return the decorator
return formatInternal;
}
/**
* A custom validator to valdiate a range of numbers or dates.
* This is internally to support the infarstructure and not intended to being used by custom code.
*
* @param p The field's name
*
*/
function validateRange(f, t) {
return function (c) {
if ((Number(f) || Number(t)) && Number(c.value)) {
const fr = Number(f);
const to = Number(t);
const v = Number(c.value);
return (!fr || v >= fr) && (!to || v <= to)
? null
: {
range: {
valid: false
}
};
}
if ((Date.parse(f.toString()) || Date.parse(t.toString())) && Date.parse(c.value)) {
const fr = Date.parse(f.toString());
const to = Date.parse(t.toString());
const v = Date.parse(c.value);
return (!fr || v >= fr) && (!to || v <= to)
? null
: {
range: {
valid: false
}
};
}
};
}
/**
* A custom validator to compare two fields. This is internally to support the infrastructure
* and not intended to being used by custom code.
*
* @param p The field's name
*
*/
function validateCompare(p) {
let changeEventWasAdded = false;
return function (c) {
const form = c.root;
if (form && form.controls && !changeEventWasAdded) {
form.controls[p].valueChanges.subscribe(() => {
// trigger validation for particular element
c.updateValueAndValidity();
});
changeEventWasAdded = true;
}
if (c.value) {
// compare the current value with the referenced control's value
return !c.value || c.value === c.root['controls'][p].value
? null
: {
compare: {
valid: false
}
};
}
};
}
/**
* The form validator service creates a {@link FormGroup} object from a viewmodel. If the viewmodel
* has been decorated with validation decorators the validators are created accordingly.
*
* The simplest way is creating a class with properties and add decorators, such as
* {@link StringLength}. The service will than create a {@link FormGroup} that contains a}validator
* of type {@link StringLength} for the property the decorator is written}on.
*
* The decorators provide properties for additional information, such as a custom error message.
*
*/
class FormValidatorService {
constructor(fb) {
this.fb = fb;
}
/**
* Call this method to actually create the FormGroup object. Provide a valid model type.
*
* @param target A valid model type.
* @returns A FormGroup with validators
*/
build(target) {
const valGroup = {};
const errGroup = {};
let form;
let targetInstance;
if (target) {
// the cast is just to suppress TS errors and shows it's intentionally
try {
targetInstance = new target();
}
catch (ex) {
console.error('Invalid viewmodel for FormValidatorService');
}
}
if (targetInstance) {
// tslint:disable-next-line:forin
for (const propName in targetInstance) {
const validators = new Array();
const errmsgs = new Object();
const isRequired = `__isRequired__${propName}` in target.prototype;
if (isRequired) {
(errmsgs)['required'] = target.prototype[`__errRequired__${propName}`];
validators.push(Validators.required);
}
const hasMaxLength = `__hasMaxLength__${propName}` in target.prototype;
if (hasMaxLength) {
(errmsgs)['maxlength'] = target.prototype[`__errMaxLength__${propName}`];
const maxLength = parseInt(target.prototype[`__hasMaxLength__${propName}`], 10);
validators.push(Validators.maxLength(maxLength));
}
const hasMinLength = `__hasMinLength__${propName}` in target.prototype;
if (hasMinLength) {
(errmsgs)['minlength'] = target.prototype[`__errMinLength__${propName}`];
const minLength = parseInt(target.prototype[`__hasMinLength__${propName}`], 10);
validators.push(Validators.minLength(minLength));
}
const hasPattern = `__hasPattern__${propName}` in target.prototype;
if (hasPattern) {
(errmsgs)['pattern'] = target.prototype[`__errPattern__${propName}`];
const pattern = new RegExp(target.prototype[`__hasPattern__${propName}`]);
validators.push(Validators.pattern(pattern));
}
const hasRangeFrom = `__hasRangeFrom__${propName}` in target.prototype;
const hasRangeTo = `__hasRangeTo__${propName}` in target.prototype;
if (hasRangeFrom || hasRangeTo) {
(errmsgs)['range'] = target.prototype[`__errRange__${propName}`];
let f = Number(target.prototype[`__hasRangeFrom__${propName}`]);
let t = Number(target.prototype[`__hasRangeTo__${propName}`]);
if (!f && !t) {
// If NaN assume Date
f = Date.parse(f.toString());
t = Date.parse(t.toString());
}
validators.push(validateRange(f, t));
}
const hasCompare = `__hasCompareProperty__${propName}` in target.prototype;
if (hasCompare) {
(errmsgs)['compare'] = target.prototype[`__errCompareProperty__${propName}`];
const compare = target.prototype[`__withCompare__${propName}`];
validators.push(validateCompare(compare));
}
if (validators.length === 0) {
// even if there is no validator we need to add the property to the group
(valGroup)[propName] = [target[propName]];
}
if (validators.length === 1) {
(valGroup)[propName] = [target[propName] || '', validators[0]];
}
if (validators.length >= 1) {
(valGroup)[propName] = [target[propName] || '', Validators.compose(validators)];
}
(errGroup)[propName] = errmsgs;
}
// create form group
form = this.fb.group(valGroup);
// forward the model to the editors for easy access to other decorators
// the cast is just to suppress TS errors and shows it's intentionally
(form)['__editorModel__'] = targetInstance;
// register controls and add messages
// tslint:disable-next-line:forin
for (const propName in errGroup) {
const ctrl = form.controls[propName];
if (!ctrl) {
continue; // control might not be in the form
}
(form.controls[propName])['messages'] = (errGroup)[propName];
}
}
// return FormGroup for immediate usage
return form;
}
}
FormValidatorService.decorators = [
{ type: Injectable }
];
FormValidatorService.ctorParameters = () => [
{ type: FormBuilder, decorators: [{ type: Inject, args: [FormBuilder,] }] }
];
/**
* The pagination component creates a few buttons to navigate a grid. The underlaying model
* is going to handle the date on the client. The pagination does not support a server backend,
* all relevant data must be loaded first.
*
* Example of usage:
* @example
* ```html
* <ac-pagination></ac-pagination>
* ```
*
* <example-url>/#/widget/grid</example-url>
*/
class DataGridPaginationComponent {
constructor() {
/**
* An event fired once the user has changed the page by clicking a button.
*/
this.pageNumberChanged = new EventEmitter();
this.currentPageNumber = 1;
}
ngOnInit() {
this.setCurrentPage(1);
}
ngOnChanges(changes) {
if (changes['maxPageIndex']) {
const change = changes['maxPageIndex'];
if (this.currentPageNumber > change.currentValue) {
// throws ExpressionChangedAfterItHasBeenCheckedException
// if there's no setTimeout.
// no need to add setTimeout if ngOnChanges
// is fired after changes made on root component.
setTimeout(() => this.setCurrentPage(1), 1);
}
}
}
setCurrentPage(pageNumber, event) {
if (event) {
event.preventDefault();
}
if (pageNumber === 0 || pageNumber > this.maxPageIndex
|| pageNumber === this.currentPageNumber) {
return;
}
this.pageNumberChanged.emit(pageNumber);
this.currentPageNumber = pageNumber;
}
range(min, max) {
const result = new Array();
for (let i = min; i <= max; i++) {
result.push(i);
}
return result;
}
get pageStartNumber() {
const startNumber = this.currentPageNumber <= 4
? 1
: this.currentPageNumber >= this.maxPageIndex - 3
? this.maxPageIndex - 6
: this.currentPageNumber - 3;
return startNumber < 1 ? 1 : startNumber;
}
get pageEndNumber() {
const pageEnd = this.pageStartNumber + 6;
return pageEnd > this.maxPageIndex ? this.maxPageIndex : pageEnd;
}
}
DataGridPaginationComponent.decorators = [
{ type: Component, args: [{
selector: 'ac-datagrid-pagination',
template: "<div>\n <ul class=\"pagination float-right\" [ngClass]=\"{ 'pagination-sm': size == 'sm', 'pagination-lg': size == 'lg' }\">\n <li [class.disabled]=\"currentPageNumber === 1 || !maxPageIndex\" class=\"page-item\">\n <a href (click)=\"setCurrentPage(1, $event)\" aria-label=\"Previous\" class=\"page-link\">\n <span aria-hidden=\"true\">\u00AB</span>\n </a>\n </li>\n <li [class.disabled]=\"currentPageNumber === 1 || !maxPageIndex\" class=\"page-item\">\n <a\n href\n aria-label=\"Previous\"\n (click)=\"setCurrentPage(currentPageNumber - 1, $event)\"\n class=\"page-link\">\n <span aria-hidden=\"true\">\u2039</span>\n </a>\n </li>\n <li\n *ngFor=\"let index of range(pageStartNumber, pageEndNumber)\"\n [class.active]=\"currentPageNumber === index\"\n class=\"page-item\">\n <a href (click)=\"setCurrentPage(index, $event)\" class=\"page-link\">\n <span aria-hidden=\"true\">{{ index }}</span>\n </a>\n </li>\n <li [class.disabled]=\"currentPageNumber === maxPageIndex || !maxPageIndex\">\n <a class=\"page-link\"\n href\n (click)=\"setCurrentPage(currentPageNumber + 1, $event)\"\n aria-label=\"Last\"\n >\n <span aria-hidden=\"true\">\u203A</span>\n </a>\n </li>\n <li [class.disabled]=\"currentPageNumber === maxPageIndex || !maxPageIndex\" class=\"page-item\">\n <a href (click)=\"setCurrentPage(maxPageIndex, $event)\" aria-label=\"Last\" class=\"page-link\">\n <span aria-hidden=\"true\">\u00BB</span>\n </a>\n </li>\n </ul>\n</div>\n",
styles: [""]
},] }
];
DataGridPaginationComponent.propDecorators = {
maxPageIndex: [{ type: Input }],
pageNumberChanged: [{ type: Output }],
size: [{ type: Input }]
};
/**
* @ignore
*/
Object.same = function (source, target) {
if (source === target) {
return true;
}
if (!(source instanceof Object) || !(target instanceof Object)) {
return false;
}
// if they are not strictly equal, they both need to be Objects
for (const prop in source) {
if (!source.hasOwnProperty(prop)) {
continue;
}
if (source[prop] === undefined || source[prop] === null || source[prop] === '') {
continue;
}
if (typeof source[prop] === 'object' && Object.same(source[prop], target[prop])) {
continue;
}
if (typeof source[prop] === 'string' && target[prop].startsWith(source[prop])) {
continue;
}
if (source[prop] === target[prop]) {
continue;
}
return false;
}
return true;
};
/**
* @ignore
*/
Object.equals = function (x, y) {
if (x === y) {
return true;
}
// if both x and y are null or undefined and exactly the same
if (!(x instanceof Object) || !(y instanceof Object)) {
return false;
}
// if they are not strictly equal, they both need to be Objects
if (x.constructor !== y.constructor) {
return false;
}
// they must have the exact same prototype chain, the closest we can do is
// test there constructor.
for (const p in x) {
if (!x.hasOwnProperty(p)) {
continue;
}
// other properties were tested using x.constructor === y.constructor
if (!y.hasOwnProperty(p)) {
return false;
}
// allows to compare x[ p ] and y[ p ] when set to undefined
if (x[p] === y[p]) {
continue;
}
// if they have the same strict value or identity then they are equal
if (typeof (x[p]) !== 'object') {
return false;
}
// Numbers, Strings, Functions, Booleans must be strictly equal
if (!Object.equals(x[p], y[p])) {
return false;
}
// Objects and Arrays must be tested recursively
}
for (const p in y) {
if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
return false;
}
// allows x[ p ] to be set to undefined
}
return true;
};
/**
* Describe a header field with name, tooltip and other properties.
*/
class DataGridHeaderModel {
/**
* The ctor
* @param text The text to display.
* @param desc A tooltip that is shown on mouseover (using the `title` attribute).
* @param prop The propertie's internal name.
* @param hidden optionally set a field as hidden and hence do not show in the grid.
*/
constructor(text, desc, prop, hidden = false) {
this.text = text;
this.desc = desc;
this.prop = prop;
this.hidden = hidden;
this.isSortable = true;
this.templateHint = 'text';
}
}
/**
* Sort direction, controlled by simple string comparision or a callback.
*/
var Direction;
(function (Direction) {
Direction[Direction["Ascending"] = 0] = "Ascending";
Direction[Direction["Descending"] = 1] = "Descending";
})(Direction || (Direction = {}));
/**
* The controlling class for Grid applications.
*
* This class takes an array of elements and handles:
* - visible headers, managed by @Hidden() decorator
* - create header titles, managed by @Display() decorator
* - sorting
* - filtering
* - count total rows
* - paging
*/
class DataGridModel {
constructor(items, type, pageSize = 10) {
/**
* The search value filters the rows. Provide the property name and the filter instruction. Search is pure client.
*/
this.searchValue = {};
this.currentPageIndex = 1;
/**
* Event fired if user clicks Edit button.
*/
this.onEdit = new EventEmitter();
/**
* Event fired if user clicks Delete button.
*/
this.onDelete = new EventEmitter();
/**
* Current sort direction per column.
*/
this.sortDirection = {};
this._items = items;
this.pageSize = pageSize;
const typeInstance = new type();
if (typeInstance) {
// make header from decorators, omit if null
this.createHeadersForType(typeInstance);
}
}
/**
* Returns the number of rows regardless the actual filter (the total).
*/
get totalRows() {
return this._items.length;
}
get totalFilteredRows() {
return this.itemsFiltered ? this.itemsFiltered.length : 0;
}
get currentRowStart() {
return this.totalRows > this.pageSize ? this.startRow + 1 : this.totalRows === 0 ? 0 : 1;
}
get currentRowEnd() {
return this.startRow + this.pageSize < this.totalRows ? this.startRow + this.pageSize : this.totalRows;
}
get startRow() {
if (this.currentPageIndex === 0) {
return 0;
}
return (this.currentPageIndex - 1) * this.pageSize;
}
get maxPageIndex() {
const index = Math.ceil(this.totalFilteredRows / this.pageSize);
return index;
}
set items(value) {
this._items = value;
}
get items() {
return this._items;
}
get itemsFiltered() {
// not actually a filter present
if (!this.searchValue || (Object.keys(this.searchValue).length === 0 && this.searchValue.constructor === Object)) {
return this.items;
}
return this.items.filter((item) => {
// tslint:disable-next-line:forin
for (const s in this.searchValue) {
const pattern = new RegExp(this.searchValue[s]);
if (pattern.test(item[s])) {
return true;
}
}
return false;
});
}
get itemsOnCurrentPage() {
return this.itemsFiltered.slice(this.startRow, this.startRow + this.pageSize);
}
/**
* Get all headers (column names) and their properties.
*/
get headers() {
return this._headers.filter((h) => !h.hidden);
}
/**
* Returns the columns currently not shown. {@link addColumn and @see removeColumn for more}information.
*/
get headersNotVisible() {
return this._headers.filter((h) => h.hidden);
}
/**
* Simple sort fucntion that makes a array sort call for the specified column.
* @param colName The column which has to be sorted after.
* // tslint:disable-next-line:max-line-length
* @param dir The order, descended is *desc*, any other string is ascending.
* If nothing is provided, the direction toggles. Initital value is *ascending*.
*/
sortColumn(colName, dir, sortCallback) {
if (!dir) {
// if nothing is provided, toggle current
dir = this.sortDirection[colName] === Direction.Ascending ? Direction.Descending : Direction.Ascending;
}
// remember last and update UI
this.sortDirection[colName] = dir;
if (sortCallback) {
this.items.sort(sortCallback);
}
else {
this.items.sort((a, b) => {
if (dir === Direction.Descending) {
return a[colName] > b[colName] ? 1 : -1;
}
else {
return a[colName] > b[colName] ? -1 : 1;
}
});
}
}
/**
* Make a column invisible. This is just changing the render process, the column is still
* in the headers collection and can be made visible again by calling {@link addColumn}later.
*/
removeColumn(colname) {
const col = this._headers.find((h) => h.prop === colname);
if (col) {
col.hidden = true;
}
}
/**
* Add a column to the current grid, that has been removed recently.
* It's just adding columns that already exists in the headers collection.
* If the column name provided does not exists, the method does nothing.
*/
addColumn(colname) {
const col = this._headers.find((h) => h.prop === colname);
if (col) {
col.hidden = false;
}
}
/**
* Called by infrastructure to inform caller of edit wish
* @param item The item to edit
*/
editItem(item) {
this.onEdit.emit(item);
}
/**
* Called by infrastructure to inform caller of delete wish
* @param item The item to delete
*/
deleteItem(item) {
this.onDelete.emit(item);
}
createHeadersForType(type) {
// assume simple object structure, iterating an array of viewmodels
// has at least one row, so we can read the headers
// first we read the properties
this._headers = new Array();
for (const p in type) {
if (!type.hasOwnProperty(p)) {
continue;
}
const propName = Display.Name(type, p, p);
const propDesc = Display.Desc(type, p, p);
// check if hidden, show if no hidden decorator
const isHidden = Hidden.IsHidden(type, p, false);
const header = new DataGridHeaderModel(propName, propDesc, p, isHidden);
// sorting
header.isSortable = type[`__isSortable__${p}`] === undefined ? true : !!type[`__isSortable__${p}`];
header.sortCallback = type[`__sortCallback__${p}`] || undefined;
// look for templates and pipes provided by user, if none, we have templates for all ES types
header.templateHint = type[`__templatehint__${p}`] || typeof type[p];
header.templateHintParams = type[`__templatehintParams__${p}`];
header.pipe = type[`__uipipe__${p}`];
header.pipeParams = type[`__pipeparams__${p}`];
header.uiHint = UiHint.HintRule(type, p, {});
this._headers.push(header);
}
}
}
/**
* A classic data grid. You provide a model to handle all features. The model is build from a
* simple array of objects with decorators.
*
* > See 'Documentation and Examples' tab for a complete documentation.
*
* ### Summary
*
* The datagrid provides basic functions for data tables:
*
* * sorting
* * filtering
* * pagination
* * editing
*
* Provide an decorator enhanced model and the grid appears driven by model meta data.
*
* There are many attributes and ways to change the appearance. Also some classes can be controlled by
* the host component:
*
* * `.col-borders`
* * `.col-last`
* * `.col-first`
*
* All these styles are applied to the <col> elements of the underlying table.
*
* The model used in the example is an array of objects, where the properties are decorated with
* various decorators used to control the grid's render behavior.
*
* ~~~typescript
* const data: UserViewModel[] = this.dataSource; // provide a simple array here
* this.model = new DataGridModel<UserViewModel>(data, UserViewModel);
* ~~~
*
* The class {@link DataGridModel} controls the grid. You must provide a viewmodel, this is mandatory. The viewmodel is being examined at runtime,
* so assure you provide a class and set all properties to a default to force creation of properties.
*
* ~~~typescript
* export class UserViewModel {
* email: string = ''; // the = '' is necessary!
* // more omitted for brevity
* }
* ~~~
*
* The grid can be extended with the {@link DataGridPaginationComponent} to page through huge data sets. The model
* handles the pagination, the additional {@link DataGridPaginationComponent} is only a predefined renderer that
* supports the used theme.
*
* <example-url>/#/widget/grid</example-url>
*
* @example
* <ac-datagrid
* [model]="model"
* [showActions]="false"
* [columnStyle]=""
* ></ac-datagrid>
*/
class DataGridComponent {
constructor() {
/**
* @ignore
*/
this.directionEnumHelper = Direction;
this.externals = {};
/**
* Show the action column at all. Use {@link showDeleteButton and @see showEditButton to switch the}buttons
* on or off individually. Default is `true` (actions visible).
*/
this.showActions = true;
/**
* The text that appears on the Delete button. Default is 'Delete'.
*/
this.textDeleteButton = 'Delete';
/**
* The text that appears on the Edit button. Default is 'Edit'.
*/
this.textEditButton = 'Edit';
/**
* The column header of the column that shows the buttons. Default is 'Actions'.
*/
this.textButtonsHeader = 'Actions';
/**
* The text that appears if there are no items to show. Can also be overwritten by a more complex piece
* of code by adding a template like this:
*
* @example
* <ng-template #data-warning-noitems>
* <div class="alert alert-danger">The grid is empty</div>
* </ng-template>
*/
this.textNoItems = 'There are no items to show';
/**
* Event forwarded from model class and being fired after the model class's onEdit event.
* The event is invoked by the appropriate internal button via click.
*/
this.editItem = new EventEmitter();
/**
* Event forwarded from model class and being fired after the model class's onDelete event.
* The event is invoked by the appropriate internal button via click.
*/
this.deleteItem = new EventEmitter();
// tslint:disable-next-line:member-ordering
/**
* @ignore
*/
this.warnProp = {};
}
/**
* The filter value to filter the content. The data is of type `{ [prop: string]: any }`.
*
* @param value A dictionary with filter instructions as shown below. The filter logic applies one after another, like an __AND__ conjunction.
*
* @example
* {
* "email": "paul@sample.com"
* "name": "Paul"
* }
*
*/
set filter(value) {
if (this.model) {
this.model.searchValue = value;
}
}
ngAfterViewInit() {
if (this.model) {
this.model.onEdit.subscribe(item => this.editItem.emit(item));
this.model.onDelete.subscribe(item => this.deleteItem.emit(item));
}
}
ngOnDestroy() {
if (this.model) {
this.model.onEdit.unsubscribe();
this.model.onDelete.unsubscribe();
}
}
/**
* @ignore
* Controls the template used to display certain data types.
* If the host provides a template it's being used, otherwise a fallback is provided
* @param uiHint Property of {@link UiHint} decorator
*/
getActiveTemplate(uiHint, prop) {
if (this[uiHint]) {
// if provided by user via ContentChild and overwriting defaults (string == string etc.)
return this[uiHint];
}
if (this.externals[uiHint]) {
// if provided by user via ContentChild but completely replaced
return this.externals[uiHint];
}
if (this[`${uiHint}Fallback`]) {
// otherwise we take ours from ng-template via ViewChild
return this[`${uiHint}Fallback`];
}
// if we go here the model requested a custom templat