UI libraries / InstantSearch.js / Widgets
Signature
numericMenu({
  container: string|HTMLElement,
  attribute: string,
  items: object[],
  // Optional parameters
  templates: object,
  cssClasses: object,
  transformItems: function,
});
Import
1
import { numericMenu } from 'instantsearch.js/es/widgets';

About this widget

The numericMenu widget displays a list of numeric filters in a list. Those numeric filters are pre-configured when creating the widget.

Requirements

The value provided to the attribute option must be an attribute which is a number in the index, not a string.

Examples

1
2
3
4
5
6
7
8
9
10
numericMenu({
  container: '#numeric-menu',
  attribute: 'price',
  items: [
    { label: 'All' },
    { label: 'Less than 500$', end: 500 },
    { label: 'Between 500$ - 1000$', start: 500, end: 1000 },
    { label: 'More than 1000$', start: 1000 },
  ],
});

Options

container
type: string|HTMLElement
Required

The CSS Selector or HTMLElement to insert the widget into.

1
2
3
4
numericMenu({
  // ...
  container: '#numeric-menu',
});
attribute
type: string
Required

The name of the attribute in the record.

1
2
3
4
numericMenu({
  // ...
  attribute: 'price',
});
items
type: object[]
Required

A list of all the options to display, with:

  • label: string: label of the option.
  • start: number: the option must be greater than or equal to start (lower bound).
  • end: number: the option must be smaller than or equal to end (upper bound).
1
2
3
4
5
6
7
8
9
numericMenu({
  // ...
  items: [
    { label: 'All' },
    { label: 'Less than 500$', end: 500 },
    { label: 'Between 500$ - 1000$', start: 500, end: 1000 },
    { label: 'More than 1000$', start: 1000 },
  ],
});
templates
type: object
Optional

The templates to use for the widget.

1
2
3
4
5
6
numericMenu({
  // ...
  templates: {
    // ...
  },
});
cssClasses
type: object
default: {}
Optional

The CSS classes you can override:

  • root: the root element of the widget.
  • noRefinementRoot: container class without results.
  • list: the list of results.
  • item: the list items.
  • selectedItem: the selected item in the list.
  • label: the label of each item.
  • labelText: the text element of each item.
  • radio: the radio button of each item.
1
2
3
4
5
6
7
8
9
10
numericMenu({
  // ...
  cssClasses: {
    root: 'MyCustomNumericMenu',
    list: [
      'MyCustomNumericMenuList',
      'MyCustomNumericMenuList--subclass',
    ],
  },
});
transformItems
type: function
default: items => items
Optional

Receives the items and is called before displaying them. Should return a new array with the same shape as the original array. Useful for transforming, removing, or reordering items.

In addition, the full results data is available, which includes all regular response parameters, as well as parameters from the helper (for example disjunctiveFacetsRefinements).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
numericMenu({
  // ...
  transformItems(items) {
    return items.map(item => ({
      ...item,
      label: item.label.toUpperCase(),
    }));
  },
});

/* or, combined with results */
numericMenu({
  // ...
  transformItems(items, { results }) {
    return items.map(item => ({
      ...item,
      label: item.isRefined && results
        ? `${item.label} (${results.nbHits} hits)`
        : item.label,
    }));
  },
});

Templates

You can customize parts of the widget’s UI using the Templates API.

Every template provides an html function you can use as a tagged template. Using html lets you safely provide templates as an HTML string. It works directly in the browser without a build step. See Templating your UI for more information.

The html function is available starting from v4.46.0.

item
type: string|function
Optional

The template for each item. It exposes:

  • attribute: string: the name of the attribute.
  • label: string: the label for the option.
  • value: string: the encoded URL of the bounds object with a {start, end} form. This value can be used verbatim in the webpage and can be read by refine directly. If you want to inspect the value, you can do JSON.parse(window.decodeURI(value)) to get the object.
  • isRefined: boolean: whether the refinement is selected.
  • url: string: the URL with the applied refinement.
  • cssClasses: object: the CSS classes provided to the widget.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
numericMenu({
  // ...
  templates: {
    item(data, { html }) {
      return html`
        <label class="${data.cssClasses.label}">
          <input
            type="radio"
            class="${data.cssClasses.radio}"
            name="${data.attribute}"
            ${data.isRefined ? ' checked' : ''}
          />
          <span class="${data.cssClasses.labelText}">
            ${data.label}
          </span>
        </label>`;
    },
  },
});

