UNPKG

@terminus/ngx-tools

Version:

[![CircleCI][circle-badge]][circle-link] [![codecov][codecov-badge]][codecov-project] [![semantic-release][semantic-release-badge]][semantic-release] [![MIT License][license-image]][license-url] <br> [![NPM version][npm-version-image]][npm-url] [![Github

869 lines (839 loc) 27.5 kB
import { isNull, isAbstractControl, isString, isBoolean, isArray, isUndefined, isNumber } from '@terminus/ngx-tools/type-guards'; import { FormGroup } from '@angular/forms'; import { coerceNumberProperty, coerceDateProperty } from '@terminus/ngx-tools/coercion'; import { zip, range, throwError, timer, ReplaySubject } from 'rxjs'; import { async } from 'rxjs/internal/scheduler/async'; import { retryWhen, mergeMap, take, takeUntil } from 'rxjs/operators'; /** * Helper function to abbreviate a number * * @param input - The number to be abbreviated. * @param decimalPlace - The decimals users define for final abbreviated number. Default to 1. * @returns The abbreviated number * * @example * abbreviateNumber(1234, '1') // Returns: '1.2K' */ function abbreviateNumber(input, decimalPlace = 1) { const SCALE_NUMBER = 3; const MATH_POWER = 10; if (!input) { return ''; } const baseNumberAndExponent = input .toExponential() .split('e+') .map(el => +el); if (baseNumberAndExponent[1] < SCALE_NUMBER) { return input.toString(); } const scaleLevel = baseNumberAndExponent[1] % SCALE_NUMBER; baseNumberAndExponent[0] = baseNumberAndExponent[0] * Math.pow(MATH_POWER, scaleLevel); const calculatedScale = [ '', 'K', 'M', 'B', 'T', ][(baseNumberAndExponent[1] - scaleLevel) / SCALE_NUMBER]; const newNumber = baseNumberAndExponent[0].toFixed(decimalPlace).toString(); return newNumber + calculatedScale; } /** * A helper function to apply TypeScript mixins * * https://www.typescriptlang.org/docs/handbook/mixins.html * * @param derivedCtor - The mixin target class * @param baseCtors - An array of classes to combine into the target class * @returns The mixed class * * @example * applyMixins(SmartObject, [Disposable, Activatable]); */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function applyMixins(derivedCtor, baseCtors) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { derivedCtor.prototype[name] = baseCtor.prototype[name]; }); }); } /** * Determine if an object exists in an array * * @param object - The object to look for in the array * @param array - The array to search through * @param comparator - The function to determine what object values to compare * @returns True if a match was found * * @example * const arr = [{id: 1}, {id: 2}, {id: 3}]; * const comparator = (v) => v.id; * arrayContainsObject({id: 2}, array, comparator); // Returns: true */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function arrayContainsObject(object, array, comparator) { let hasDuplicate = false; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < array.length; i++) { if (comparator(array[i]) === comparator(object)) { hasDuplicate = true; break; } } return hasDuplicate; } /** * Create an array with `null` & `undefined` values removed * * @param arr - The array to compact * @returns The compacted array * * @example * compactArray(['hi', null, 2, true, undefined]) // Returns: ['hi', 2, true] */ function compactArray(arr) { if (!arr || arr.length < 1) { return undefined; } const valuesToReturn = []; arr.map(i => { if (i === null || i === undefined || '') { return; } valuesToReturn.push(i); }); return valuesToReturn; } /** * Return a function, that, as long as it continues to be invoked, will not be triggered. The * function will be called after it stops being called for N milliseconds. * * @param func - The function to call after the debounce period * @param wait - The length of time to wait between calls (ms) * @param immediate - Whether the debounced function should be fired immediately * @param windowRef - A reference to the global window object * @returns The debounced function * * @example * const myFunc = () => {console.log('hi!')}; * const myDebouncedFunc = debounce(myFunc, 200); * myDebouncedFunc(); * myDebouncedFunc(); * myDebouncedFunc(); * // 'hi!' will only be logged once */ function debounce(func, wait, immediate = false, windowRef = window) { let timeout = null; return function () { const context = this; const args = arguments; const later = function () { timeout = null; if (!immediate) { func.apply(context, args); } }; const callNow = immediate && !timeout; if (timeout) { clearTimeout(timeout); } timeout = windowRef.setTimeout(later, wait); if (callNow) { func.apply(context, args); } }; } /** * Define the typeCache which will hold all action types for the entire application */ let typeCache = {}; /** * Ensure you only define an action once in the entirety of the application * * @param label - The action label * @returns uniqueLabel - The unique label * * @example * defineType('[log-in] User log in') as '[log-in] User log in'; */ function defineType(label) { // Verify the label does not already exist in the cache if (typeCache[label]) { throw new Error(`Action type '${label}' is not unique!`); } // Save the label to the cache typeCache[label] = true; return label; } /** * Ensure action is defined only once in the entirety of the application * * @param typeEnum * * @example * export enum actionTypes { * AssignState = '[mock-meta-reducer] Assign State', * }; * defineTypeEnum(actionTypes); */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function defineTypeEnum(typeEnum) { for (const val in typeEnum) { // istanbul ignore else if (typeEnum.hasOwnProperty(val)) { defineType(typeEnum[val]); } } } /** * Reset the type cache * NOTE: FOR TESTS ONLY */ function resetTypeCache() { typeCache = {}; } /* eslint-disable @typescript-eslint/no-magic-numbers */ /** * Generate a canonically formatted UUID that is Version 1 through 5 and is the appropriate Variant as per RFC4122. * * @returns The UUID * * @example * generateUUID() // Returns a UUID such as: `f4ee5eed-ed19-3681-713e-907a23ed7858` */ function generateUUID() { const buf = new Uint16Array(8); window.crypto.getRandomValues(buf); const S4 = function (num) { let ret = num.toString(16); while (ret.length < 4) { ret = `0${ret}`; } return ret; }; return (`${S4(buf[0]) + S4(buf[1])}-${S4(buf[2])}-${S4(buf[3])}-${S4(buf[4])}-${S4(buf[5])}${S4(buf[6])}${S4(buf[7])}`); } /** * Return the value of a FormControl within a FormGroup * * @param form - The FormGroup that contains the control * @param controlName - The name of the control * @returns The value * * @example * getFormControlValue(myFormGroup, 'myControl'); * getFormControlValue<boolean>(myFormGroup, 'myControl'); */ function getFormControlValue(form, controlName) { if (!form || !controlName) { return undefined; } const control = form.get(controlName); return !isNull(control) && isAbstractControl(control) ? control.value : undefined; } /** * Return an object containing arrays organized by property * * @param array - The array to split * @param key - The object property to split by * @returns An object containing arrays separated by property value * * @example * interface MyObj { * a: string; * b: number; * } * const myArray: MyObj[] = [{a: 'foo', b: 1}, {a: 'bar', b: 6}, {a: 'foo', b: 6}]; * groupBy<MyObj, keyof MyObj>(myArray, 'a'); * Returns: * { * foo: [{a: 'foo', b: 1}, {a: 'foo', b: 6}], * bar: [{a: 'bar', b: 6}], * } */ function groupBy(array, key) { const initialValue = {}; return array.reduce((accumulator, x) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const idx = x[key]; // Create an array for the property if one does not exist if (!accumulator[idx]) { accumulator[idx] = []; } // Add the item to the property array accumulator[idx].push(x); return accumulator; }, initialValue); } /** * Determine if a form control has a required validator * * @param formItem - The control or form group to check * @returns If a required control is found * * @example * const ctrl = new FormControl(null, [Validators.required]; * const group = new FormGroup({myControl: [null, [Validators.required]]}); * hasRequiredControl(ctrl); // Returns: true * hasRequiredControl(group); // Returns: true */ function hasRequiredControl(formItem) { if (!formItem) { return false; } // Dealing with FormGroup if (formItem instanceof FormGroup) { let isRequired = false; // Check each control within the group // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < Object.keys(formItem.controls).length; i += 1) { const control = formItem.controls[Object.keys(formItem.controls)[i]]; isRequired = controlHasRequiredField(control); // Break out of the loop when we find the first required control if (isRequired) { break; } } return isRequired; } // Dealing with AbstractControl return controlHasRequiredField(formItem); } /** * Determine if a form control has a required validator * * @param control - The control to test * @returns If the control is required * * @example * const ctrl = new FormControl(null, [Validators.required]; * controlHasRequiredField(ctrl); // Returns: true */ function controlHasRequiredField(control) { const validator = control.validator ? control.validator({}) : null; return !!(validator && validator.required); } const DEFAULT_JITTER_FACTOR = .3; const DEFAULT_BACK_OFF_TIME = 2; const DEFAULT_BASE_WAIT_TIME = 100; /** * Calculate retry timing * * `jitter`: "Slight irregular movement, variation, or unsteadiness, * especially in an electrical signal or electronic device" * * @param options - The options object * - `jitter`: If the duration should be affected by a jitter effect * - `jitterFactor`: How widely the jitter effect should vary * - `backOffFactor`: How quickly the duration should back off * - `baseWaitTime`: The base time when determining sleep duration * @returns The duration to sleep * * @example * const calcOpts: DelayCalculator = { * jitter: true, * jitterFactor: .3, * backOffFactor: 2, * baseWaitTime: 100, * } * // Create a retrier with a custom backoff * retryWithBackoff({retries: 3, delayCalculator: exponentialBackoffDelayCalculator(calcOpts)}) */ const exponentialBackoffDelayCalculator = ({ jitter = true, jitterFactor = DEFAULT_JITTER_FACTOR, backOffFactor = DEFAULT_BACK_OFF_TIME, baseWaitTime = DEFAULT_BASE_WAIT_TIME, }) => function (attempt) { let sleepDuration = baseWaitTime * Math.pow(backOffFactor, attempt); if (jitter) { sleepDuration *= (1 - (jitterFactor * Math.random())); } return sleepDuration; }; const DEFAULT_RETRY_COUNT = 2; const ERROR_CODE_TOO_MANY_REQUESTS = 429; const ERROR_CODE_500_MIN = 500; const ERROR_CODE_500_MAX = 599; const httpRetryer = ({ retries = DEFAULT_RETRY_COUNT, delayCalculator = exponentialBackoffDelayCalculator({}), scheduler = async, }) => retryWhen((errors) => zip(errors, range(1, retries + 1)).pipe(mergeMap(([err, retry]) => { if (retry > retries || !isConsideredError(err)) { return throwError(err); } let waitTime = delayCalculator(retry); if (err.status === ERROR_CODE_TOO_MANY_REQUESTS) { const headerWaitTime = extractRetryAfterTime(err); waitTime = headerWaitTime || waitTime; } return timer(waitTime, scheduler).pipe(take(1)); }))); /** * @param err */ function extractRetryAfterTime(err) { const retryHeaderValue = err.headers.get('Retry-After'); if (retryHeaderValue) { return coerceNumberProperty(retryHeaderValue, null) || coerceDateProperty(retryHeaderValue, null); } return null; } /** * @param err */ function isConsideredError(err) { if (err.hasOwnProperty('status') && err.hasOwnProperty('headers')) { const e = err; return e.status === 0 || e.status === ERROR_CODE_TOO_MANY_REQUESTS || (e.status >= ERROR_CODE_500_MIN && e.status <= ERROR_CODE_500_MAX); } return false; } /** * Helper function to parse an object with deep keys * * @param object - An object with key as string or string * @param keys - A string or array of strings * @returns String value at the lowest layer or object itself * * @example * objectDeepParse(myObject, ['foo', 'bar']) // Returns: myObject.foo.bar if found * objectDeepParse(myObject, 'foo.bar') // Returns: myObject.foo.bar if found */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function objectDeepParse(object, keys) { if (isString(object) || isBoolean(object) || !object) { return object; } keys = isArray(keys) ? keys : keys.split('.'); object = object[keys[0]]; if (object && keys.length > 1) { return objectDeepParse(object, keys.slice(1)); } return object; } /** * Utility class to assist with pulling specific values from a `SimpleChanges` object. */ class NgChangeObjectValueParser { /** * Function to parse previousValue from triggered by changes on ngOnChange * * @param changes - SimpleChanges * @param path - string * @returns lowest layer value or changes object itself when path cannot be parsed * * @example * valueParser.getOldValue(myChanges, 'my.path') */ // eslint-disable-next-line @typescript-eslint/no-explicit-any static getOldValue(changes, path) { const [keys, key] = this.parsePath(path); return (key && changes[key]) ? objectDeepParse(changes[key].previousValue, keys) : undefined; } /** * Function to parse currentValue from triggered by changes on ngOnChange * * @param changes - SimpleChanges * @param path - string * @returns lowest layer value or changes object itself when path cannot be parsed * * @example * valueParser.getNewValue(myChanges, 'my.path') */ // eslint-disable-next-line @typescript-eslint/no-explicit-any static getNewValue(changes, path) { const [keys, key] = this.parsePath(path); return (key && changes[key]) ? objectDeepParse(changes[key].currentValue, keys) : undefined; } /** * Function to parse path to get keys for each layer * * @param path - string * @returns an array of two elements, one being an array of all the keys except first one, one being the first key * * @example * valueParser.parsePath('my.path') // Returns: [['my'], 'path'] */ static parsePath(path) { const keys = path.split('.'); let key = keys.shift(); if (!key) { key = keys[0]; } return [keys, key]; } } /** * Helper function to determine if a specific value has changed * * @param changes - The object of changes * @param path - The object path in question * @returns True if the value has changed * * @example * inputHasChanged(changesObject, 'myInputName') */ function inputHasChanged(changes, path) { if (!changes || !path) { return undefined; } const oldValue = NgChangeObjectValueParser.getOldValue(changes, path); const newValue = NgChangeObjectValueParser.getNewValue(changes, path); return oldValue !== newValue; } /** * Helper function to determine if input is unset. * * @param x - the input being tested * @returns boolean * * @example * isUnset(null); // Returns: true * isUnset(undefined); // Returns: true * isUnset('hello'); // Returns: false */ // eslint-disable-next-line @typescript-eslint/no-explicit-any const isUnset = (x) => isNull(x) || isUndefined(x); /** * Placeholder function. * * @returns Undefined */ const noop = () => void 0; /** * Helper function to get an object with deep keys * * @param object - The object to query. * @param path - The string path of the property to get. * @param defaultValue - (optional) - The value returned for undefined resolved values. * @returns The updated object * * @example * const myObj = {foo: {bar: 'baz', bing: 'bang'}}; * objectDeepGet(myObj, 'foo.bar') // Returns: 'baz' * objectDeepGet(myObj, 'does.not.exist', 'hi') // Returns: 'hi' */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function objectDeepGet(object, path, defaultValue) { if (!object) { return defaultValue; } const keys = path.split('.'); object = object[keys[0]]; if (object && keys.length > 1) { return objectDeepGet(object, keys.slice(1).join('.'), defaultValue); } if (object === undefined && defaultValue) { return defaultValue; } return object; } /** * Helper function to get an object with deep keys * * @param obj - The object to modify. * @param keys - The path of the property to set. * @param value - The value to set. * @returns The updated object * * @example * const myObj: MyObjType = { * foo: { * bar: { * baz: true, * }, * }, * }; * const updatedObject = objectDeepSet(myObj, 'foo.bar.baz', false); * const updatedObject = objectDeepSet<boolean, MyObjType>(myObj, 'foo.bar.baz', false); */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function objectDeepSet(obj, keys, value) { const paths = keys.split('.'); if (paths.length === 1) { const path = paths[0]; return (Object.assign(Object.assign({}, obj), { [path]: value })); } const [path, ...remainingPaths] = paths; const nestedObj = obj[path]; const newNestedObj = objectDeepSet(nestedObj, remainingPaths.join('.'), value); return (Object.assign(Object.assign({}, obj), { [path]: newNestedObj })); } const DEFAULT_RETRY_COUNT$1 = 3; /** * Return the difference in time in words * * @param options - The options object * - `retries`: How many times it should retry before throwing an error * - `delayCalculator`: The calculator to determine the delay timing * @returns The observable timer * * @example * return this.exampleDatabase.getSomething() * .pipe( * map((res: MyResponse) => { * if (res) { * return res; * } else { * return null; * } * }), * retryWithBackoff({}), // Using default options * ) * ; */ const retryWithBackoff = ({ retries = DEFAULT_RETRY_COUNT$1, delayCalculator = exponentialBackoffDelayCalculator({}), }) => retryWhen(errors => zip(errors, range(1, retries)) .pipe(mergeMap(([err, retry]) => { if (retry >= retries) { return throwError(err); } return timer(delayCalculator(retry)); }))); /** * Helper function to return an array of values from an hash object * * @param keys - The array containing the key values (number|string) to retrieve from the hash * @param hash - The object to pull values from * @returns The array of values that match keys * * @example * const tactic1: MyType = { * id: 1, * name: 'tactic1', * goal: 'goal1', * } * const tactic2: MyType = { * id: 2, * name: 'tactic2', * goal: 'goal2', * } * const tactics = { 1: tactic1, 2: tactic2 } * returnValuesByKeys<MyType>([1], tactics) // Returns: `[tactic1]` */ function returnValuesByKeys(keys, hash) { const stringyKeys = keys.map((id) => id.toString()); const values = []; for (const key of stringyKeys) { // istanbul ignore else if (hash[key]) { values.push(hash[key]); } } return values; } /** * Round a number * * Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round#A_better_solution * * @param num - The number to round (also accepting strings for exponential support) * @param precision - How precise to make the rounding * @returns The rounded number * * @example * roundNumber(1.050, 1) // Returns: `1.1` * roundNumber(3456.3456, 1) // Returns: `3456.3` * roundNumber(3456.3456, -2) // Returns: `3500` * roundNumber('1.23e+5', -4) // Returns: `120000` */ function roundNumber(num, precision = 0) { if (!isNumber(num)) { return undefined; } const shift = function (innerNum, innerPrecision) { const numArray = innerNum.toString().split('e'); return +(`${numArray[0]}e${numArray[1] ? (+numArray[1] + innerPrecision) : innerPrecision}`); }; return shift(Math.round(shift(num, +precision)), -precision); } /** * Set the value of a FormControl * * @param form - The FormGroup * @param controlName - The name of the control * @param controlValue - The value to set the control to * * @example * setFormControlValue<number>(myForm, 'budget', 50); */ function setFormControlValue(form, controlName, controlValue) { if (!form || !controlName) { return; } const control = form.get(controlName); if (control) { control.setValue(controlValue); } } /* * 'Inspired' by https://github.com/sindresorhus/camelcase */ /** * Function that preserves camel case * * @param input - The string input * @returns The adjusted string */ function preserveCamelCase(input) { let isLastCharLower = false; let isLastCharUpper = false; let isLastLastCharUpper = false; for (let i = 0; i < input.length; i++) { const c = input[i]; if (isLastCharLower && /[a-zA-Z]/.test(c) && c.toUpperCase() === c) { input = `${input.slice(0, i)}-${input.slice(i)}`; isLastCharLower = false; isLastLastCharUpper = isLastCharUpper; isLastCharUpper = true; i++; } else if (isLastCharUpper && isLastLastCharUpper && /[a-zA-Z]/.test(c) && c.toLowerCase() === c) { input = `${input.slice(0, i - 1)}-${input.slice(i - 1)}`; isLastLastCharUpper = isLastCharUpper; isLastCharUpper = false; isLastCharLower = true; } else { isLastCharLower = c.toLowerCase() === c; isLastLastCharUpper = isLastCharUpper; isLastCharUpper = c.toUpperCase() === c; } } return input; } /** * Post process a conversion into PascalCase if necessary * * @param x - The string * @param pascalCase - A boolean representing if the string should be converted to PascalCase * @returns The final string */ const postProcess = (x, pascalCase) => (pascalCase ? x.charAt(0).toUpperCase() + x.slice(1) : x); const ɵ0 = postProcess; /** * Convert a string to camelCase * * @param input - The string to convert * @param pascalCase - A boolean representing if the string should be converted to PascalCase * @returns The camelCase version of the string * * @example * toCamelCase('MY_TEXT') // Returns: `myText` * toCamelCase('MY_TEXT', true) // Returns: `MyText` */ function toCamelCase(input, pascalCase = false) { if (!input) { return undefined; } // Trim whitespace input = input.trim(); // Test for a single character if (input.length === 1) { return pascalCase ? input.toUpperCase() : input.toLowerCase(); } // Test if we are dealing with a single lowercased word if (/^[a-z\d]+$/.test(input)) { return postProcess(input, pascalCase); } // Test if there are any uppercase if (input !== input.toLowerCase()) { input = preserveCamelCase(input); } input = input .replace(/^[_.\- ]+/, '') .toLowerCase() .replace(/[_.\- ]+(\w|$)/g, (m, p1) => p1.toUpperCase()); return postProcess(input, pascalCase); } /** * Patch the component with unsubscribe behavior * * @param component - The component class (`this` context) * @returns An observable representing the unsubscribe event */ function componentDestroyed(component) { if (component.componentDestroyed$) { return component.componentDestroyed$; } // eslint-disable-next-line @angular-eslint/no-lifecycle-call const oldNgOnDestroy = component.ngOnDestroy; const stop$ = new ReplaySubject(); // eslint-disable-next-line @angular-eslint/no-lifecycle-call component.ngOnDestroy = () => { // istanbul ignore else if (oldNgOnDestroy) { oldNgOnDestroy.apply(component); } stop$.next(true); stop$.complete(); }; component.componentDestroyed$ = stop$.asObservable(); return component.componentDestroyed$; } /** * A pipe-able operator to unsubscribe during OnDestroy lifecycle event * * @param component - The component class (`this` context) * @returns The component wrapped in an Observable * * @example * source.pipe(untilComponentDestroyed(this)).subscribe... */ const untilComponentDestroyed = // eslint-disable-next-line max-len (component) => (source) => source.pipe(takeUntil(componentDestroyed(component))); /** * Helper function to determine if a specific value has changed * * @param changes - The object of changes * @param path - A string with keys defined, separate with '.' * @param control - The formControl * @returns True if the value has changed * * @example * ... * AngularInput: * public myInput; * AngularInput: * public myFormControl; * * ngOnChanges(changes: SimpleChanges) { * // This will update the form control's value whenever the `@Input` value changes: * updateControlOnInputChanges(changes, 'myInput', this.myFormControl)); * } * ... */ function updateControlOnInputChanges(changes, path, control) { if (!changes || !path || !control) { return false; } if (inputHasChanged(changes, path)) { const newValue = NgChangeObjectValueParser.getNewValue(changes, path); control.setValue(newValue); return true; } return false; } /** * Create version information from a version string * * @example * VERSION.full // Returns: 1.2.3 * VERSION.major // Returns: 1 * VERSION.minor // Returns: 2 * VERSION.patch // Returns: 3 */ class Version { constructor(full) { this.full = full; const parts = full.split('.'); const itemsToRemoveForPatch = 2; this.major = parts[0]; this.minor = parts[1]; this.patch = parts.slice(itemsToRemoveForPatch).join('.'); } } const VERSION = new Version('8.0.6'); /** * Generated bundle index. Do not edit. */ export { NgChangeObjectValueParser, VERSION, Version, abbreviateNumber, applyMixins, arrayContainsObject, compactArray, componentDestroyed, debounce, defineType, defineTypeEnum, exponentialBackoffDelayCalculator, generateUUID, getFormControlValue, groupBy, hasRequiredControl, httpRetryer, inputHasChanged, isUnset, noop, objectDeepGet, objectDeepParse, objectDeepSet, resetTypeCache, retryWithBackoff, returnValuesByKeys, roundNumber, setFormControlValue, toCamelCase, untilComponentDestroyed, updateControlOnInputChanges, ɵ0 }; //# sourceMappingURL=terminus-ngx-tools-utilities.js.map