Originally built for a talk at UtahJS Conf. The Pure Typeahead is a composable typeahead component for React that allows you to build whatever kind of typeahead experience you need without configuration.
$ yarn add pure-typeahead
or
$ npm install --save pure-typeahead
In your application JavaScript file:
import { Typeahead, TypeaheadInput, TypeaheadResultsList, TypeaheadResult } from 'pure-typeahead';
Due to vast styling difference between typeahead experiences, this typeahead does not provide any styles out of the box. Please see demo/src/styles/index.js
for an example of how the typeahead can be styled.
This package has no dependencies, but does have React 16 as a peer dependency. The component will not work with lower versions of React.
This component consists of four React components: Typeahead
, TypeaheadInput
, TypeaheadResultsList
, and TypeaheadResult
.
The Typeahead
component wraps the other, more functional component. It's purpose is to orchestrate the interactions of its children components. The Typeahead
component needs a TypeaheadInput
component and a TypeaheadResultsList
component as direct children. It can have other children as well. For CSS targeting purposes, the Typeahead
component produces a custom element tag, <pure-typeahead>
, in the rendered DOM.
Name | Required | Type | Default Value | Description |
---|---|---|---|---|
onDismiss | optional | function | No-op function | This function is called when the typeahead is dismissed when the user presses the escape key |
onBlur | optional | function | No-op function | This function is called when the entire typeahead component is blurred. If you want to listen to blurs on the input, this is the place to do it |
The TypeaheadInput
component is the input where the user will type their typeahead query. For accessibility to work properly, you need to use TypeaheadInput
instead of a plain <input>
element.
Name | Required | Type | Default Value | Description |
---|---|---|---|---|
value | required | string | N/A | The value of the input. This input is a controlled component, so the value must be provided and updated through onChange . |
type | optional | string | 'text' | The type of input to be used. Don't use 'checkbox' or 'radio' and expect this thing to work. |
placeholder | optional | string | none | The placeholder text to be placed in the input. |
onChange | required | function | N/A | The function to be called when the value of the input changes. This is where you will likely query for new typeahead results. You also must update the value prop within this function. |
onKeyDown | optional | function | No-op function | Passes through the input's onKeyDown function |
onKeyUp | optional | function | No-op function | Passes through the input's onKeyUp function |
onBlur | optional | function | No-op function | Passes through the input's onBlur function |
onFocus | optional | function | No-op function | Passes through the input's onFocus function |
The TypeaheadResultsList
component wraps the TypeaheadResult
components. It expects to have TypeaheadResult
components as direct children, but can also have other components as children. For example, you can add <h3>
s as headers between groups of results. You can also add an <svg>
as a loading spinner, or a <p>
describing that no results were found. Only TypeaheadResult
components will be navigable with the arrow keys and selectable with the mouse. Other children will be skipped.
This component, which must be a direct child of the TypeaheadResultsList
component, is where you describe one of the results of the typeahead. You can add anything you want as a child of this component, including text, images, and buttons.
Name | Required | Type | Default Value | Description |
---|---|---|---|---|
onSelect | required | function | N/A | This function will be called when the typeahead is selected, whether by click or by keyboard interaction. |
onHighlight | optional | function | No-op function | This function will be called when the typeahead is highlighted through keyboard interaction |
The typeahead doesn't do much on its own; it must be properly composed to work correctly. As such, some examples are in order.
import React, { Component } from 'react';
import { cities } from './cities';
import Typeahead from '../../src/Typeahead';
import TypeaheadInput from '../../src/TypeaheadInput';
import TypeaheadResultsList from '../../src/TypeaheadResultsList';
import TypeaheadResult from '../../src/TypeaheadResult';
class Demo extends Component {
state = {
results: [],
taValue: '',
selectedResult: null
}
typeaheadInputChange(evt) {
const { value: str } = evt.target;
const results = cities.filter(
city => city.name.match(new RegExp(str, 'i'))
);
this.setState({
taValue: str,
results
});
}
resultSelected(result) {
this.setState({
selectedResult: result,
results: [],
taValue: ''
});
}
render() {
const { selectedResult } = this.state;
return [
<Typeahead key="typeahead">
<TypeaheadInput
value={this.state.taValue}
onChange={(evt)=> this.typeaheadInputChange(evt)}
/>
<TypeaheadResultsList>
{this.state.results.map(result => (
<TypeaheadResult
onSelect={() => this.resultSelected(result)}
>
{result.name}
</TypeaheadResult>
))}
</TypeaheadResultsList>
</Typeahead>,
(selectedResult &&
<div key="selected-result">
<h2>{selectedResult.name}</h2>
<h3>{selectedResult.county} County</h3>
<h3>{selectedResult.population} pop.</h3>
</div>
)
];
}
}
export default Demo;
import React, { Component } from 'react';
import { cities } from './cities';
import Typeahead from '../../src/Typeahead';
import TypeaheadInput from '../../src/TypeaheadInput';
import TypeaheadResultsList from '../../src/TypeaheadResultsList';
import TypeaheadResult from '../../src/TypeaheadResult';
class Demo extends Component {
state = {
results: [],
taValue: '',
selectedResult: null
}
typeaheadInputChange(evt) {
const { value: str } = evt.target;
const results = cities.filter(
city => city.name.match(new RegExp(str, 'i'))
);
this.setState({
taValue: str,
results
});
}
resultSelected(result) {
this.setState({
selectedResult: result,
results: [],
taValue: ''
});
}
render() {
const { results, selectedResult } = this.state;
// Group cities by county
const counties = results.reduce((acc, city) => {
if (acc[city.county]) {
acc[city.county].push(city);
} else {
acc[city.county] = [city];
}
return acc;
}, {});
return [
<Typeahead key="typeahead">
<TypeaheadInput
value={this.state.taValue}
onChange={(evt)=> this.typeaheadInputChange(evt)}
/>
<TypeaheadResultsList>
{Object.keys(counties).reduce((arr, county) => {
return [
...arr,
// Display county name at the top of each county group
(<h3 key={county}>{county}</h3>),
...(counties[county].map(city => (
<TypeaheadResult onSelect={() => this.resultSelected(city)}>{city.name}</TypeaheadResult>
)))
]
}, [])}
</TypeaheadResultsList>
</Typeahead>,
(selectedResult &&
<div key="selected-result">
<h2>{selectedResult.name}</h2>
<h3>{selectedResult.county} County</h3>
<h3>{selectedResult.population} pop.</h3>
</div>
)
];
}
}
export default Demo;