UNPKG

@leancodepl/utils

Version:

Common utility functions and React hooks for web applications

407 lines (389 loc) 13 kB
import invariant from 'tiny-invariant'; import { useState, useCallback, useMemo } from 'react'; /** * Adds a prefix to all keys in an object, creating a new object with prefixed keys. * * @template T - The type of the input object * @template TPrefix - The type of the prefix string * @param object - The object whose keys will be prefixed * @param prefix - The prefix string to add to each key * @returns A new object with all keys prefixed * @example * ```typescript * const apiData = { userId: 1, userName: 'John' }; * const prefixed = addPrefix(apiData, 'api_'); * // Result: { api_userId: 1, api_userName: 'John' } * ``` */ function addPrefix(object, prefix) { return Object.fromEntries(Object.entries(object).map(([key, value])=>[ `${prefix}${key}`, value ])); } function transformFirst(value, transformFn) { if (value.length === 0) { return ""; } return transformFn(value[0]) + value.slice(1); } /** * Converts the first character of a string to lowercase. * * @param value - The string to transform * @returns The string with the first character in lowercase * @example * ```typescript * const result = toLowerFirst('UserName'); * // Result: 'userName' * ``` */ function toLowerFirst(value) { return transformFirst(value, (value)=>value.toLowerCase()); } /** * Converts the first character of a string to uppercase. * * @param value - The string to transform * @returns The string with the first character in uppercase * @example * ```typescript * const result = toUpperFirst('userName'); * // Result: 'UserName' * ``` */ function toUpperFirst(value) { return transformFirst(value, (value)=>value.toUpperCase()); } function _extends() { _extends = Object.assign || function assign(target) { for(var i = 1; i < arguments.length; i++){ var source = arguments[i]; for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key]; } return target; }; return _extends.apply(this, arguments); } function transformDeep(value, mode) { if (value === null || value === undefined) { return undefined; } if (Array.isArray(value)) { return value.map((val)=>transformDeep(val, mode)); } if (typeof value === "object") { const transformKey = mode === "capitalize" ? toUpperFirst : toLowerFirst; return Object.entries(value).reduce((accumulator, [key, value])=>_extends({}, accumulator, { [transformKey(key)]: transformDeep(value, mode) }), {}); } return value; } /** * Recursively transforms all object keys to use uncapitalized (camelCase) format. * * @template T - The type of the input value * @param value - The value to transform (can be object, array, or primitive) * @returns A new object with all keys converted to camelCase * @example * ```typescript * const serverData = { UserId: 1, UserName: 'John', Profile: { FirstName: 'John' } }; * const clientData = uncapitalizeDeep(serverData); * // Result: { userId: 1, userName: 'John', profile: { firstName: 'John' } } * ``` */ function uncapitalizeDeep(value) { return transformDeep(value, "uncapitalize"); } /** * Recursively transforms all object keys to use capitalized (PascalCase) format. * * @template T - The type of the input value * @param value - The value to transform (can be object, array, or primitive) * @returns A new object with all keys converted to PascalCase * @example * ```typescript * const clientData = { userId: 1, userName: 'John', profile: { firstName: 'John' } }; * const serverData = capitalizeDeep(clientData); * // Result: { UserId: 1, UserName: 'John', Profile: { FirstName: 'John' } } * ``` */ function capitalizeDeep(value) { return transformDeep(value, "capitalize"); } /** * Asserts that a value is not undefined. Throws an error if the value is undefined. * This is a type assertion function that narrows the type to exclude undefined. * * @template T - The type of the value being checked * @param value - The value to check for undefined * @param message - Optional error message to use if assertion fails * @throws {Error} When the value is undefined * @example * ```typescript * function processUser(user?: User) { * assertDefined(user); * return user.name; // TypeScript knows user is defined * } * ``` */ function assertDefined(value, message) { invariant(value !== undefined, message); } /** * Asserts that a value is not null. Throws an error if the value is null. * This is a type assertion function that narrows the type to exclude null. * * @template T - The type of the value being checked * @param value - The value to check for null * @param message - Optional error message to use if assertion fails * @throws {Error} When the value is null * @example * ```typescript * function processData(data: string | null) { * assertNotNull(data); * return data.toUpperCase(); // TypeScript knows data is not null * } * ``` */ function assertNotNull(value, message) { invariant(value !== null, message); } /** * Asserts that a value is not null or undefined. Throws an error if the value is null or undefined. * This is a type assertion function that narrows the type to exclude null and undefined. * * @template T - The type of the value being checked * @param value - The value to check for null or undefined * @param message - Optional error message to use if assertion fails * @throws {Error} When the value is null or undefined * @example * ```typescript * function processOptionalData(data?: string | null) { * assertNotEmpty(data); * return data.toUpperCase(); // TypeScript knows data is not null/undefined * } * ``` */ function assertNotEmpty(value, message) { invariant(value !== null && value !== undefined, message); } /** * Ensures that a value is defined, returning it if defined or throwing an error if undefined. * * @template T - The type of the value being checked * @param value - The value to ensure is defined * @param message - Optional error message to use if the value is undefined * @returns The value if it is defined * @throws {Error} When the value is undefined * @example * ```typescript * function processUser(user?: User) { * const definedUser = ensureDefined(user); * return definedUser.name; // definedUser is guaranteed to be defined * } * ``` */ function ensureDefined(value, message) { assertDefined(value, message); return value; } /** * Ensures that a value is not null, returning it if not null or throwing an error if null. * Unlike assertNotNull, this function returns the value for use in expressions. * * @template T - The type of the value being checked * @param value - The value to ensure is not null * @param message - Optional error message to use if the value is null * @returns The value if it is not null * @throws {Error} When the value is null * @example * ```typescript * function processData(data: string | null) { * const nonNullData = ensureNotNull(data); * return nonNullData.toUpperCase(); // nonNullData is guaranteed to be not null * } * ``` */ function ensureNotNull(value, message) { assertNotNull(value, message); return value; } /** * Ensures that a value is not null or undefined, returning it if valid or throwing an error if empty. * * @template T - The type of the value being checked * @param value - The value to ensure is not null or undefined * @param message - Optional error message to use if the value is null or undefined * @returns The value if it is not null or undefined * @throws {Error} When the value is null or undefined * @example * ```typescript * function processOptionalData(data?: string | null) { * const validData = ensureNotEmpty(data); * return validData.toUpperCase(); // validData is guaranteed to be not null/undefined * } * ``` */ function ensureNotEmpty(value, message) { assertNotEmpty(value, message); return value; } function downloadFile(dataOrUrl, options = {}) { if (typeof dataOrUrl === "string") { const { name } = options; const a = document.createElement("a"); a.href = dataOrUrl; a.target = "_blank"; if (name) a.download = name; a.click(); } else { const url = URL.createObjectURL(dataOrUrl); downloadFile(url, options); URL.revokeObjectURL(url); } } /** * React hook for tracking async task execution with loading state. * Automatically manages a loading counter and provides a wrapper function for tasks. * * @returns A tuple containing [isLoading: boolean, runInTask: function] * @example * ```typescript * function MyComponent() { * const [isLoading, runInTask] = useRunInTask(); * * const handleSave = async () => { * await runInTask(async () => { * await saveData(); * }); * }; * * return ( * <button onClick={handleSave} disabled={isLoading}> * {isLoading ? 'Saving...' : 'Save'} * </button> * ); * } * ``` */ function useRunInTask() { const [runningTasks, setRunningTasks] = useState(0); const runInTask = useCallback(async (task)=>{ setRunningTasks((runningTasks)=>runningTasks + 1); try { return await task(); } finally{ setRunningTasks((runningTasks)=>runningTasks - 1); } }, []); return [ runningTasks > 0, runInTask ]; } function useBoundRunInTask(block) { const [isRunning, runInTask] = useRunInTask(); const runBlockInTask = useMemo(()=>block ? (...args)=>runInTask(()=>block(...args)) : undefined, [ block, runInTask ]); return [ isRunning, runBlockInTask ]; } /** * React hook for generating keys based on current route matches. * * @template TKey - The type of the route keys * @param routeMatches - Record of route keys to match objects or arrays * @returns Array of active route keys * @example * ```typescript * function NavigationComponent() { * const routeMatches = { * home: useRouteMatch('/'), * about: useRouteMatch('/about'), * contact: useRouteMatch('/contact') * }; * * const activeRoutes = useKeyByRoute(routeMatches); * // Returns ['home'] if on home page, ['about'] if on about page, etc. * * return ( * <nav> * {activeRoutes.map(route => ( * <span key={route}>Active: {route}</span> * ))} * </nav> * ); * } * ``` */ function useKeyByRoute(routeMatches) { const keys = []; for(const key in routeMatches){ const matches = routeMatches[key]; if (Array.isArray(matches) ? matches.some((match)=>match !== null) : matches !== null) { keys.push(key); } } return keys; } /** * React hook for boolean state management helpers. * * @param set - The state setter function from useState * @returns A tuple containing [setTrue: function, setFalse: function] * @example * ```typescript * function MyComponent() { * const [isVisible, setIsVisible] = useState(false); * const [show, hide] = useSetUnset(setIsVisible); * * return ( * <div> * <button onClick={show}>Show</button> * <button onClick={hide}>Hide</button> * {isVisible && <div>Content is visible</div>} * </div> * ); * } * ``` */ function useSetUnset(set) { return [ useCallback(()=>set(true), [ set ]), useCallback(()=>set(false), [ set ]) ]; } /** * React hook for managing dialog state with optional callback after closing. * Provides convenient open/close functions and tracks the dialog's open state. * * @param onAfterClose - Optional callback function to execute after the dialog closes * @returns Object containing dialog state and control functions * @example * ```typescript * function MyComponent() { * const { isDialogOpen, openDialog, closeDialog } = useDialog(() => { * console.log('Dialog closed'); * }); * * return ( * <div> * <button onClick={openDialog}>Open Dialog</button> * {isDialogOpen && <Dialog onClose={closeDialog} />} * </div> * ); * } * ``` */ function useDialog(onAfterClose) { const [isDialogOpen, setIsDialogOpen] = useState(false); const [openDialog, closeDialog] = useSetUnset(setIsDialogOpen); const close = useCallback(()=>{ closeDialog(); if (onAfterClose) setTimeout(onAfterClose); }, [ closeDialog, onAfterClose ]); return { isDialogOpen, openDialog, closeDialog: close }; } export { addPrefix, assertDefined, assertNotEmpty, assertNotNull, capitalizeDeep, downloadFile, ensureDefined, ensureNotEmpty, ensureNotNull, toLowerFirst, toUpperFirst, uncapitalizeDeep, useBoundRunInTask, useDialog, useKeyByRoute, useRunInTask, useSetUnset };