@carforyou/search-parameters
TypeScript icon, indicating that this package has built-in type declarations

2.0.2 • Public • Published

CAR FOR YOU Search Parameters Handling

semantic-release

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

parameters processing

  1. 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" to ListingType.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

  2. 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 query

  • handling 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 have year and month 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 boolean
  • bodyType - is an array of strings
  • listingType - is an enum in the application. Values are imported and manual
  • powerTo - is a number value with a unit. The unit can be either kW or HP
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 classified
  • skip
    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 by i18n 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 function
  • mappings - a collection of function maps keys to specific values (think makeKey - 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 request
  • size - page size
  • sortOrder - ascending or descending sorting
  • sortType - way the data is sorted
  • language - that you want to leave to i18n 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 query
  • buildSearchPath - 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 prop
  • addFilters
  • resetFilters - a function that removes the filters except the ones whose names are passed to it
  • updatePagination
  • 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.

Readme

Keywords

none

Package Sidebar

Install

npm i @carforyou/search-parameters

Weekly Downloads

2

Version

2.0.2

License

MIT

Unpacked Size

64.5 kB

Total Files

45

Last publish

Collaborators

  • carforyou-engineering
  • lkappeler