UNPKG

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
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 &lt;th&gt; 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