HTML output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="ais-NumericMenu">
  <ul class="ais-NumericMenu-list">
    <li class="ais-NumericMenu-item ais-NumericMenu-item--selected">
      <label class="ais-NumericMenu-label">
        <input
          class="ais-NumericMenu-radio"
          type="radio"
          name="NumericMenu"
          checked
        />
        <span class="ais-NumericMenu-labelText">All</span>
      </label>
    </li>
    <li class="ais-NumericMenu-item">
      <label class="ais-NumericMenu-label">
        <input
          class="ais-NumericMenu-radio"
          type="radio"
          name="NumericMenu"
        />
        <span class="ais-NumericMenu-labelText">Less than 500</span>
      </label>
    </li>
  </ul>
</div>

Customize the UI with connectNumericMenu

If you want to create your own UI of the numericMenu widget, you can use connectors.

To use connectNumericMenu, you can import it with the declaration relevant to how you installed InstantSearch.js.

1
import { connectNumericMenu } from 'instantsearch.js/es/connectors';

Then it’s a 3-step process:

// 1. Create a render function
const renderNumericMenu = (renderOptions, isFirstRender) => {
  // Rendering logic
};

// 2. Create the custom widget
const customNumericMenu = connectNumericMenu(
  renderNumericMenu
);

// 3. Instantiate
search.addWidgets([
  customNumericMenu({
    // instance params
  })
]);

Create a render function

This rendering function is called before the first search (init lifecycle step) and each time results come back from Algolia (render lifecycle step).

const renderNumericMenu = (renderOptions, isFirstRender) => {
  const {
    object[] items,
    boolean canRefine,
    function refine,
    function sendEvent,
    function createURL,
    object widgetParams,
  } = renderOptions;

  if (isFirstRender) {
    // Do some initial rendering and bind events
  }

  // Render the widget
}

Rendering options

items
type: object[]

The list of available options, with each option:

  • label: string: the label for the option.
  • value: string: the encoded URL of the bounds object with the {start, end} form. This value can be used verbatim in the webpage and can be read by refine directly. If you want to inspect the value, you can do JSON.parse(window.decodeURI(value)) to get the object.
  • isRefined: boolean: whether the refinement is selected.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const renderNumericMenu = (renderOptions, isFirstRender) => {
  const { items } = renderOptions;

  document.querySelector('#numeric-menu').innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <label>
                <input
                  type="radio"
                  name="price"
                  value="${item.value}"
                  ${item.isRefined ? 'checked' : ''}
                />
                ${item.label}
              </label>
            </li>`
        )
        .join('')}
    </ul>
  `;
};
canRefine
since: v4.45.0
type: boolean

Whether the search can be refined. This is true if there are results or if a range other than “All” is selected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const renderNumericMenu = (renderOptions, isFirstRender) => {
  // `canRefine` is only available from v4.45.0
  // Use `hasNoResults` in earlier minor versions.
  const { items, canRefine } = renderOptions;

  document.querySelector('#numeric-menu').innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <label>
                <input
                  type="radio"
                  name="price"
                  value="${item.value}"
                  ${item.isRefined ? 'checked' : ''}
                  ${!canRefine ? 'disabled' : ''}
                />
                ${item.label}
              </label>
            </li>`
        )
        .join('')}
    </ul>
  `;
};
refine
type: function

Sets the selected value and triggers a new search.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const renderNumericMenu = (renderOptions, isFirstRender) => {
  const { items, refine } = renderOptions;

  const container = document.querySelector('#numeric-menu');

  container.innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <label>
                <input
                  type="radio"
                  name="price"
                  value="${item.value}"
                  ${item.isRefined ? 'checked' : ''}
                />
                ${item.label}
              </label>
            </li>`
        )
        .join('')}
    </ul>
  `;

  [...container.querySelectorAll('input')].forEach(element => {
    element.addEventListener('change', event => {
      refine(event.currentTarget.value);
    });
  });
};
sendEvent
type: (eventType, facetValue) => void

The function to send click events. The click event is automatically sent when refine is called. You can learn more about the insights middleware.

  • eventType: 'click'
  • facetValue: string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// For example,
sendEvent('click', [10, 20]);

