Skip to content

Commit

Permalink
feat(api): add namespace when storing widgets state
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

- our internal state shape now includes namespacing to avoid id collision. Also some existing keys were renamed:
* searchbox was using 'q' now it uses 'query'
* hitsPerPage was using 'hPP' now it uses 'hitsPerPage'
* pagination and infiniteHits were using 'p' now it uses 'page'
For more information about the state shape, please read our documentation.
  • Loading branch information
mthuret committed Nov 30, 2016
1 parent 8d7a8d0 commit 0186a6d
Show file tree
Hide file tree
Showing 33 changed files with 338 additions and 154 deletions.
2 changes: 1 addition & 1 deletion docgen/src/examples/e-commerce-infinite/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ const CustomResults = createConnector({

getProps(props, state, search) {
const noResults = search.results ? search.results.nbHits === 0 : false;
return {query: state.q, noResults};
return {query: state.query, noResults};
},
})(({noResults, query}) => {
if (noResults) {
Expand Down
2 changes: 1 addition & 1 deletion docgen/src/examples/e-commerce/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ const CustomResults = createConnector({

getProps(props, state, search) {
const noResults = search.results ? search.results.nbHits === 0 : false;
return {query: state.q, noResults};
return {query: state.query, noResults};
},
})(({noResults, query}) => {
if (noResults) {
Expand Down
2 changes: 1 addition & 1 deletion docgen/src/guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: What's react-instantsearch?
layout: main-entry.pug
category: guide
tocVisibility: true
navWeight: 7
navWeight: 1000
---

React-instantsearch is the ultimate toolbox for creating instant search experience using React and Algolia.
Expand Down
2 changes: 1 addition & 1 deletion docgen/src/guides/3rd-party-librairies.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: 3rd party UI Libraries
layout: guide.pug
category: guide
navWeight: 3
navWeight: 500
---

Even if react-instantsearch provides widgets out of the box you are free to use it with
Expand Down
2 changes: 1 addition & 1 deletion docgen/src/guides/advanced-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Advanced Topics
layout: guide.pug
category: guide
navWeight: -1
navWeight: 100
---

## How to preselect values using Virtual Widgets
Expand Down
8 changes: 4 additions & 4 deletions docgen/src/guides/conditional-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Conditional Display
layout: guide.pug
category: guide
navWeight: 1
navWeight: 300
---

Using our connector and [`createConnector`](create-own-widget.html) approach, you can
Expand All @@ -15,15 +15,15 @@ about the api.
## Displaying content when the query is empty

You can do that by using the [`createConnector`](create-own-widget.html) function and
then access the `state` of all widgets. By doing so you're able to get the query and decide what to do according to its state.
then access the [`state`](/guides/instantsearch-state.html) of all widgets. By doing so you're able to get the query and decide what to do according to its state.

Here's an example:

```javascript
const Content = createConnector({
displayName: 'ConditionalQuery',
getProps(props, state) {
return {query: state.q};
return {query: state.query};
},
})(({query}) => {
const content = query
Expand All @@ -46,7 +46,7 @@ const content = createConnector({
displayName: 'ConditionalResults',
getProps(props, state, search) {
const noResults = search.results ? search.results.nbHits === 0 : false;
return {query: state.q, noResults};
return {query: state.query, noResults};
},
})(({noResults, query}) => {
const content = noResults
Expand Down
2 changes: 1 addition & 1 deletion docgen/src/guides/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Using connectors
layout: guide.pug
category: guide
navWeight: 4
navWeight: 700
---

While react-instantsearch already provides widgets out of the box,
Expand Down
6 changes: 3 additions & 3 deletions docgen/src/guides/create-own-widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Creating widgets
layout: guide.pug
category: guide
navWeight: 2
navWeight: 400
---

If you wish to implement features that are not covered by the default widgets connectors, you will need to create your own connector via the `createConnector` method. This methods takes in a descriptor of your connector with the following properties and methods:
Expand All @@ -19,7 +19,7 @@ This method should return the props to forward to the composed component.

`props` are the props that were provided to the higher-order component.

`state` holds the state of all widgets, with the shape `{[widgetId]: widgetState}`. Stateful widgets describe the format of their state in their respective documentation entry.
`state` holds the state of all widgets. You can find the shape of all widgets state in [the corresponding guide](/guides/instantsearch-state.html).

`search` holds the search results, search errors and search loading state, with the shape `{results: ?SearchResults, error: ?Error, loading: bool}`. The `SearchResults` type is described in the [Helper's documentation](https://community.algolia.com/algoliasearch-helper-js/reference.html#searchresults).

Expand All @@ -29,7 +29,7 @@ This method should return the props to forward to the composed component.

This method defines exactly how the `refine` prop of connected widgets affects the InstantSearch state.

It takes in the current props of the higher-order component, the state of all widgets, as well as all arguments passed to the `refine` and `createURL` props of stateful widgets, and returns a new state.
It takes in the current props of the higher-order component, the [state](/guides/instantsearch-state.html) of all widgets, as well as all arguments passed to the `refine` and `createURL` props of stateful widgets, and returns a new state.

```javascript
const CoolWidget = createConnector({
Expand Down
112 changes: 112 additions & 0 deletions docgen/src/guides/instantsearch-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: InstantSearch state
layout: guide.pug
category: guide
navWeight: 600
---

The `InstantSearch` state contains all widgets states.
If a widget uses an attribute, we store it under its widget category to prevent collision.

Here's the `InstantSearch` state shape for all the connectors or widgets that we provide:

### Range state

```javascript
{
range: {
attributeName: {
min: 'min_value',
max: 'max_value'
}
}
}
```
### RefinementList state

```javascript
{
refinementList: {
attributeName: ['value']
}
}
```

### HierarchicalMenu state

```javascript
{
hierarchicalMenu: {
attributeName: 'value'
}
}
```

### HitsPerPage state

```javascript
{
hitsPerPage: 10
}
```

### Menu state

```javascript
{
menu: {
attributeName: 'value'
}
}
```

### MultiRange state

```javascript
{
multiRange: {
attributeName: 'min_value:max_value'
}
}
```
### Toggle state

```javascript
{
multiRange: {
attributeName: 'on || off'
}
}
```

### SortBy state

```javascript
{
sortBy: 'index_name'
}
```

### SearchBox state

```javascript
{
query: 'query_value'
}
```

### Pagination state

```javascript
{
page: 2
}
```

### InfiniteHits state

```javascript
{
page: 2
}
```
2 changes: 1 addition & 1 deletion docgen/src/guides/internationalization.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Internationalization
layout: guide.pug
category: guide
navWeight: 5
navWeight: 800
---

All widgets rendering text that is not otherwise configurable via props accept a `translations` prop. This prop is a mapping of keys to translation values. Translation values can be either a `String` or a `Function`, as some take parameters. The different translation keys supported by components and their optional parameters are described on their respective documentation page.
Expand Down
2 changes: 1 addition & 1 deletion docgen/src/guides/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Styling
layout: guide.pug
category: guide
navWeight: 6
navWeight: 900
---

Default widgets in react-instantsearch comes with some styling already applied and loaded. When styling components, you can decide to either extend or completely replace our default styles, using CSS class names.
Expand Down
4 changes: 2 additions & 2 deletions docgen/src/guides/using-multiple-instantsearch.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Multiple InstantSearch
layout: guide.pug
category: guide
navWeight: 0
navWeight: 200
---

You can use multiple `<InstantSearch/>` instances for cases like:
Expand All @@ -14,7 +14,7 @@ You can use multiple `<InstantSearch/>` instances for cases like:
Two props on the [InstantSearch root component](/component/InstantSearch.html) can be used to inject state or be notified of state changes:

* onStateChange(nextState): a function being called every time the `InstantSearch` state is updated.
* state: an object that is the current state of InstantSearch
* [state](/guides/instantsearch-state.html): an object that is the current state of InstantSearch

The idea is to have a main component that will receive every new state of the first instance and then pass it back to each `InstantSearch` instances.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {PropTypes} from 'react';
import {omit} from 'lodash';
import {omit, isEmpty} from 'lodash';

import createConnector from '../core/createConnector';
import {SearchParameters} from 'algoliasearch-helper';

export const getId = props => props.attributes[0];

const namespace = 'hierarchicalMenu';

function getCurrentRefinement(props, state) {
const id = getId(props);
if (typeof state[id] !== 'undefined') {
if (state[id] === '') {
if (state[namespace] && typeof state[namespace][id] !== 'undefined') {
const subState = state[namespace];
if (subState[id] === '') {
return null;
}
return state[id];
return subState[id];
}
if (props.defaultRefinement) {
return props.defaultRefinement;
Expand Down Expand Up @@ -146,12 +149,16 @@ export default createConnector({
const id = getId(props);
return {
...state,
[id]: nextRefinement || '',
[namespace]: {[id]: nextRefinement || ''},
};
},

cleanUp(props, state) {
return omit(state, getId(props));
const cleanState = omit(state, `${namespace}.${getId(props)}`);
if (isEmpty(cleanState[namespace])) {
return omit(cleanState, namespace);
}
return cleanState;
},

getSearchParameters(searchParameters, props, state) {
Expand Down Expand Up @@ -206,7 +213,7 @@ export default createConnector({
attributeName: rootAttribute,
value: nextState => ({
...nextState,
[id]: '',
[namespace]: {[id]: ''},
}),
currentRefinement,
}],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('connectHierarchicalMenu', () => {
};

results.getFacetValues.mockImplementationOnce(() => ({}));
props = getProps({attributes: ['ok']}, {ok: 'wat'}, {results});
props = getProps({attributes: ['ok']}, {hierarchicalMenu: {ok: 'wat'}}, {results});
expect(props).toEqual({items: [], currentRefinement: 'wat'});

results.getFacetValues.mockImplementationOnce(() => ({}));
Expand Down Expand Up @@ -134,7 +134,7 @@ describe('connectHierarchicalMenu', () => {
const nextState = refine({attributes: ['ok']}, {otherKey: 'val'}, 'yep');
expect(nextState).toEqual({
otherKey: 'val',
ok: 'yep',
hierarchicalMenu: {ok: 'yep'},
});
});

Expand Down Expand Up @@ -177,7 +177,7 @@ describe('connectHierarchicalMenu', () => {
rootPath: 'ROOT_PATH',
showParentLevel: true,
limitMin: 1,
}, {ATTRIBUTE: 'ok'});
}, {hierarchicalMenu: {ATTRIBUTE: 'ok'}});
expect(params).toEqual(
initSP
.addHierarchicalFacet({
Expand All @@ -198,7 +198,7 @@ describe('connectHierarchicalMenu', () => {
});

it('registers its filter in metadata', () => {
const metadata = getMetadata({attributes: ['ok']}, {ok: 'wat'});
const metadata = getMetadata({attributes: ['ok']}, {hierarchicalMenu: {ok: 'wat'}});
expect(metadata).toEqual({
id: 'ok',
items: [{
Expand All @@ -210,12 +210,18 @@ describe('connectHierarchicalMenu', () => {
}],
});

const state = metadata.items[0].value({ok: 'wat'});
expect(state).toEqual({ok: ''});
const state = metadata.items[0].value({hierarchicalMenu: {ok: 'wat'}});
expect(state).toEqual({hierarchicalMenu: {ok: ''}});
});

it('should return the right state when clean up', () => {
const state = cleanUp({attributes: ['name']}, {name: {state: 'state'}, another: {state: 'state'}});
let state = cleanUp({attributes: ['name']}, {
hierarchicalMenu: {name: 'state', name2: 'state'},
another: {state: 'state'},
});
expect(state).toEqual({hierarchicalMenu: {name2: 'state'}, another: {state: 'state'}});

state = cleanUp({attributes: ['name2']}, state);
expect(state).toEqual({another: {state: 'state'}});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import createConnector from '../core/createConnector';
import {omit} from 'lodash';

function getId() {
return 'hPP';
return 'hitsPerPage';
}

function getCurrentRefinement(props, state) {
Expand Down
Loading

0 comments on commit 0186a6d

Please sign in to comment.