@youha-eng/next-query-utils
v0.1.1
Published
Youha specific utilities for using next-query-state to persist page's current state in the URL.
Downloads
4
Readme
@youha-eng/next-query-utils
Youha specific utilities for using next-query-state to persist page's current state in the URL.
Contains code specific for Youha's API (useFilter
).
Installation
$ yarn add @youha-eng/next-query-utils next-query-state
or
$ npm install @youha-eng/next-query-utils next-query-state
Documentation
usePagination
Utility hook for pagination state control.
- Save state in the URL as
"page"
and"pageSize"
query parameters. page
andpageSize
converted intolimit
andoffset
for easier API calls.page
andpageSize
can be updated viasetPagination
.- Using
changePageSize
to updatepageSize
automatically changespage
so that the first item remains visible on the screen.
const [{ page, pageSize, limit, offset }, setPagination, changePageSize] = usePagination({
defaultPageSize: 20,
});
const onPageChange = (e) => {
setPagination({ page: parseInt(e.currentTarget.value) }, { history: "push" });
};
const onPageSizeChange = (e) => {
changePageSize(parseInt(e.currentTarget.value));
};
return (
<div>
<div>
page: <input value={page} onChange={onPageChange} />
</div>
<div>
page size:
<select value={pageSize} onChange={onPageSizeChange}>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
);
useSort
Utility hook for sort state control.
- Returned value always starts with '+' or '-'.
- Allowed values can be set with
allowed
parameter. '+' or '-' prefix only allows that sort direction, and no prefix allows both '+' and '-'. showPlus
option is for showing '+' in the URL or not. No prefix implies ascending order. Default is false because '+' gets percent encoded and makes URL look dirty in current implementation.delimiter
option sets which delimiter to use for separating sort strings. Set to null for using duplicate keys instead of delimited string. Defaults to "_".- By default,
defaultSort
,allowed
,showPlus
,delimiter
option must not be changed. Setdynamic
option totrue
to change those.
const [sort, setSort] = useSort({
defaultSort: ["+fieldA"],
allowed: ["fieldB", "+fieldA"],
history: "push",
showPlus: true,
delimiter: "_",
});
return (
<div>
{["+a", "-a", "+b", "-b"].map((field) => (
<button key={field} onClick={() => setSort((p) => [...p, field as SortType])}>
{field}
</button>
))}
{sort.map((v, i) => (
<div key={i}>{v}</div>
))}
</div>
);
useFilter
Utility hook for constructing filter expression by reading from URL state.
- Filter expression is a list of 3-tuple made up of field name, filter operator, and filter value.
- For example,
?rangeMin=12345&rangeMax=6431&search=foo&category=a&category=b
becomes:[["search","=","foo"], ["range","<=",6431], ["range",">=",12345], ["category","=",["a","b"]]]
- For example,
useFilter
takes a list ofFilterDef
, which is made up of Map ofSerializers<T>
, and atransform
function that takes the parsed state object and creates a list of filter expression which will be concatenated.useFilter
returns the concatenated filter expression list.FilterDef
can be manually created, but a preset factory utilityfilterDefFactory
andfilterTypes
is provided for convenience.
Preset
Since useFilter
takes a list of FilterDef
instead of a map, a function to make map into list is needed, which is filterDefFactory
.
When using preset and custom created FilterDef
together, spread the return value of filterDefFactory
like below.
useFilter([
...filterDefFactory({
search: filterTypes.nullable.string.equal(),
range: filterTypes.integer.range(),
category: filterTypes.enum(["a", "b", "c"]).in(),
}),
// Custom FilterDef
{
queryType: ...,
transformer: ()=>{}
}
])
filterDefFactory
takes a map of FilterGenerator
. its Key is the name of the field, and value is FilterGenerator
. FilterGenerator
can be easily constructed from filterTypes
.
To use filterTypes
, select the data type of the field, then select which kind of filter (equal, range, in) it is.
There are 5 types available, string
, float
, integer
, boolean
, enum
.
You can also have null
as a value like this: filterTypes.nullable.string
, which the null value is represented in the URL as %00
.
Then, select which kind of filter is enabled.
"equal"
only allows 1 value, which will result to filter expression like this: [[field, "=", value]]
"range"
will read from 2 query params with postfix "Min"
and "Max"
(for example ?fieldMin=10&fieldMax=20
), and will result to filter expression like this: [[field, ">=". fieldMin], [field, "<=", fieldMax]]
.
It includes the filter expression only if it's in the query string.
- There is a boolean
excludeNull
option that adds[field, "!=", null]
filter expression when min or max filter exists.
"in"
allows many values, and will result to filter expression like this: [[field, "=", values]]
delimiter
option can be used to set which delimiter should be used to separate multiple values in the URL. Default isundefined
which doesn't use delimiter and uses duplicate query keys to express list of values. (?field=a&field=b
)
Example: Read filter state in URL with useFilter
to show filtered results
function FilterResults() {
const filters = useFilter(
filterDefFactory({
search: filterTypes.nullable.string.equal(),
range: filterTypes.integer.range(),
category: filterTypes.enum(["a", "b", "c"]).in(),
})
);
// Send filters as API request to server and show its response.
const data = getDataFromServer(filters)
return (
<div>
{data.map((d)=><DataPresenter data={d}>)}
</div>
);
}
Example: Control filter state with useQueryState
function FilterPanel() {
const [search, setSearch] = useQueryState("search", queryTypes.string);
const [rangeMin, setRangeMin] = useQueryState("rangeMin", queryTypes.integer);
const [rangeMax, setrangeMax] = useQueryState("rangeMax", queryTypes.integer);
const [categories, setCategories] = useQueryState(
"category",
queryTypes.array(queryTypes.stringEnum(["a", "b", "c"])).withDefault([])
);
return (
<div>
<div>
search:
<input
value={search ?? ""}
onChange={(e) => setSearch(e.currentTarget.value || null)}
/>
</div>
<div>
range:
<input
value={rangeMin ?? ""}
onChange={(e) => setRangeMin(parseIntOrNull(e.currentTarget.value))}
/>
<input
value={rangeMax ?? ""}
onChange={(e) => setrangeMax(parseIntOrNull(e.currentTarget.value))}
/>
</div>
<div>
categories:
{["a", "b", "c"].map((v) => (
<span key={v}>
<input
type="checkbox"
id={v}
checked={categories.includes(v)}
onChange={(e) =>
e.currentTarget.checked
? setCategories([...categories, v])
: setCategories(categories.filter((c) => c !== v))
}
/>
<label htmlFor={v}>{v}</label>
</span>
))}
</div>
</div>
);
}
function parseIntOrNull(s: string) {
const int = parseInt(s);
return isNaN(int) ? null : int;
}