right-angled
Version:
Lightweight and easy to use angular data grids. Integrates with your markup and styles rather than generating its own.
1,246 lines (1,233 loc) • 158 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Directive, Input, NgModule, InjectionToken, EventEmitter, Optional, SkipSelf, Inject, Self, Output, Component, Pipe, HostListener, HostBinding } from '@angular/core';
import * as i2 from '@angular/common';
import { CommonModule } from '@angular/common';
import { BehaviorSubject, Subject, scheduled, EMPTY, animationFrameScheduler } from 'rxjs';
import { __decorate } from 'tslib';
import { map, debounce } from 'rxjs/operators';
/**
* Represents possible values for operation status (e.g. of request to the server).
*/
var OperationStatus;
(function (OperationStatus) {
/**
* Nothing was performed before.
*/
OperationStatus[OperationStatus["Initial"] = 0] = "Initial";
/**
* Last operation completed successfully.
*/
OperationStatus[OperationStatus["Done"] = 1] = "Done";
/**
* Operation is performing right now.
*/
OperationStatus[OperationStatus["Progress"] = 2] = "Progress";
/**
* Last operation completed with failure.
*/
OperationStatus[OperationStatus["Fail"] = 3] = "Fail";
/**
* Last operation was cancelled.
*/
OperationStatus[OperationStatus["Cancelled"] = 4] = "Cancelled";
/**
* Last operation doesn't return any data.
*/
OperationStatus[OperationStatus["NoData"] = 5] = "NoData";
})(OperationStatus || (OperationStatus = {}));
/**
* Represents sort direction that applied as parameter by {@link SortParameter} class.
*/
var SortDirection;
(function (SortDirection) {
/**
* Ascending sort order.
*/
SortDirection[SortDirection["Asc"] = 0] = "Asc";
/**
* Descending sort order.
*/
SortDirection[SortDirection["Desc"] = 1] = "Desc";
})(SortDirection || (SortDirection = {}));
/**
* Copies values of all properties from passed object to the new object literal.
*
* If any of the properties of passed object is also a complex object then {@link cloneAsLiteral} will be called recursively.
*
* Function declarations are ignored.
* @param value value to clone.
* @returns resulted literal.
*/
function cloneAsLiteral(value) {
if (value === null) {
return null;
}
if (typeof value === 'undefined') {
return undefined;
}
if (Array.isArray(value)) {
return value.map(cloneAsLiteral);
}
if (typeof value === 'object') {
const result = {};
for (const index in value) {
if (Object.prototype.hasOwnProperty.call(value, index) && typeof value[index] !== 'function') {
result[index] = cloneAsLiteral(value[index]);
}
}
return result;
}
return value;
}
/**
* Set of key-value pairs which is used by {@link coerceValue} method to coerce specific values.
*/
// tslint:disable-next-line: object-literal-key-quotes
const coerceTypes = { true: !0, false: !1, null: null };
/**
* Coerce type of passed value.
*
* For example if you pass string with value 'null' it returns `null`, if you pass 'true' it returns boolean value `true`, if you pass '1.0' it returns number `1.0` etc.
*
* If passed value is complex object or array this method will be called for each property or array item.
* @param value value to coerce.
* @returns resulted value.
* @see {@link coerceTypes}
*/
function coerceValue(value) {
let result = value;
if (result === null) {
return null;
}
if (typeof result === 'undefined') {
return undefined;
}
if (typeof result === 'object' || Array.isArray(result)) {
for (const index in result) {
if (Object.prototype.hasOwnProperty.call(result, index)) {
result[index] = coerceValue(result[index]);
}
}
}
else if (result && result !== true && !isNaN(result)) {
result = +result;
}
else if (result === 'undefined') {
result = undefined;
}
else if (typeof coerceTypes[result] !== 'undefined') {
result = coerceTypes[result];
}
return result;
}
/**
* Cleaning up passed array by calling `splice` function.
*
* Next, each element of passed array will be checked for existence of `destroy` method and if it exists it will be called.
* @param collection array of elements to destroy.
* @param async if `true` then iterating over array and `destroy` methods calling will be executed via setTimeout (,0).
*/
function destroyAll(collection, async = true) {
if (!Array.isArray(collection)) {
return;
}
let items = collection.splice(0, collection.length);
if (async) {
setTimeout(() => {
items.forEach((item) => {
if (item && item.destroy) {
item.destroy();
}
});
items = null;
}, 0);
}
else {
items.forEach((item) => {
if (item && item.destroy) {
item.destroy();
}
});
}
}
/**
* Used for declarative building of objects which represent valuable state of `target object`.
*
* Since this declaration is pretty abstract and hard to understand, let's look at a specific sample.
*
* Typical usage of this service is something like this:
* ```JavaScript
* class EndUserClass {
* @filter
* public parameter1 = 'Hey';
* @filter
* public parameter2 = 'There';
* }
* let endUserClassInstance = new EndUserClass();
* let filterService = new FilterService(endUserClassInstance);
* ```
* Now we can use created `filtersService` instance for several cases:
* - by calling {@link getRequestState} we can get serializable representation of object state
* ```JavaScript
* {
* parameter1: 'Hey',
* parameter2: 'There'
* }
* ```
* - by calling {@link resetValues} we can reset values of annotated properties to initial values (or to what was specified in {@link FilterConfig.defaultValue} property).
* - by calling {@link applyParams} we can automatically apply any set of values to annotated properties (we can pass queryString object to automatically apply values from it, for example).
* - by calling {@link getRequestState} with some filters we can "query" the state of filters and save it to the settings storage, for example.
* - by calling {@link registerFilterTarget} you can add any count of additional objects and get their
* composed state via {@link getRequestState} method as well as process them all with {@link resetValues} and {@link applyParams}.
*/
class RTFiltersService {
constructor() {
/**
* Collection of {@link FilterConfig} settings that matches configuration for all objects that were registered in current service instance via {@link registerFilterTarget} method.
*
* This collection is also filled up with configurations of passed `target objects` base classes since config applicability is determined by `instanceof` check.
*
* This collection is "lazy" and will be filled up on first call of {@link resetValues}, {@link applyParams} or {@link getRequestState} method.
*/
this.appliedFiltersMap = new Map();
}
/**
* Global collection of all filters configuration.
*
* Used to build {@link appliedFiltersMap} for concrete set of objects that were registered as `target objects` via {@link registerFilterTarget} for concrete service instance.
*/
static { this.filterPropertiesMap = new Map(); }
/**
* @param target `target object` that will be registered with {@link registerFilterTarget} method.
*/
/**
* Used to register type property as `target property` with specified filter config for later usage with {@link FiltersService}.
*
* This method is called by {@link filter} annotation, for example.
* @param targetType type definition that contains specified property declaration.
* @param propertyConfig configuration for property as a filter.
*/
static registerFilterConfig(targetType, propertyConfig) {
const typeConfigs = RTFiltersService.filterPropertiesMap.has(targetType)
? RTFiltersService.filterPropertiesMap.get(targetType)
: new Array();
typeConfigs.push(propertyConfig);
RTFiltersService.filterPropertiesMap.set(targetType, typeConfigs);
}
/**
* Used to build resulted value of `target property` based on specified {@link FilterConfig}.
*
* This method is used by {@link getRequestState} and also calls oneself for the case of array values.
*
*
* In addition to {@link FilterConfig} configuration, this method checks if `target property` has method `toRequest()`. If so this method will be used to get serialized value.
*
* This convention has sense in several scenarios:
* - Serialization of complex object can be performed by it's own method which was declared once instead of copy-paste it in {@link FilterConfig.serializeFormatter} declarations.
* - Very tricky but sometimes useful usage of this convention is to declare `toRequest` in `Date` prototype.
*
* This gives ability to easily send `Date` objects to the server in appropriate format which server can apply or, for example, always send UTC-dates.
*
* But be accurate with this approach. There's a lot of problems with extending of embedded types.
*
* @param target `target` object which holds specified `target property`. Used as `this` scope for {@link FilterConfig.serializeFormatter} method.
* @param value raw value of `target` property.
* @param config filter configuration for `target` property.
*/
static buildFilterValue(target, value, config) {
let result = value;
if (config && config.serializeFormatter) {
return config.serializeFormatter.call(target, result);
}
result = config && config.emptyIsNull ? result || null : result;
result = config && config.coerce ? coerceValue(result) : result;
if (result && result.toRequest) {
return result.toRequest();
}
if (Array.isArray(result)) {
const temp = [];
for (let i = 0; i < result.length; i++) {
temp[i] = RTFiltersService.buildFilterValue(target, result[i], null);
}
return temp;
}
return result;
}
/**
* Performs service destroy.
*/
destroy() {
this.appliedFiltersMap.clear();
}
/**
* Goes through all `target properties` of all `target objects` and sets their values to configured default.
*
* Default value will be determined as:
* - If value for {@link FilterConfig.defaultValue} is specified it will be applied.
* - Otherwise this service writes to {@link FilterConfig.defaultValue} value of `target property` at the moment of first call of
* {@link resetValues}, {@link applyParams} or {@link getRequestState}.
*
* This method performs next actions:
* - If value specified as {@link FilterConfig.defaultValue} is function it will be called with `target object` as `this` scope. If specified value is not a function it will be used by itself.
* - Result of previous step will be cloned by {@link cloneLiteral} function to avoid possible reference types problems.
* - If {@link FilterConfig.parseFormatter} method is specified it will be called with previous step result as parameter.
* - Result of previous steps is applied as value to `target property`.
*/
resetValues() {
this.appliedFiltersMap.forEach((targetConfig, target) => {
for (const config of targetConfig) {
const defaultValue = typeof config.defaultValue === 'function' ? config.defaultValue.call(target) : config.defaultValue;
const clonedObject = cloneAsLiteral({ defaultValue });
this.setPropertyValue(target, config, config.parseFormatter ? config.parseFormatter.call(target, clonedObject.defaultValue) : clonedObject.defaultValue);
}
});
}
/**
* Goes through all `target properties` of all `target objects` and tries to find property within passed object which name is equal
* to configured {@link FilterConfig.parameterName}.
*
* If match is found, value of the property from passed object is applied to `target property` in accordance with
* {@link FilterConfig.ignoreOnAutoMap}, {@link FilterConfig.emptyIsNull}, {@link FilterConfig.coerce}, {@link FilterConfig.parseFormatter}.
* @param params - object with values to apply.
*/
applyParams(params) {
this.appliedFiltersMap.forEach((targetConfig, target) => {
for (const config of targetConfig) {
if (params && Object.prototype.hasOwnProperty.call(params, config.parameterName) && false === config.ignoreOnAutoMap) {
let proposedVal = config.emptyIsNull ? params[config.parameterName] || null : params[config.parameterName];
proposedVal = config.coerce ? coerceValue(proposedVal) : proposedVal;
this.setPropertyValue(target, config, config.parseFormatter ? config.parseFormatter.call(target, proposedVal, params) : proposedVal);
}
}
});
}
/**
* Goes through all `target properties` of all `target objects` and applies their values to one resulted object literal.
*
* Typical usage of this method is building request to send it to the server.
*
* Names of properties in result object depends on {@link FilterConfig.parameterName}. Final values would be constructed by {@link buildFilterValue} method.
* @param filterFn - optional function to filter applied values.
* @returns resulted object literal.
*/
getRequestState(filterFn) {
const result = {};
this.appliedFiltersMap.forEach((targetConfig, target) => {
for (let config of targetConfig) {
config = { ...config };
const proposedVal = this.getPropertyValue(target, config);
if (filterFn ? filterFn(config, proposedVal, target) : true) {
const resultValue = RTFiltersService.buildFilterValue(target, proposedVal, config);
if (!config.omitIfNullOrUndefined || (resultValue !== null && typeof resultValue !== 'undefined')) {
result[config.parameterName] = resultValue;
}
}
}
});
return result;
}
/**
* Registers passed object as `target object` for current service instance.
*
* {@link getRequestState} method will compose result from objects that were registered by this method.
*
* {@link applyParams} and {@link resetValues} methods processes registered objects that were registered by this method.
* @param targets object(s) to register as `target object`.
*/
registerFilterTarget(...targets) {
targets.forEach((target) => {
if (target === null || target === undefined) {
return;
}
this.appliedFiltersMap.set(target, null);
this.buildFilterTargetMap(target);
});
}
/**
* Removes passed object from `target objects` collection for current service instance.
*
* This means that {@link getRequestState}, {@link applyParams} and {@link resetValues} methods stops to process this objects.
* @param targets object(s) to remove from collection of `target object`.
*/
removeFilterTarget(...targets) {
targets.forEach((target) => {
this.appliedFiltersMap.delete(target);
});
}
/**
* Builds map of settings for passed `target` object.
*/
buildFilterTargetMap(target) {
let targetConfig = new Array();
RTFiltersService.filterPropertiesMap.forEach((typeConfig, type) => {
if (target instanceof type) {
targetConfig = targetConfig.concat(typeConfig);
for (const config of targetConfig) {
if (typeof config.defaultValue === 'undefined') {
config.defaultValue = cloneAsLiteral({
defaultValue: this.getPropertyValue(target, config)
}).defaultValue;
}
}
}
});
if (targetConfig.length > 0) {
this.appliedFiltersMap.set(target, targetConfig);
}
else {
this.appliedFiltersMap.delete(target);
}
}
getPropertyValue(target, config) {
if (target[config.propertyName] instanceof BehaviorSubject) {
return target[config.propertyName].getValue();
}
// duck type reactive form control
if (!!target[config.propertyName] && !!target[config.propertyName].setValue && !!target[config.propertyName].status) {
return target[config.propertyName].value;
}
return target[config.propertyName];
}
setPropertyValue(target, config, value) {
if (!!target[config.propertyName] && target[config.propertyName] instanceof Subject) {
target[config.propertyName].next(value);
return;
}
// duck type reactive form control
if (!!target[config.propertyName] && target[config.propertyName].setValue) {
target[config.propertyName].setValue(value);
return;
}
target[config.propertyName] = value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTFiltersService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTFiltersService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTFiltersService, decorators: [{
type: Injectable
}] });
class RegisterAsFilterDirective {
constructor(filtersService) {
this.filtersService = filtersService;
}
ngOnInit() {
this.filtersService.registerFilterTarget(this.filterTarget);
}
ngOnDestroy() {
this.filtersService.removeFilterTarget(this.filterTarget);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RegisterAsFilterDirective, deps: [{ token: RTFiltersService }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RegisterAsFilterDirective, selector: "[rtRegisterAsFilter]", inputs: { filterTarget: ["rtRegisterAsFilter", "filterTarget"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RegisterAsFilterDirective, decorators: [{
type: Directive,
args: [{
selector: '[rtRegisterAsFilter]'
}]
}], ctorParameters: function () { return [{ type: RTFiltersService }]; }, propDecorators: { filterTarget: [{
type: Input,
args: ['rtRegisterAsFilter']
}] } });
/**
* Object literal used by {@link getDefaultFilterConfig} function to build filter configuration.
* This object can be used to change default values of filter configs globally.
* By default it has next values:
* ```Javascript
* {
* coerce: true,
* defaultValue: undefined,
* emptyIsNull: false,
* ignoreOnAutoMap: false,
* omitIfNullOrUndefined: false,
* parameterName: <value of 'propertyName' parameter>,
* parseFormatter: undefined,
* propertyName: <value of 'propertyName' parameter>,
* serializeFormatter: undefined
* }
* ```
*/
const DefaultFilterConfig = {
coerce: true,
defaultValue: undefined,
emptyIsNull: false,
ignoreOnAutoMap: false,
omitIfNullOrUndefined: false,
parseFormatter: undefined,
serializeFormatter: undefined
};
/**
* Returns filter configuration based on {@link DefaultFilterConfig} values with applied `parameterName` and `propertyName` properties values.
* @param propertyName name of the property in `target type`, for which configuration is created. This value will be used to set {@link FilterConfig.propertyName} and {@link FilterConfig.parameterName} values.
* @see {@link FilterConfig}
*/
function getDefaultFilterConfig(propertyName) {
return {
parameterName: propertyName,
propertyName,
...cloneAsLiteral(DefaultFilterConfig)
};
}
/**
* Annotation that can be used to configure type property as filter to use with {@link FiltersService}
* @param targetOrNameOrConfig
* - if annotation is applied without any parameters then result of {@link getDefaultFilterConfig} function will be used. Value of {@link FilterConfig.parameterName} property will be equal to annotated property name.
* - if annotation is applied with string parameter then result of {@link getDefaultFilterConfig} function will be used. Value of {@link FilterConfig.parameterName} property will be equal to applied parameter value.
* - if annotation is applied with object as parameter then result of {@link getDefaultFilterConfig} will be used and all properties which were specified in passed object would be applied to resulting configuration via Object.assign.
* @param key specified by TypeScript automatically.
* @see {@link FilterConfig}
*/
function filter(targetOrNameOrConfig, key) {
const decorateWithConfig = (target, key2) => {
const config = getDefaultFilterConfig(key2);
if (typeof targetOrNameOrConfig === 'string') {
config.parameterName = targetOrNameOrConfig;
}
else {
Object.assign(config, targetOrNameOrConfig);
}
RTFiltersService.registerFilterConfig(target.constructor, config);
};
if (key) {
const targetTemp = targetOrNameOrConfig;
// tslint:disable-next-line:no-parameter-reassignment
targetOrNameOrConfig = null;
decorateWithConfig(targetTemp, key);
return;
}
return decorateWithConfig;
}
class RTFiltersModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTFiltersModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.12", ngImport: i0, type: RTFiltersModule, declarations: [RegisterAsFilterDirective], imports: [CommonModule], exports: [RegisterAsFilterDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTFiltersModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTFiltersModule, decorators: [{
type: NgModule,
args: [{
declarations: [RegisterAsFilterDirective],
exports: [RegisterAsFilterDirective],
imports: [CommonModule]
}]
}] });
/**
* Provides sorting functionality.
* @note This type is configured to use with {@link FiltersService}.
*/
class RTSortingsService {
constructor() {
/**
* Sortings that were selected by the user and must be applied on next request of data.
*
* @note This property is ready to use with {@link FiltersService} since it has {@link filter} annotation.
*/
this.sortings = new Array();
this.defaultSortingsInternal = new Array();
}
/**
* Default sortings that will be used by service.
*/
get defaultSortings() {
return this.defaultSortingsInternal;
}
/**
* If called when {@link sortings} is empty then applied value will be copied to {@link sortings} immediately.
*/
set defaultSortings(value) {
this.defaultSortingsInternal = value || [];
if (this.sortings.length === 0) {
this.sortings = this.cloneDefaultSortings();
}
}
/**
* Sets {@link sortings} according to specified parameters.
* @param fieldName name of the field by which sorting must be executed on server. This value will be used as {@link SortParameter.fieldName}.
*
* In case when sorting with the same field name is already specified, direction of this sorting will be toggled to reversed value and this sorting will be pushed to the end of {@link sortings} array.
* So it will be applied last.
* @param savePrevious `true` to keep previously applied sortings in {@link sortings} array.
*
* @param startDirectionStr - sort direction
*/
setSort(fieldName, savePrevious, startDirectionStr = 'Asc') {
const startDirection = SortDirection[startDirectionStr] !== undefined ? SortDirection[startDirectionStr] : SortDirection.Asc;
let newSort = { direction: startDirection, fieldName };
for (let i = 0; i < this.sortings.length; i++) {
if (this.sortings[i].fieldName === fieldName) {
const existedSort = this.sortings.splice(i, 1)[0];
newSort = {
direction: existedSort.direction,
fieldName: existedSort.fieldName
};
newSort.direction = newSort.direction === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc;
break;
}
}
if (savePrevious) {
this.sortings.push(newSort);
}
else {
this.sortings.length = 0;
this.sortings.push(newSort);
}
}
/**
* Removes sort with specified field name from {@link sortings} array.
* @param fieldName name of the sort to remove.
*/
removeSort(fieldName) {
for (let i = 0; i < this.sortings.length; i++) {
if (this.sortings[i].fieldName === fieldName) {
this.sortings.splice(i, 1);
}
}
}
/**
* Removes all sortings from {@link sortings} array.
*/
removeAllSortings() {
this.sortings.length = 0;
}
/**
* Performs service destroy.
*/
destroy() {
this.defaultSortingsInternal.length = 0;
this.sortings.length = 0;
}
/**
* Internal method for default sortings cloning.
* This method is used as {@link FilterConfig.defaultValue} as well as for copying to {@link sortings} when {@link defaultSortings} setter is used and {@link sortings} is empty.
*/
cloneDefaultSortings() {
return this.defaultSortingsInternal.map((s) => ({
direction: s.direction,
fieldName: s.fieldName
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTSortingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTSortingsService }); }
}
__decorate([
filter({
defaultValue() {
return this.cloneDefaultSortings();
},
parameterName: 'sortings',
parseFormatter(rawValue) {
return Array.isArray(rawValue)
? rawValue.map((sort) => ({
direction: sort.direction * 1,
fieldName: sort.fieldName
}))
: [];
},
serializeFormatter() {
return this.sortings.map((sort) => ({
direction: sort.direction,
fieldName: sort.fieldName
}));
}
})
], RTSortingsService.prototype, "sortings", void 0);
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTSortingsService, decorators: [{
type: Injectable
}], propDecorators: { sortings: [] } });
/**
* Implementation of {@link SubscriptionProxy} to work with any objects with `subscribe/unsubscribe` contracts. This contract is suitable for Observable, for example.
*/
class PushBasedSubscriptionProxy {
/**
* Returns `true` if this proxy type can subscribe to passed object. `false` otherwise.
*/
static isAcceptable(target) {
return !!target.subscribe;
}
/**
* @inheritdoc
*/
attach(target, completeAction, errorAction) {
return target.subscribe({ error: errorAction, next: completeAction });
}
/**
* @inheritdoc
*/
detach(subscription) {
subscription.unsubscribe();
}
}
/**
* Implementation of {@link SubscriptionProxy} which works with Promise and adds ability to unsubscribe from it.
*/
class PromiseSubscriptionProxy {
constructor() {
this.isAlive = true;
}
/**
* Returns `true` if this proxy type can subscribe to passed object. `false` otherwise.
*/
static isAcceptable(target) {
return target instanceof Promise;
}
/**
* @inheritdoc
*/
attach(target, completeAction, errorAction) {
return target.then((value) => {
if (this.isAlive) {
completeAction(value);
}
}, (error) => {
if (this.isAlive) {
errorAction(error);
}
});
}
/**
* @inheritdoc
*/
detach(subscription) {
this.isAlive = false;
}
}
/**
* Service to manage async subscriptions which acts as mediator to {@link SubscriptionProxy} contract implementations.
*/
class AsyncSubscriber {
constructor() {
this.proxy = null;
this.lastTarget = null;
this.subscription = null;
}
/**
* @see {@link SubscriptionProxy.attach}
*/
attach(target, completeAction, errorAction) {
if (this.lastTarget !== null) {
this.destroy();
}
this.lastTarget = target;
this.proxy = this.getProxy(target);
this.subscription = this.proxy.attach(target, completeAction, errorAction);
}
/**
* Detaches from underlying subscription and destroys all internal objects.
*/
destroy() {
if (this.proxy) {
this.proxy.detach(this.subscription);
}
this.proxy = null;
this.lastTarget = null;
this.subscription = null;
}
/**
* @see {@link SubscriptionProxy.detach}
*/
detach() {
if (this.proxy) {
this.proxy.detach(this.subscription);
}
}
getProxy(target) {
if (PromiseSubscriptionProxy.isAcceptable(target)) {
return new PromiseSubscriptionProxy();
}
if (PushBasedSubscriptionProxy.isAcceptable(target)) {
return new PushBasedSubscriptionProxy();
}
throw new Error('Passed object is not subscribable');
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AsyncSubscriber, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AsyncSubscriber }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AsyncSubscriber, decorators: [{
type: Injectable
}] });
/**
* Implements {@link Pager} contract and represents list without any paging mechanics.
* @note This type is configured to use with {@link FiltersService}.
*/
class NullObjectPager {
constructor() {
/**
* @inheritdoc
*/
this.appendedOnLoad = false;
/**
* @inheritdoc
*/
this.totalCount = 0;
/**
* @inheritdoc
*/
this.loadedCount = 0;
}
/**
* @inheritdoc
*/
processResponse(response) {
let alignedResponse;
if (Array.isArray(response)) {
alignedResponse = {
items: response,
loadedCount: response.length,
totalCount: response.length
};
}
else {
alignedResponse = response;
}
this.loadedCount = alignedResponse.loadedCount || (alignedResponse.items && alignedResponse.items.length ? alignedResponse.items.length : 0);
this.totalCount = alignedResponse.totalCount || 0;
}
/**
* @inheritdoc
*/
reset() {
this.totalCount = 0;
this.loadedCount = 0;
}
}
/**
* Base contract for state management services.
*/
class RTStateService {
}
const RTFilterTarget = new InjectionToken('RTFilterTarget');
class OperationStatusStream {
}
class RTList {
/**
* Global settings of list..
*
* These settings are static and their values are copied to the properties of the same name for each instance of {@link RTList} type.
*
* So, changing of this settings will affect all instances of {@link RTList} type that will be created after such changes.
* If you want to change settings of concrete object you can use it the same name properties.
*/
static { this.settings = {
/**
* @see {@link RTList.keepRecordsOnLoad}
*/
keepRecordsOnLoad: false
}; }
constructor(asyncSubscriber, stateServices, // | RTStateService[],
filterTargets, sortingsService, filtersService) {
this.asyncSubscriber = asyncSubscriber;
this.sortingsService = sortingsService;
this.filtersService = filtersService;
this.loadStarted = new EventEmitter();
this.loadSucceed = new EventEmitter();
this.loadFailed = new EventEmitter();
this.filterTargets = [];
/**
* Specifies that list must destroy previously loaded records immediately or keep them until data request is completed.
*/
this.keepRecordsOnLoad = RTList.settings.keepRecordsOnLoad;
/**
* Can be used with `Observable` data sourceto specify that list must keep previously loaded records or destroy them when new value published
*/
this.appendStreamedData = null;
/**
* Array of registered {@link RTStateService} instances.
*/
this.stateServices = new Array();
/**
* Async implementation of @see RTList.status
*/
this.status$ = new BehaviorSubject(OperationStatus.Initial);
/**
* Async implementation of @see RTList.destroyed
*/
this.destroyed$ = new BehaviorSubject(false);
/**
* Async implementation of @see RTList.inited
*/
this.inited$ = new BehaviorSubject(false);
/**
* Async implementation of @see RTList.items
*/
this.items$ = new BehaviorSubject([]);
/**
* Async implementation of @see RTList.busy
*/
this.busy$ = this.status$.pipe(map(status => status === OperationStatus.Progress));
/**
* Async implementation of @see RTList.ready
*/
this.ready$ = this.status$.pipe(map(status => status !== OperationStatus.Progress));
this.loadDataSuccessCallback = (response) => {
if (this.tryInterceptStatusResponse(response)) {
return response;
}
this.tryCleanItemsOnLoad(true);
return this.loadSuccessCallback(response);
};
this.reloadDataSuccessCallback = (response) => {
if (this.tryInterceptStatusResponse(response)) {
return response;
}
this.tryCleanItemsOnReload(true);
return this.loadSuccessCallback(response);
};
this.loadDataFailCallback = () => {
this.tryCleanItemsOnLoad(true);
this.loadFailCallback();
};
this.reloadDataFailCallback = () => {
this.tryCleanItemsOnReload(true);
this.loadFailCallback();
};
if (stateServices != null) {
if (Array.isArray(stateServices)) {
this.stateServices.push(...stateServices);
}
else {
this.stateServices.push(stateServices);
}
}
this.pager = new NullObjectPager();
if (filterTargets != null) {
if (Array.isArray(filterTargets)) {
this.filterTargets.push(...filterTargets);
}
else {
this.filterTargets.push(filterTargets);
}
}
}
/**
* Array of elements transferred in the {@link ListResponse.items} property.
*/
get items() {
return this.items$.getValue();
}
/**
* Configured {@link Pager} service.
*/
get pager() {
return this.pagerInternal;
}
set pager(value) {
this.filtersService.removeFilterTarget(this.pagerInternal);
this.pagerInternal = value;
this.filtersService.registerFilterTarget(this.pagerInternal);
}
/**
* True if the service was already destroyed via {@link destroy} call.
*/
get destroyed() {
return this.destroyed$.getValue();
}
/**
* True if the service was already initialized via {@link init} call.
*/
get inited() {
return this.inited$.getValue();
}
/**
* Current execution status of the list.
*/
get status() {
return this.status$.getValue();
}
/**
* returns `true`, if there is a data request executed at the moment (i.e. {@link state} is equal to {@link ProgressState.Progress})
*/
get busy() {
return this.status === OperationStatus.Progress;
}
/**
* returns `true`, if there is no data request executed at the moment (i.e. {@link state} is NOT equal to {@link ProgressState.Progress})
*/
get ready() {
return this.status !== OperationStatus.Progress;
}
/**
* Performs initialization logic of the service. This method must be called before first use of the service.
*/
init() {
if (this.inited) {
return;
}
this.filtersService.registerFilterTarget(...this.filterTargets);
this.filtersService.registerFilterTarget(this.pager, this.sortingsService);
const restoredState = {};
Object.assign(restoredState, ...this.stateServices.map((service) => service.getState() || {}));
this.filtersService.applyParams(restoredState);
this.inited$.next(true);
}
/**
* Performs destroy logic of the list itself and all of the inner services.
*/
destroy() {
this.asyncSubscriber.destroy();
this.filtersService.destroy();
this.sortingsService.destroy();
this.clearData();
this.destroyed$.next(true);
}
/**
* Registers passed object(s) as state service to manage the list state.
*/
registerStateService(...services) {
services.forEach((service) => {
this.stateServices.push(service);
});
}
/**
* Removes passed object(s) from state services collection of the list.
*/
removeStateService(...services) {
services.forEach((service) => {
const index = this.stateServices.findIndex((s) => s === service);
if (index !== -1) {
this.stateServices.splice(index, 1);
}
});
}
/**
* Registers passed object(s) as filter targets in underlying {@link FiltersService} to include their configured properties as parameters to the data request.
* @see {@link FiltersService.registerFilterTarget}
*/
registerFilterTarget(...targets) {
this.filtersService.registerFilterTarget(...targets);
}
/**
* @see {@link FiltersService.removeFilterTarget}
*/
removeFilterTarget(...targets) {
this.filtersService.removeFilterTarget(...targets);
}
/**
* @see {@link FiltersService.getRequestState}
*/
getRequestState(filterFn) {
return this.filtersService.getRequestState(filterFn);
}
/**
* Resets the list parameters (sortings, paging, filters) to their default values.
*/
resetSettings() {
this.asyncSubscriber.detach();
this.filtersService.resetValues();
this.pager.reset();
this.clearData();
this.status$.next(OperationStatus.Initial);
}
/**
* Cancels the request executed at the moment.
*/
cancelRequests() {
if (this.busy || this.appendStreamedData !== null) {
this.asyncSubscriber.detach();
this.status$.next(OperationStatus.Cancelled);
this.clearData();
}
}
/**
* Clears {@link items} array. Calls {@link destroyAll} method for {@link items} array to perform optional destroy logic of the elements.
* {@see destroyAll}
*/
clearData() {
destroyAll(this.items);
this.items$.next([]);
}
/**
* Performs data loading by calling specified {@link fetchMethod} delegate.
* @return result of {@link fetchMethod} execution.
*/
loadData() {
if (this.busy) {
return null;
}
this.loadStarted.emit();
const subscribable = this.beginRequest();
this.tryCleanItemsOnLoad(false);
this.asyncSubscriber.attach(subscribable, this.loadDataSuccessCallback, this.loadDataFailCallback);
this.stateServices.forEach((service) => {
service.persistState(this.filtersService);
});
return subscribable;
}
/**
* Resets paging parameters and performs data loading by calling {@link loadData} if list not in {@link OperationStatus.Progress} state.
* @return result of {@link fetchMethod} if it was called. `null` otherwise.
*/
reloadData() {
if (this.busy) {
return null;
}
this.loadStarted.emit();
this.pager.reset();
const subscribable = this.beginRequest();
this.tryCleanItemsOnReload(false);
this.asyncSubscriber.attach(subscribable, this.reloadDataSuccessCallback, this.reloadDataFailCallback);
this.stateServices.forEach((service) => {
service.persistState(this.filtersService);
});
return subscribable;
}
/**
* Callback which is executed if {@link fetchMethod} execution finished successfully.
*/
loadSuccessCallback(response) {
const items = Array.isArray(response) ? response : response.items;
this.items$.next(this.items.concat(items));
this.pager.processResponse(response);
// In case when filter changed from last request and there's no data now
// Don't do this for flat responses since we don't know real count items
if (!Array.isArray(response) && this.pager.totalCount === 0) {
this.clearData();
this.pager.reset();
}
this.status$.next(this.pager.totalCount === 0 && this.items.length === 0 ? OperationStatus.NoData : OperationStatus.Done);
this.loadSucceed.emit(response);
return response;
}
/**
* Callback which is executed if {@link fetchMethod} execution finished with error.
*/
loadFailCallback() {
this.tryCleanItemsOnLoad(true);
this.status$.next(OperationStatus.Fail);
this.loadFailed.emit();
}
tryInterceptStatusResponse(response) {
if (this.responseHasStatus(response)) {
switch (response.status) {
case OperationStatus.Fail:
this.reloadDataFailCallback();
return true;
case OperationStatus.Cancelled:
this.cancelRequests();
return true;
case OperationStatus.Progress:
return true;
}
}
return false;
}
tryCleanItemsOnLoad(requestCompleted) {
if (this.keepRecordsOnLoad === requestCompleted && this.pager.appendedOnLoad === false) {
this.clearData();
}
if (requestCompleted && this.appendStreamedData === false) {
this.clearData();
}
}
tryCleanItemsOnReload(requestCompleted) {
if (this.keepRecordsOnLoad === requestCompleted) {
this.clearData();
}
if (requestCompleted && this.appendStreamedData === false) {
this.clearData();
}
}
beginRequest() {
this.status$.next(OperationStatus.Progress);
const requestState = this.filtersService.getRequestState();
return this.fetchMethod(requestState);
}
responseHasStatus(response) {
return response.status !== null && typeof response.status !== 'undefined';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTList, deps: [{ token: AsyncSubscriber }, { token: RTStateService, optional: true }, { token: RTFilterTarget, optional: true, skipSelf: true }, { token: RTSortingsService }, { token: RTFiltersService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTList }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RTList, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: AsyncSubscriber }, { type: RTStateService, decorators: [{
type: Optional
}] }, { type: undefined, decorators: [{
type: SkipSelf
}, {
type: Optional
}, {
type: Inject,
args: [RTFilterTarget]
}] }, { type: RTSortingsService }, { type: RTFiltersService }]; } });
const LIST_PROVIDERS = [AsyncSubscriber, RTList, RTFiltersService, RTSortingsService, { provide: OperationStatusStream, useExisting: RTList }];
class ListDirective {
set fetchMethod(value) {
this.listService.fetchMethod = value;
}
constructor(listService, sortingsService) {
this.listService = listService;
this.sortingsService = sortingsService;
this.listInit = new EventEmitter(false);
this.afterListInit = new EventEmitter(false);
this.loadSucceed = new EventEmitter();
this.loadFailed = new EventEmitter();
this.loadStarted = new EventEmitter();
this.loadOnInit = true;
this.keepRecordsOnLoad = false;
this.appendStreamedData = null;
this.successSubscription = listService.loadSucceed.subscribe((response) => {
this.loadSucceed.emit(response);
});
this.failSubscription = listService.loadFailed.subscribe(() => {
this.loadFailed.emit();
});
this.loadStartedSubscription = listService.loadStarted.subscribe(() => {
this.loadStarted.emit();
});
this.items$ = this.listService.items$;
this.busy$ = this.listService.busy$;
this.ready$ = this.listService.ready$;
}
ngAfterViewInit() {
// We call init in ngAfterViewInit to:
// 1. allow all child controls to be applied to markup and regiter themself in filtersService
// 2. give ability to all child controls to apply their default values
// 3. overwrite theese default values by values passed via persistence services
// 4. execute all ngAfterViewInit for custom services registration (setTimeout)
setTimeout(() => {
this.listInit.emit(this.listService);
this.listService.init();
this.afterListInit.emit(this.listService);
if (this.loadOnInit) {
this.listService.loadData();
}
}, 0);
}
ngOnDestroy() {
this.listService.destroy();
this.successSubscription.unsubscribe();
this.failSubscription.unsubscribe();
this.loadStartedSubscription.unsubscribe();
}
ngOnChanges(changes) {
if (changes.defaultSortings) {
this.sortingsService.defaultSortings = changes.defaultSortings.currentValue;
}
if (changes.keepRecordsOnLoad) {
this.listService.keepRecordsOnLoad = changes.keepRecordsOnLoad.currentValue;
}
if (changes.appendStreamedData) {
this.listService.appendStreamedData = changes.appendStreamedData.currentValue;
}
}
reloadData() {
return this.listService.reloadData();
}
loadData() {
return this.listService.loadData();
}
resetSettings() {
this.listService.resetSettings();
}
cancelRequests() {
this.listService.cancelRequests();
}
get items() {
return this.listService.items;
}
get busy() {
return this.listService.busy;
}
get ready() {
return this.listService.ready;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ListDirective, deps: [{ token: RTList, self: true }, { token: RTSortingsService, self: true }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ListDirective, selector: "[rtList]", inputs: { defaultSortings: "defaultSortings", loadOnInit: "loadOnInit", keepRecordsOnLoad: "keepRecordsOnLoad", appendStreamedData: "appendStreamedData", fetchMethod: ["rtList", "fetchMethod"] }, outputs: { listInit: "listInit", afterListInit: "afterL