UNPKG

@domonda/query-params

Version:

Useful but simple query params manipulator for React.

124 lines (123 loc) 5.06 kB
/** * * queryParams * */ import { stringify as qsStringify, parse as qsParse } from 'query-string'; import { parseISOToDate, stripTime } from './date'; const ARRAY_FORMAT = 'bracket'; // consists of numbers, optional floating point, does not begin with zero // (valid, native, `Number`s in JS never start with zero) const NUMBER_REGEX = /^(?!0)([-0-9]+)(\.[0-9]+)?$/; function isNaN(num) { // only NaN is not equal to itself return num !== num; } export function strictUriEncode(str) { return encodeURIComponent(str).replace(/[!'()*]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); } /** Stringify function which omits `undefined` values and persists empty arrays. */ export function stringify(value, props = {}) { const { prependQuestionMark } = props; const str = qsStringify(Object.entries(value).reduce((acc, [key, val]) => { // omit `undefined`s if (val === undefined) { return acc; } // convert valid dates to iso strings, omit invalid dates if (val instanceof Date) { if (isNaN(val.getDate())) { return acc; } return Object.assign(Object.assign({}, acc), { [key]: val.toISOString() }); } // persist empty arrays if (Array.isArray(val) && val.length === 0) { return Object.assign(Object.assign({}, acc), { [key]: [null] }); } // everything else is just passed through return Object.assign(Object.assign({}, acc), { [key]: val }); }, {}), { arrayFormat: ARRAY_FORMAT }); if (prependQuestionMark && str.length > 0) { return '?' + str; } return str; } /** Parses the URL query string following the `QueryModel` definitions. */ export function parseQueryParams(queryString, model) { const parsedQuery = qsParse(queryString, { arrayFormat: ARRAY_FORMAT, }); const parsedValues = Object.keys(model).reduce((accumulator, key) => { const { type, validate, defaultValue } = model[key]; const parsedAndValidatedQueryValue = (() => { let parsedQueryValue = parsedQuery[key]; if (parsedQueryValue) { switch (type) { case 'boolean': const maybeBool = String(parsedQueryValue).toLowerCase(); if (maybeBool === 't' || maybeBool === 'true' || maybeBool === '1') { parsedQueryValue = true; } else { parsedQueryValue = false; } break; case 'number': parsedQueryValue = parseFloat(parsedQueryValue); break; case 'array': { if (parsedQueryValue.length === 1 && parsedQueryValue[0] == null) { parsedQueryValue = []; } else { // parse numbers, wherever possible parsedQueryValue = parsedQueryValue.map((val) => { if (NUMBER_REGEX.test(val)) { const maybeNum = parseFloat(val); if (!isNaN(maybeNum)) { return maybeNum; } } return val; }); } break; } case 'date': if (parsedQueryValue) { parsedQueryValue = stripTime(parseISOToDate(parsedQueryValue)); if (isNaN(parsedQueryValue.getTime())) { return undefined; } } break; } } if (parsedQueryValue !== undefined && validate && !validate(parsedQueryValue)) { return undefined; } return parsedQueryValue; })(); return deepFreeze(Object.assign(Object.assign({}, accumulator), { [key]: parsedAndValidatedQueryValue === undefined ? typeof defaultValue === 'function' ? defaultValue() : defaultValue : parsedAndValidatedQueryValue })); }, {}); return parsedValues; } /** Following the model, gives the query params populated with default values. */ export function defaultQueryParams(model) { return parseQueryParams('', model); } function deepFreeze(object) { Object.freeze(object); Object.getOwnPropertyNames(object).forEach((name) => { const property = object[name]; if (property && typeof property === 'object' && !Object.isFrozen(property)) { deepFreeze(property); } }); return object; }