@carforyou/search-parameters
v2.0.2
Published
A package that helps with search parameters handling
Downloads
42
Readme
CAR FOR YOU Search Parameters Handling
This package deals with concerns regarding search query and search parameters for different things that you can search in the CAR FOR YOU universe.
Overview
Parameters processing
Query Decoding
In this step, the parameters that were encoded are converted back to their rich forms.
Currently, the following encodings are supported:- array parameters (encoded as comma-separated strings)
- mapping strings to enums (
"manual"
toListingType.Manual
) - joining multi-field values (e.g. make, model & type filter, values with a unit, dates, etc.)
After this step, the parameters are in the format expected by other parts of the application
Parameters Classification
In this step parameters are assigned to groups related to their functions:pagination
sort
filters
others
After this step, the parameters are in the format expected by the application and the API client.
Search context
The SearchContext
has two functions:
sharing classified parameters down the component tree
This reduces the need for props drilling and makes it easier to render all the components that depend on the search queryhandling changes to search-related parameters
The context provides methods to handle modification of the search query (e.g. applying a new filter). This way, they are all kept in one place and are easier to handle.
Usage
Query decoding/encoding
This package provides a way to decode/encode the basic query parameters. The following cases are supported:
- booleans
{
kind: "boolean"
}
- enums
{
kind: "enum",
mapping: {
V1: MyEnum.Value1,
V2: MyEnum.Value2,
}
}
- combined parameters
A combined parameter is a multi-field parameter that is passed as a single URL parameter (e.g. a value with a unit). The fields are separated by the pipe (|
).
{
kind: "combined",
definition: {
fields: [
{
name: "value",
// an optional conversion function, default is identity
convert: Number
},
{
name: "unit"
}
],
// function that ensures that the decoded object is valid
// if invalid parameter is passed it will be filtered out
isValid: ({ value, unit }) => value > 0 && ["kW", "HP"].includes(unit),
}
}
- dates
Dates are a special type of combined parameters - they haveyear
andmonth
Number
fields that are separated by a dash (-
).
{
kind: "date",
}
- array parameters
The elements of the array are comma (,
) separated.
{
kind: "array",
// an optional encoding of array elements
// it can define either `enum` or `combined` encoding
elementEncoding: { ... }
}
This package provides a decodeParams
function that decodes the parameters and a dual encodeParams
function that reverses the process. Both functions are higher-order and require encoding definition to be passed to them. This is useful if you plan to reuse your encoding/decoding function across the project.
How to use it in practice?
Definition description
Let's say that you have the following parameters you want to handle:
hasImages
- is a booleanbodyType
- is an array of stringslistingType
- is an enum in the application. Values areimported
andmanual
powerTo
- is a number value with a unit. Theunit
can be eitherkW
orHP
Step 1. Creating the definition
An encoding definition is an object whose keys are parameter names, and values are descriptions of encoding of said parameters. Any which don't have encoding defined will just be passed through as they appear (i.e. they won't be encoded).
enum ListingType {
Imported = "imported",
Manual = "manual",
}
const definition = {
hasImages: {
kind: "boolean"
},
bodyType: {
kind: "array"
// no element encoding here since we want strings
},
listingType: {
kind: "enum",
// describes how to map strings to enum values
mapping: {
manual: ListingType.Manual,
imported: ListingType.Imported,
}
},
powerTo: {
kind: "combined",
definition: {
// this will separate fields in the string form
separator: "|",
fields: [
{
fieldName: "value",
// we want to have numbers
convert: Number
},
{
fieldName: "unit"
}
],
// zero or negative power doesn't make sense
// we also only support two units
isValid: ({ value, unit }) =>
value > 0 && ["kW", "HP"].includes(unit),
}
}
}
Step 2. Decoding the parameters
Let's say those are our URL parameters:
const parameters = {
hasImages: "true",
bodyType: "coupe,cabriolet",
listingType: "manual",
powerTo: "150|kW"
}
To decode them we would:
import { decodeParams } from "@carforyou/search-parameters"
// optional if you want to reuse the decoding function
const decodingFunction = decodeParams(definition)
const decodedParameters = decodingFunction(parameters)
This will yield:
{
hasImages: true,
bodyType: ["coupe", "cabriolet"],
listingType: ListingType.manual,
powerTo: { value: 150, unit: "kW" },
}
Step 3. Encoding the parameters
If you would need to generate a link with some parameters, you can convert them back:
import { encodeParams } from "@carforyou/search-parameters"
// optional if you want to reuse the encoding function
const encodingFunction = encodeParams(definition)
const encodedParameters = decodingFunction(decodedParameters)
This will yield:
const encodedParameters = {
hasImages: "true",
bodyType: "coupe,cabriolet",
listingType: "manual",
powerTo: "150|kW"
}
Parameters classification
This package provides a way to group related search parameters (classify them). The following cases are support
pagination
filters
sort
other
this group captures all the parameters which weren't classifiedskip
this removes a parameter from classification. This can be useful when you're dealing with parameters that you want the framework to handle (e.g.language
that is handled byi18n
framework)
Since it's desirable to know how to render filter value as a tag either to visualize applied filters better or to enable clearing single filters more easily when you want to classify a parameter as a filter, you need to provide getLabel
method as well. It takes the current filter value as an argument and returns a string or an array of strings (think about multiple selection filters). You can also pass an optional argument containing:
t
- translation functionmappings
- a collection of function maps keys to specific values (thinkmakeKey
-make.name
mapping)
This package provides classifyParams
that classifies the parameters and a dual toDecodedParam
function that reverses (flattens) the classification. classifyParams
is a higher-order function that requires classification to be passed to it. This is useful if you plan to reuse the classification function within the project. Values considered as empty (null
, undefined
, ""
and []
) will be removed during classification.
How to use it in practice
Classification description
Let's say that we have following parameters:
page
- current page of the paginated requestsize
- page sizesortOrder
- ascending or descending sortingsortType
- way the data is sortedlanguage
- that you want to leave toi18n
to handle- a few filters:
bodyType
hasImages
listingType
powerTo
cityId
Step 1. Creating the classification
A parameter classification is an object whose keys are parameter names and values are classification groups. Any parameter for which classification is not defined will belong to other
group by default.
const classification = {
page: "pagination",
size: "pagination",
sortOrder: "sort",
sortType: "sort",
language: "skip",
bodyType: {
kind: "filters",
getLabel: (value, { t }) =>
value.map((bodyType) => t(`bodyTypes.${bodyType}`)),
},
hasImages: {
kind: "filters",
getLabel: (value, { t }) =>
value ? t("hasImages") : t("noImages"),
},
listingType: {
kind: "filters",
getLabel: (value, { t }) => t(`listingTypes.${value}`),
},
powerTo: {
kind: "filters",
// powerTo is a combined parameter as defined above
getLabel: ({ value, unit }) => `max: ${value} ${unit}`,
},
cityId: {
kind: "filters",
getLabel: (value, mappings: { getCityName }) => getCityName({ cityId: value })
}
}
Step 2. Classifying the parameters
Let's say those are our parameters:
{
language: "de",
page: 1,
size: 5,
sortOrder: "ASC",
sortType: "RELEVANCE",
hasImages: true,
bodyType: ["coupe", "cabriolet"],
listingType: ListingType.manual,
powerTo: { value: 150, unit: "kW" },
utm_campaign: "i am utm campaign",
}
To classify them we would:
import { classifyParams } from "@carforyou/search-parameters"
// optional if you want to reuse the classification function
const classificationFunction = classifyParams(classification)
const classifiedParameters = classificationFunction(parameters)
This will yield:
{
pagination: {
page: 1,
size: 5,
},
sort: {
sortOrder: "ASC",
sortType: "RELEVANCE",
},
filters: {
hasImages: true,
bodyType: ["coupe", "cabriolet"],
listingType: ListingType.manual,
powerTo: { value: 150, unit: "kW" },
},
other: {
utm_campaign: "i am utm campaign",
}
}
Step 3. Flattening the query
If you need to generate a link with some parameters you can reverse the classification:
import { toDecodedParams } from "@carforyou/search-parameters"
const flattenedQuery = toDecodedParams(classifiedParameters)
This will yield:
{
page: 1,
size: 5,
sortOrder: "ASC",
sortType: "RELEVANCE",
hasImages: true,
bodyType: ["coupe", "cabriolet"],
listingType: ListingType.manual,
powerTo: { value: 150, unit: "kW" },
utm_campaign: "i am utm campaign",
}
Deriving filters
Sometimes the URL parameters do not map 1-1 to the filters that you use. For example, one parameter needs be translated to multiple filters or mapped to another field.
To this end deriveFilters
method provided by the package can be used. It is a higher-order function that requires a definition. This is useful if you plan to reuse it across the project.
Example description
We want to allow user filtering by ListingType
while manual
and imported
correspond directly to a manual
filter there is also an additional premium
type that allows finding listings with promotional features enabled.
Step 1. Defining how to derive filters
A derived filters definition is an object whose keys are parameters names and values are an object containing:
getValue
- a function that derives the filter value
The argument the function is an object containing classified query and the result is the value of the derived filter.
getLabel
- a function that generates the label for the filter
Similar to the one used in parameter classification
const definition = {
isManual: {
getValue: ({ filters }) => {
switch (filters?.listingType) {
case ListingType.Manual:
return true
case ListingType.Imported:
return false
}
},
getLabel: (value) => (value ? "Manual" : "Imported"),
},
isPremium: {
derive: ({ filters }) => filters?.listingType === ListingType.Premium,
getLabel: (value) => (value ? "Premium" : "Non-premium"),
},
}
Step 2. Deriving the filters
Let's say we have query (note that this is post-classification):
const classifiedQuery = {
filters: {
listingType: ListingType.Imported,
},
pagination: {},
sort: {},
others: {},
}
To derive the filters:
import { deriveFilters } from "@carforyou/search-parameters"
// optional if you want to reuse the deriving function
const derivingFunction = deriveFilters(definition)
const queryWithDerivedFilters = derivingFunction(parameters)
This will yield:
const classifiedQuery = {
filters: {
listingType: ListingType.Imported,
},
derivedFilters: {
isManual: true,
isPremium: false,
}
pagination: {},
sort: {},
others: {},
}
Sharing and modifying the search query
To share and allow modifying the search query SearchQueryContext
can be used.
You would render the provider. The provider accepts two props:
searchQuery
- is a classified search querybuildSearchPath
- is a function that returns a search path based on a decoded query
When the query is modified (e.g. by applying new filter) this is the page the user will navigate to.
import {
SearchQueryProvider,
decodeParams,
encodeParams,
classifyParams,
deriveFilters
} from "@carforyou/search-parameters"
const SearchPage = ({ searchQuery, searchResult }) => {
return (
<SearchQueryProvider
searchQuery={searchQuery}
buildSearchPath={(newQuery) =>
`/search?${toQueryString(encodeParams(encodingDefinition)(newQuery))}`
}
>
// rest of the search page
</SearchQueryProvider>
)
}
export const getServerSideProps = async ({ query }) => {
const searchQuery = deriveFilters(derivingDefinition)(
classifyParams(classification)(
decodeParams(encodingDefinition)(query)
)
)
const searchResult = //...
return {
searchQuery,
searchResult,
}
}
export default SearchPage
The context provides following properties:
searchQuery
- the query that had been passed as a propaddFilters
resetFilters
- a function that removes the filters except the ones whose names are passed to itupdatePagination
updateSort
buildSearchPath
- a function that can be used to render search related links (e.g. page links in the pagination component)
Development
npm run build
You can link your local npm package to integrate it with any local project:
cd carforyou-search-parameters-pkg
npm run build
cd carforyou-listings-web
npm link ../carforyou-search-parameters-pkg
Release a new version
New versions are released on the ci using semantic-release as soon as you merge into master. Please make sure your merge commit message adheres to the corresponding conventions.
Circle CI
You will need to enable the repository in circle CI UI to be able to build it.
For slack notifications to work, you will need to provide the token in circle settings.