/*
  A payload like the following will be sent to the `insights` middleware.
  {
    eventType: 'click',
    insightsMethod: 'clickedFilters',
    payload: {
      eventName: 'Filter Applied',
      filters: ['numerics<=20', 'numerics>=10'],
      index: '',
    },
    widgetType: 'ais.numericMenu',
  }
*/
createURL
type: function

Generates a URL for the next state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const renderNumericMenu = (renderOptions, isFirstRender) => {
  const { items, createURL } = renderOptions;

  document.querySelector('#numeric-menu').innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <a href="${createURL(item.value)}">${item.label}</a>
            </li>`
        )
        .join('')}
    </ul>
  `;
};
widgetParams
type: object

All original widget options forwarded to the render function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const renderNumericMenu = (renderOptions, isFirstRender) => {
  const { widgetParams } = renderOptions;

  widgetParams.container.innerHTML = '...';
};

// ...

search.addWidgets([
  customNumericMenu({
    // ...
    container: document.querySelector('#numeric-menu'),
  })
]);

Create and instantiate the custom widget

We first create custom widgets from our rendering function, then we instantiate them. When doing that, there are two types of parameters you can give:

  • Instance parameters: they are predefined parameters that you can use to configure the behavior of Algolia.
  • Your own parameters: to make the custom widget generic.

Both instance and custom parameters are available in connector.widgetParams, inside the renderFunction.

const customNumericMenu = connectNumericMenu(
  renderNumericMenu
);

search.addWidgets([
  customNumericMenu({
    attribute: string,
    items: object[],
    // Optional parameters
    transformItems: function,
  })
]);

Instance options

attribute
type: string
Required

The name of the attribute in the record.

1
2
3
4
customNumericMenu({
  // ...
  attribute: 'price',
});
items
type: object[]
Required

A list of all the options to display, with:

  • label: string: label of the option.
  • start: string: the option must be greater than or equal to start (lower bound).
  • end: string: the option must be smaller than or equal to end (upper bound).
1
2
3
4
5
6
7
8
9
customNumericMenu({
  // ...
  items: [
    { label: 'All' },
    { label: 'Less than 500$', end: 500 },
    { label: 'Between 500$ - 1000$', start: 500, end: 1000 },
    { label: 'More than 1000$', start: 1000 },
  ],
});
transformItems
type: function
default: items => items
Optional

Receives the items and is called before displaying them. Should return a new array with the same shape as the original array. Useful for transforming, removing, or reordering items.

In addition, the full results data is available, which includes all regular response parameters, as well as parameters from the helper (for example disjunctiveFacetsRefinements).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
customNumericMenu({
  // ...
  transformItems(items) {
    return items.map(item => ({
      ...item,
      label: item.label.toUpperCase(),
    }));
  },
});

/* or, combined with results */
customNumericMenu({
  // ...
  transformItems(items, { results }) {
    return items.map(item => ({
      ...item,
      label: item.isRefined && results
        ? `${item.label} (${results.nbHits} hits)`
        : item.label,
    }));
  },
});

Full example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Create the render function
const renderNumericMenu = (renderOptions, isFirstRender) => {
  // `canRefine` is only available from v4.45.0
  // Use `hasNoResults` in earlier minor versions.
  const { items, canRefine, refine, widgetParams } = renderOptions;

  widgetParams.container.innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <label>
                <input
                  type="radio"
                  name="${widgetParams.attribute}"
                  value="${item.value}"
                  ${item.isRefined ? 'checked' : ''}
                  ${!canRefine ? 'disabled' : ''}
                />
                ${item.label}
              </label>
            </li>`
        )
        .join('')}
    </ul>
  `;

  [...widgetParams.container.querySelectorAll('input')].forEach(element => {
    element.addEventListener('change', event => {
      refine(event.currentTarget.value);
    });
  });
};

// Create the custom widget
const customNumericMenu = connectNumericMenu(
  renderNumericMenu
);

// Instantiate the custom widget
search.addWidgets([
  customNumericMenu({
    container: document.querySelector('#numeric-menu'),
    attribute: 'price',
    items: [
      { label: 'All' },
      { label: 'Less than 500$', end: 500 },
      { label: 'Between 500$ - 1000$', start: 500, end: 1000 },
      { label: 'More than 1000$', start: 1000 },
    ],
  })
]);
Did you find this page helpful?