@leancodepl/utils
Version:
Common utility functions and React hooks for web applications
407 lines (389 loc) • 13 kB
JavaScript
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 };