Доработка хуков useQuery, useParams, покрытие тестами (#30)
This commit is contained in:
@ -21,3 +21,5 @@ export const PAGE_TITLE = {
|
||||
[PageType.Settings]: 'Settings',
|
||||
[PageType.SigIn]: 'SigIn',
|
||||
};
|
||||
|
||||
export const UTC_DATE_FORMAT = '';
|
||||
|
||||
8
src/core/hooks/usePageType.ts
Normal file
8
src/core/hooks/usePageType.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {useMemo} from 'react';
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {getPageType} from '../utils/common';
|
||||
|
||||
export const usePageType = () => {
|
||||
const location = useLocation();
|
||||
return useMemo(() => getPageType(location.pathname), [location.pathname]);
|
||||
};
|
||||
@ -1,25 +1,10 @@
|
||||
import {useMemo} from 'react';
|
||||
import {useLocation, useParams as useReactParams} from 'react-router-dom';
|
||||
import {PageType} from '../enums/common';
|
||||
import {getPageType} from '../utils/common';
|
||||
import {useParams as useReactParams} from 'react-router-dom';
|
||||
import {getParamsFromUrl} from '../utils/getParamFromUrl';
|
||||
import {QueryParsers} from '../utils/getQueryFromUrl';
|
||||
|
||||
type ParamsParser<T> = (value?: string) => T;
|
||||
export type ParamsParsers<T> = Partial<{[K in keyof T]: ParamsParser<T[K]>}>;
|
||||
|
||||
export function useParams<T extends {[name: string]: unknown}>(paramParsers: ParamsParsers<T> = {}) {
|
||||
export function useParams<T extends Record<string, unknown>>(paramParsers: QueryParsers<T>) {
|
||||
const params = useReactParams<Record<keyof T, string>>();
|
||||
const {pathname} = useLocation();
|
||||
|
||||
return useMemo(() => {
|
||||
return Object.keys(paramParsers).reduce<T & {pageType: PageType}>((memo, key) => {
|
||||
const parser = paramParsers[key];
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(params[key]),
|
||||
};
|
||||
}, {
|
||||
pageType: getPageType(pathname),
|
||||
} as T & {pageType: PageType});
|
||||
}, [params, paramParsers, pathname]);
|
||||
return useMemo(() => getParamsFromUrl(paramParsers, params), [params, paramParsers]);
|
||||
}
|
||||
|
||||
@ -1,70 +1,9 @@
|
||||
import {parse, ParsedUrlQuery} from 'querystring';
|
||||
import {head} from 'lodash';
|
||||
import {useMemo} from 'react';
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {toNumber} from '../utils/parsers';
|
||||
import {getQueryFromUrl, QueryParsers} from '../utils/getQueryFromUrl';
|
||||
|
||||
type QueryParser<T> = (value?: string | string[]) => T;
|
||||
export type QueryParsers<T> = Partial<{[K in keyof T]: QueryParser<T[K]>}>;
|
||||
|
||||
export function stringParser<T extends string>(): QueryParser<Undefinable<T>>;
|
||||
export function stringParser<T extends string>(defaultValue: T): QueryParser<T>;
|
||||
export function stringParser<T extends string>(defaultValue?: T) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return value ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function numberParser(): QueryParser<Undefinable<number>>;
|
||||
export function numberParser(defaultValue?: number): QueryParser<number>;
|
||||
export function numberParser(defaultValue?: number) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return toNumber(value) ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanParser(): QueryParser<Undefinable<boolean>>;
|
||||
export function booleanParser(defaultValue: boolean): QueryParser<boolean>;
|
||||
export function booleanParser(defaultValue?: boolean) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
// Date parser
|
||||
|
||||
// Array parser (должен уметь с enum)
|
||||
|
||||
export function useQuery<T extends {[name: string]: unknown}>(
|
||||
queryParsers: QueryParsers<T>
|
||||
): ParsedUrlQuery | Partial<T> {
|
||||
export function useQuery<T extends Record<string, unknown>>(queryParsers: QueryParsers<T>): T {
|
||||
const {search} = useLocation();
|
||||
|
||||
return useMemo(() => {
|
||||
const query = parse(search.slice(1));
|
||||
return queryParsers ? Object.keys(queryParsers).reduce<T>((memo, key) => {
|
||||
if (key in queryParsers) {
|
||||
const parser = queryParsers[key];
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(query[key]),
|
||||
};
|
||||
}
|
||||
return memo;
|
||||
}, {} as T) : query;
|
||||
}, [search, queryParsers]);
|
||||
return useMemo(() => getQueryFromUrl(queryParsers, search), [search, queryParsers]);
|
||||
}
|
||||
|
||||
30
src/core/utils/__test__/getParamFromUrl.test.ts
Normal file
30
src/core/utils/__test__/getParamFromUrl.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {PageType} from '../../enums/common';
|
||||
import {getParamsFromUrl} from '../getParamFromUrl';
|
||||
import {QueryParsers} from '../getQueryFromUrl';
|
||||
import {booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('getParamsFromUrl', () => {
|
||||
type Params = {
|
||||
name?: string;
|
||||
id?: number;
|
||||
pageType?: PageType;
|
||||
isNew: boolean;
|
||||
};
|
||||
|
||||
it('Получение параметров', () => {
|
||||
const parsers: QueryParsers<Params> = {
|
||||
name: stringParser(),
|
||||
id: numberParser(),
|
||||
pageType: stringParser(),
|
||||
isNew: booleanParser(false),
|
||||
};
|
||||
expect(getParamsFromUrl(parsers, {olo: 't'})).toEqual({isNew: false});
|
||||
expect(getParamsFromUrl(parsers, {name: 't'})).toEqual({name: 't', isNew: false});
|
||||
expect(getParamsFromUrl(parsers, {
|
||||
name: 't',
|
||||
id: '6',
|
||||
pageType: 'tags',
|
||||
isNew: 'true',
|
||||
})).toEqual({name: 't', id: 6, pageType: PageType.Tags, isNew: true});
|
||||
});
|
||||
});
|
||||
32
src/core/utils/__test__/getQueryParamFromUrl.test.ts
Normal file
32
src/core/utils/__test__/getQueryParamFromUrl.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {PageType} from '../../enums/common';
|
||||
import {getQueryFromUrl, QueryParsers} from '../getQueryFromUrl';
|
||||
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('getQueryFromUrl', () => {
|
||||
type Query = {
|
||||
name?: string;
|
||||
id?: number;
|
||||
pageType?: PageType;
|
||||
isNew: boolean;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
it('Получение параметров', () => {
|
||||
const parsers: QueryParsers<Query> = {
|
||||
name: stringParser(),
|
||||
id: numberParser(),
|
||||
pageType: stringParser(),
|
||||
isNew: booleanParser(false),
|
||||
colors: arrayParser([]),
|
||||
};
|
||||
expect(getQueryFromUrl(parsers, '?olo=2')).toEqual({isNew: false, colors: []});
|
||||
expect(getQueryFromUrl(parsers, '?olo=2&name=t')).toEqual({name: 't', isNew: false, colors: []});
|
||||
expect(getQueryFromUrl(parsers, '?name=t&id=6&pageType=tags&isNew=true&colors=red&colors=blue')).toEqual({
|
||||
name: 't',
|
||||
id: 6,
|
||||
pageType: PageType.Tags,
|
||||
isNew: true,
|
||||
colors: ['red', 'blue'],
|
||||
});
|
||||
});
|
||||
});
|
||||
98
src/core/utils/__test__/queryParsers.test.ts
Normal file
98
src/core/utils/__test__/queryParsers.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('stringParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(stringParser()('trt')).toBe('trt');
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(stringParser()(['trt', 'ggt'])).toBe('trt');
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(stringParser()()).toBeUndefined();
|
||||
expect(stringParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(stringParser('def')()).toBe('def');
|
||||
expect(stringParser('def')([])).toBe('def');
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(stringParser('def')(['trt'])).toBe('trt');
|
||||
expect(stringParser('def')('trt')).toBe('trt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numberParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(numberParser()('45')).toBe(45);
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(numberParser()(['45', '44'])).toBe(45);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(numberParser()()).toBeUndefined();
|
||||
expect(numberParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(numberParser(33)()).toBe(33);
|
||||
expect(numberParser(33)([])).toBe(33);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(numberParser(33)(['45'])).toBe(45);
|
||||
expect(numberParser(33)('45')).toBe(45);
|
||||
});
|
||||
});
|
||||
|
||||
describe('booleanParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(booleanParser()('true')).toBe(true);
|
||||
expect(booleanParser()('false')).toBe(false);
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(booleanParser()(['true', 'false'])).toBe(true);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(booleanParser()()).toBeUndefined();
|
||||
expect(booleanParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(booleanParser(true)()).toBe(true);
|
||||
expect(booleanParser(false)([])).toBe(false);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(booleanParser(false)(['true'])).toBe(true);
|
||||
expect(booleanParser(false)('true')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(arrayParser()('rtr')).toEqual(['rtr']);
|
||||
expect(arrayParser()(['rtr', 'rtr'])).toEqual(['rtr', 'rtr']);
|
||||
expect(arrayParser()([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(arrayParser()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(arrayParser(['def'])()).toEqual(['def']);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(arrayParser(['def'])('rtr')).toEqual(['rtr']);
|
||||
expect(arrayParser(['def'])(['rtr'])).toEqual(['rtr']);
|
||||
});
|
||||
});
|
||||
15
src/core/utils/getParamFromUrl.ts
Normal file
15
src/core/utils/getParamFromUrl.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {QueryParsers} from './getQueryFromUrl';
|
||||
|
||||
export const getParamsFromUrl = <T extends Record<string, unknown>>(
|
||||
paramParsers: QueryParsers<T>,
|
||||
params: Record<string, string>
|
||||
) => {
|
||||
return Object.keys(paramParsers).reduce<T>((memo, key) => {
|
||||
const parser = paramParsers[key];
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(params[key]),
|
||||
};
|
||||
}, {} as T);
|
||||
};
|
||||
19
src/core/utils/getQueryFromUrl.ts
Normal file
19
src/core/utils/getQueryFromUrl.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {decode} from 'querystring';
|
||||
|
||||
export type QueryParser<T> = (value?: string | string[]) => T;
|
||||
export type QueryParsers<T> = {[K in keyof T]: QueryParser<T[K]>};
|
||||
|
||||
export const getQueryFromUrl = <T extends Record<string, unknown>>(queryParsers: QueryParsers<T>, search?: string) => {
|
||||
const query = decode((search || location.search).slice(1));
|
||||
|
||||
return Object.keys(queryParsers).reduce<T>((memo, key) => {
|
||||
if (key in queryParsers) {
|
||||
const parser = queryParsers[key];
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(query[key]),
|
||||
};
|
||||
}
|
||||
return memo;
|
||||
}, {} as T);
|
||||
};
|
||||
53
src/core/utils/queryParsers.ts
Normal file
53
src/core/utils/queryParsers.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {head} from 'lodash';
|
||||
import {QueryParser} from './getQueryFromUrl';
|
||||
import {toNumber} from './parsers';
|
||||
|
||||
export function stringParser<T extends string>(): QueryParser<Undefinable<T>>;
|
||||
export function stringParser<T extends string>(defaultValue: T): QueryParser<T>;
|
||||
export function stringParser<T extends string>(defaultValue?: T) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return value ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function numberParser(): QueryParser<Undefinable<number>>;
|
||||
export function numberParser(defaultValue?: number): QueryParser<number>;
|
||||
export function numberParser(defaultValue?: number) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return toNumber(value) ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanParser(): QueryParser<Undefinable<boolean>>;
|
||||
export function booleanParser(defaultValue: boolean): QueryParser<boolean>;
|
||||
export function booleanParser(defaultValue?: boolean) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function arrayParser<T extends string>(): QueryParser<Undefinable<T[]>>;
|
||||
export function arrayParser<T extends string>(defaultValue: T[]): QueryParser<T[]>;
|
||||
export function arrayParser<T extends string>(defaultValue?: T[]) {
|
||||
return (val?: string | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return val === undefined ? defaultValue : [val];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user