Skip to content

Commit

Permalink
Added batch actions and the new "shouldClearFilters" option (#391)
Browse files Browse the repository at this point in the history
- Actions called one after another will now both be applied, and consolidated into a single API request
- There is now a "shouldClearFilters" flag available on `setSearchTerm` and `SearchBox` which controls whether or not filters will be cleared when the search term is changed.
  • Loading branch information
JasonStoltz authored Sep 26, 2019
1 parent c14cc70 commit 0ef3535
Show file tree
Hide file tree
Showing 21 changed files with 498 additions and 87 deletions.
60 changes: 37 additions & 23 deletions ADVANCED.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/react-search-ui-views/src/SearchBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ function SearchBox(props) {
onSubmit,
useAutocomplete,
value,
// NOTE: These are explicitly de-structured but not used so that they are
// not passed through to the input with the 'rest' parameter
// eslint-disable-next-line no-unused-vars
autocompletedResults,
// eslint-disable-next-line no-unused-vars
Expand Down
18 changes: 13 additions & 5 deletions packages/react-search-ui/src/containers/SearchBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class SearchBoxContainer extends Component {
]),
autocompleteView: PropTypes.func,
className: PropTypes.string,
shouldClearFilters: PropTypes.bool,
debounceLength: PropTypes.number,
inputProps: PropTypes.object,
inputView: PropTypes.func,
Expand All @@ -51,7 +52,8 @@ export class SearchBoxContainer extends Component {
};

static defaultProps = {
autocompleteMinimumCharacters: 0
autocompleteMinimumCharacters: 0,
shouldClearFilters: true
};

state = {
Expand All @@ -71,22 +73,27 @@ export class SearchBoxContainer extends Component {
};

completeSuggestion = searchTerm => {
const { setSearchTerm } = this.props;
setSearchTerm(searchTerm);
const { shouldClearFilters, setSearchTerm } = this.props;
setSearchTerm(searchTerm, {
shouldClearFilters
});
};

handleSubmit = e => {
const { searchTerm, setSearchTerm } = this.props;
const { shouldClearFilters, searchTerm, setSearchTerm } = this.props;

e.preventDefault();
setSearchTerm(searchTerm);
setSearchTerm(searchTerm, {
shouldClearFilters
});
};

handleChange = value => {
const {
autocompleteMinimumCharacters,
autocompleteResults,
autocompleteSuggestions,
shouldClearFilters,
searchAsYouType,
setSearchTerm,
debounceLength
Expand All @@ -99,6 +106,7 @@ export class SearchBoxContainer extends Component {
searchAsYouType) && {
debounce: debounceLength || 200
}),
shouldClearFilters,
refresh: !!searchAsYouType,
autocompleteResults: !!autocompleteResults,
autocompleteSuggestions: !!autocompleteSuggestions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,65 @@ describe("useAutocomplete", () => {
});
});

describe("shouldClearFilters prop", () => {
it("will be passed through to setSearchTerm on submit", () => {
let viewProps;

shallow(
<SearchBoxContainer
{...params}
shouldClearFilters={false}
view={props => (viewProps = props)}
/>
);

const { onSubmit } = viewProps;
onSubmit({
preventDefault: () => {}
});
const call = params.setSearchTerm.mock.calls[0];
expect(call[1].shouldClearFilters).toEqual(false);
});

it("will be passed through to setSearchTerm on change", () => {
let viewProps;

shallow(
<SearchBoxContainer
{...params}
shouldClearFilters={false}
view={props => (viewProps = props)}
/>
);

const { onChange } = viewProps;
onChange("new term");
const call = params.setSearchTerm.mock.calls[0];
expect(call[1].shouldClearFilters).toEqual(false);
});

it("will call setSearchTerm if no onSelectAutocomplete is specified and a suggestion is selected", () => {
let viewProps;

shallow(
<SearchBoxContainer
{...params}
autocompleteResults={true}
shouldClearFilters={false}
view={props => (viewProps = props)}
/>
);

const { onSelectAutocomplete } = viewProps;
onSelectAutocomplete({
suggestion: "bird"
});

const call = params.setSearchTerm.mock.calls[0];
expect(call[1].shouldClearFilters).toEqual(false);
});
});

it("will call back to setSearchTerm with refresh: false when input is changed", () => {
let viewProps;
shallow(
Expand All @@ -220,6 +279,7 @@ it("will call back to setSearchTerm with refresh: false when input is changed",
refresh: false,
autocompleteResults: false,
autocompleteSuggestions: false,
shouldClearFilters: true,
autocompleteMinimumCharacters: 0
}
]);
Expand All @@ -243,6 +303,7 @@ it("will call back to setSearchTerm with autocompleteMinimumCharacters setting",
refresh: false,
autocompleteResults: false,
autocompleteSuggestions: false,
shouldClearFilters: true,
autocompleteMinimumCharacters: 3
}
]);
Expand Down Expand Up @@ -270,6 +331,7 @@ it("will call back to setSearchTerm with refresh: true when input is changed and
debounce: 200,
autocompleteResults: false,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteSuggestions: false
}
]);
Expand Down Expand Up @@ -298,6 +360,7 @@ it("will call back to setSearchTerm with a specific debounce when input is chang
debounce: 500,
autocompleteResults: false,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteSuggestions: false
}
]);
Expand Down Expand Up @@ -329,6 +392,7 @@ it("will call back to setSearchTerm with a specific debounce when input is chang
debounce: 500,
autocompleteResults: true,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteSuggestions: false
}
]);
Expand Down Expand Up @@ -357,6 +421,7 @@ it("will call back to setSearchTerm with a specific debounce when input is chang
debounce: 500,
autocompleteSuggestions: true,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteResults: false
}
]);
Expand All @@ -378,7 +443,7 @@ it("will call back setSearchTerm with refresh: true when form is submitted", ()
});

const call = params.setSearchTerm.mock.calls[0];
expect(call).toEqual(["a term"]);
expect(call).toEqual(["a term", { shouldClearFilters: true }]);
});

describe("onSelectAutocomplete", () => {
Expand Down Expand Up @@ -420,6 +485,25 @@ describe("onSelectAutocomplete", () => {
expect(passedAutocompleteResults).toBeDefined();
expect(passedDefaultOnSelectAutocomplete).toBeDefined();
});

it("will call setSearchTerm if no onSelectAutocomplete is specified and a suggestion is selected", () => {
let viewProps;

shallow(
<SearchBoxContainer
{...params}
autocompleteResults={true}
view={props => (viewProps = props)}
/>
);
const { onSelectAutocomplete } = viewProps;
onSelectAutocomplete({
suggestion: "bird"
});

const call = params.setSearchTerm.mock.calls[0];
expect(call[0]).toEqual("bird");
});
});

describe("autocomplete clickthroughs", () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/search-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,10 @@
"deep-equal": "^1.0.1",
"history": "^4.9.0",
"qs": "^6.7.0"
},
"jest": {
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
]
}
}
74 changes: 60 additions & 14 deletions packages/search-ui/src/DebounceManager.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,76 @@
import debounceFn from "debounce-fn";

export default class DebounceManager {
class DebounceManager {
debounceCache = {};

/*
The purpose of this is to:
Dynamically debounce and cache a debounced version of a function at the time of calling that function. This avoids
managing debounced version of functions locally.
Assumption:
Functions are debounced on a combination of unique function and wait times. So debouncing won't work on
subsequent calls with different wait times or different functions. That also means that the debounce manager
can be used for different functions in parallel, and keep the two functions debounced separately.
*/
runWithDebounce(wait, fn, ...parameters) {
/**
* Dynamically debounce and cache a debounced version of a function at the time of calling that function. This avoids
* managing debounced version of functions locally.
*
* In other words, debounce usually works by debouncing based on
* referential identity of a function. This works by comparing provided function names.
*
* This also has the ability to short-circuit a debounce all-together, if no wait
* time is provided.
*
* Assumption:
* Functions are debounced on a combination of unique function name and wait times. So debouncing won't work on
* subsequent calls with different wait times or different functions. That also means that the debounce manager
* can be used for different functions in parallel, and keep the two functions debounced separately.
*
* @param {number} wait Milliseconds to debounce. Executes immediately if falsey.
* @param {function} fn Function to debounce
* @param {function} functionName Name of function to debounce, used to create a unique key
* @param {...any} parameters Parameters to pass to function
*/
runWithDebounce(wait, functionName, fn, ...parameters) {
if (!wait) {
return fn(...parameters);
}

const key = fn.toString() + wait.toString();
const key = `${functionName}|${wait.toString()}`;
let debounced = this.debounceCache[key];
if (!debounced) {
this.debounceCache[key] = debounceFn(fn, { wait });
debounced = this.debounceCache[key];
}
debounced(...parameters);
}

/**
* Cancels existing debounced function calls.
*
* This will cancel any debounced function call, regardless of the debounce length that was provided.
*
* For example, making the following series of calls will create multiple debounced functions, because
* they are cached by a combination of unique name and debounce length.
*
* runWithDebounce(1000, "_updateSearchResults", this._updateSearchResults)
* runWithDebounce(500, "_updateSearchResults", this._updateSearchResults)
* runWithDebounce(1000, "_updateSearchResults", this._updateSearchResults)
*
* Calling the following will cancel all of those, if they have not yet executed:
*
* cancelByName("_updateSearchResults")
*
* @param {string} functionName The name of the function that was debounced. This needs to match exactly what was provided
* when runWithDebounce was called originally.
*/
cancelByName(functionName) {
Object.entries(this.debounceCache)
.filter(([cachedKey]) => cachedKey.startsWith(`${functionName}|`))
// eslint-disable-next-line no-unused-vars
.forEach(([_, cachedValue]) => cachedValue.cancel());
}
}
/**
* Perform a standard debounce
*
* @param {number} wait Milliseconds to debounce. Executes immediately if falsey.
* @param {function} fn Function to debounce
*/
DebounceManager.debounce = (wait, fn) => {
return debounceFn(fn, { wait });
};

export default DebounceManager;
Loading

0 comments on commit 0ef3535

Please sign in to comment.