Skip to content
This repository was archived by the owner on Dec 30, 2022. It is now read-only.

Commit 718c7f9

Browse files
chore(examples): add Hooks example (#3155)
1 parent 0385cd9 commit 718c7f9

19 files changed

+2748
-71
lines changed

.codesandbox/ci.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"buildCommand": "lerna run build --scope react-* --ignore *-maps --ignore *-native",
55
"sandboxes": [
66
"github/algolia/create-instantsearch-app/tree/templates/react-instantsearch",
7-
"github/algolia/doc-code-samples/tree/master/React InstantSearch/routing-basic"
7+
"github/algolia/doc-code-samples/tree/master/React InstantSearch/routing-basic",
8+
"/examples/hooks"
89
]
910
}

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ build/
1010
preview.zip
1111
.happypack/
1212
/coverage
13+
/.parcel-cache
1314

1415
# Generated files
1516
/website/examples

examples/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
'import/no-unresolved': 'off',
77
'import/named': 'off',
88
'react/prop-types': 'off',
9+
'@typescript-eslint/consistent-type-imports': ['off'],
910
'@typescript-eslint/no-use-before-define': ['off'],
1011
},
1112
};

examples/hooks/App.css

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
html,
6+
body {
7+
margin: 0;
8+
padding: 0;
9+
}
10+
11+
body {
12+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
13+
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
14+
padding: 0.5rem;
15+
max-width: 1000px;
16+
margin: 0 auto;
17+
}

examples/hooks/App.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Hit as AlgoliaHit } from '@algolia/client-search';
2+
import algoliasearch from 'algoliasearch/lite';
3+
import React from 'react';
4+
import { InstantSearch } from 'react-instantsearch-hooks';
5+
6+
import { Hits, SearchBox, RefinementList } from './components';
7+
8+
import './App.css';
9+
10+
const searchClient = algoliasearch(
11+
'latency',
12+
'6be0576ff61c053d5f9a3225e2a90f76'
13+
);
14+
15+
type HitProps = {
16+
hit: AlgoliaHit<{
17+
name: string;
18+
}>;
19+
};
20+
21+
function Hit({ hit }: HitProps) {
22+
return (
23+
<span
24+
dangerouslySetInnerHTML={{
25+
__html: (hit._highlightResult as any).name.value,
26+
}}
27+
/>
28+
);
29+
}
30+
31+
export function App() {
32+
return (
33+
<InstantSearch searchClient={searchClient} indexName="instant_search">
34+
<div
35+
style={{
36+
display: 'grid',
37+
alignItems: 'flex-start',
38+
gridTemplateColumns: '200px 1fr',
39+
gap: '0.5rem',
40+
}}
41+
>
42+
<div>
43+
<RefinementList
44+
attribute="brand"
45+
searchable={true}
46+
searchablePlaceholder="Search brands"
47+
showMore={true}
48+
/>
49+
</div>
50+
<div style={{ display: 'grid', gap: '.5rem' }}>
51+
<SearchBox placeholder="Search" />
52+
<Hits hitComponent={Hit} />
53+
</div>
54+
</div>
55+
</InstantSearch>
56+
);
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import React, { ChangeEvent, FormEvent, RefObject } from 'react';
2+
3+
export type ControlledSearchBoxProps = React.ComponentProps<'div'> & {
4+
inputRef: RefObject<HTMLInputElement>;
5+
isSearchStalled: boolean;
6+
onChange(event: ChangeEvent): void;
7+
onReset(event: FormEvent): void;
8+
onSubmit?(event: FormEvent): void;
9+
placeholder?: string;
10+
value: string;
11+
};
12+
13+
export function ControlledSearchBox({
14+
inputRef,
15+
isSearchStalled,
16+
onChange,
17+
onReset,
18+
onSubmit,
19+
placeholder,
20+
value,
21+
...props
22+
}: ControlledSearchBoxProps) {
23+
function handleSubmit(event: FormEvent) {
24+
event.preventDefault();
25+
event.stopPropagation();
26+
27+
if (onSubmit) {
28+
onSubmit(event);
29+
}
30+
31+
if (inputRef.current) {
32+
inputRef.current.blur();
33+
}
34+
}
35+
36+
function handleReset(event: FormEvent) {
37+
event.preventDefault();
38+
event.stopPropagation();
39+
40+
onReset(event);
41+
42+
if (inputRef.current) {
43+
inputRef.current.focus();
44+
}
45+
}
46+
47+
return (
48+
<div className="ais-SearchBox" {...props}>
49+
<form
50+
action=""
51+
className="ais-SearchBox-form"
52+
noValidate
53+
onSubmit={handleSubmit}
54+
onReset={handleReset}
55+
>
56+
<input
57+
ref={inputRef}
58+
className="ais-SearchBox-input"
59+
autoComplete="off"
60+
autoCorrect="off"
61+
autoCapitalize="off"
62+
placeholder={placeholder}
63+
spellCheck={false}
64+
maxLength={512}
65+
type="search"
66+
value={value}
67+
onChange={onChange}
68+
/>
69+
<button
70+
className="ais-SearchBox-submit"
71+
type="submit"
72+
title="Submit the search query."
73+
>
74+
<svg
75+
className="ais-SearchBox-submitIcon"
76+
width="10"
77+
height="10"
78+
viewBox="0 0 40 40"
79+
>
80+
<path d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z"></path>
81+
</svg>
82+
</button>
83+
<button
84+
className="ais-SearchBox-reset"
85+
type="reset"
86+
title="Clear the search query."
87+
hidden={value.length === 0 && !isSearchStalled}
88+
>
89+
<svg
90+
className="ais-SearchBox-resetIcon"
91+
viewBox="0 0 20 20"
92+
width="10"
93+
height="10"
94+
>
95+
<path d="M8.114 10L.944 2.83 0 1.885 1.886 0l.943.943L10 8.113l7.17-7.17.944-.943L20 1.886l-.943.943-7.17 7.17 7.17 7.17.943.944L18.114 20l-.943-.943-7.17-7.17-7.17 7.17-.944.943L0 18.114l.943-.943L8.113 10z"></path>
96+
</svg>
97+
</button>
98+
<span
99+
className="ais-SearchBox-loadingIndicator"
100+
hidden={!isSearchStalled}
101+
>
102+
<svg
103+
width="16"
104+
height="16"
105+
viewBox="0 0 38 38"
106+
stroke="#444"
107+
className="ais-SearchBox-loadingIcon"
108+
>
109+
<g fill="none" fillRule="evenodd">
110+
<g transform="translate(1 1)" strokeWidth="2">
111+
<circle strokeOpacity=".5" cx="18" cy="18" r="18" />
112+
<path d="M36 18c0-9.94-8.06-18-18-18">
113+
<animateTransform
114+
attributeName="transform"
115+
type="rotate"
116+
from="0 18 18"
117+
to="360 18 18"
118+
dur="1s"
119+
repeatCount="indefinite"
120+
/>
121+
</path>
122+
</g>
123+
</g>
124+
</svg>
125+
</span>
126+
</form>
127+
</div>
128+
);
129+
}

examples/hooks/components/Hits.tsx

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Hit as AlgoliaHit } from '@algolia/client-search';
2+
import React from 'react';
3+
import { useHits, UseHitsProps } from 'react-instantsearch-hooks';
4+
5+
import { cx } from '../cx';
6+
7+
export type HitsProps = React.ComponentProps<'div'> &
8+
UseHitsProps & {
9+
hitComponent: <THit extends AlgoliaHit<Record<string, unknown>>>(props: {
10+
hit: THit;
11+
}) => JSX.Element;
12+
};
13+
14+
export function Hits({ hitComponent: Hit, ...props }: HitsProps) {
15+
const { hits } = useHits(props);
16+
17+
return (
18+
<div className={cx('ais-Hits', props.className)}>
19+
<ol className="ais-Hits-list">
20+
{hits.map((hit) => (
21+
<li key={hit.objectID} className="ais-Hits-item">
22+
<Hit hit={hit} />
23+
</li>
24+
))}
25+
</ol>
26+
</div>
27+
);
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { useEffect, useRef, useState, ChangeEvent } from 'react';
2+
import {
3+
useRefinementList,
4+
UseRefinementListProps,
5+
} from 'react-instantsearch-hooks';
6+
7+
import { ControlledSearchBox } from './ControlledSearchBox';
8+
import { cx } from '../cx';
9+
10+
export type RefinementListProps = React.ComponentProps<'div'> &
11+
UseRefinementListProps;
12+
13+
export function RefinementList(props: RefinementListProps) {
14+
const {
15+
canToggleShowMore,
16+
isFromSearch,
17+
isShowingMore,
18+
items,
19+
refine,
20+
searchForItems,
21+
toggleShowMore,
22+
} = useRefinementList(props);
23+
const [query, setQuery] = useState('');
24+
const inputRef = useRef<HTMLInputElement>(null);
25+
26+
useEffect(() => {
27+
searchForItems(query);
28+
// eslint-disable-next-line react-hooks/exhaustive-deps
29+
}, [query]);
30+
31+
return (
32+
<div className={cx('ais-RefinementList', props.className)}>
33+
{props.searchable && (
34+
<div className="ais-RefinementList-searchBox">
35+
<ControlledSearchBox
36+
inputRef={inputRef}
37+
placeholder={props.searchablePlaceholder}
38+
isSearchStalled={false}
39+
onChange={(event: ChangeEvent<HTMLInputElement>) => {
40+
setQuery(event.currentTarget.value);
41+
}}
42+
onReset={() => {
43+
setQuery('');
44+
}}
45+
onSubmit={() => {
46+
if (items.length > 0) {
47+
refine(items[0].value);
48+
setQuery('');
49+
}
50+
}}
51+
value={query}
52+
/>
53+
</div>
54+
)}
55+
{props.searchable && isFromSearch && items.length === 0 && (
56+
<div className="ais-RefinementList-noResults">No results.</div>
57+
)}
58+
59+
<ul className="ais-RefinementList-list">
60+
{items.map((item) => (
61+
<li
62+
key={item.value}
63+
className={cx(
64+
'ais-RefinementList-item',
65+
item.isRefined && 'ais-RefinementList-item--selected'
66+
)}
67+
>
68+
<label className="ais-RefinementList-label">
69+
<input
70+
className="ais-RefinementList-checkbox"
71+
type="checkbox"
72+
value={item.value}
73+
checked={item.isRefined}
74+
onChange={() => {
75+
refine(item.value);
76+
setQuery('');
77+
}}
78+
/>
79+
<span
80+
className="ais-RefinementList-labelText"
81+
dangerouslySetInnerHTML={{ __html: item.highlighted }}
82+
/>
83+
<span className="ais-RefinementList-count">{item.count}</span>
84+
</label>
85+
</li>
86+
))}
87+
</ul>
88+
89+
{props.showMore && (
90+
<button
91+
className={cx(
92+
'ais-RefinementList-showMore',
93+
!canToggleShowMore && 'ais-RefinementList-showMore--disabled'
94+
)}
95+
disabled={!canToggleShowMore}
96+
onClick={toggleShowMore}
97+
>
98+
{isShowingMore ? 'Show less' : 'Show more'}
99+
</button>
100+
)}
101+
</div>
102+
);
103+
}

0 commit comments

Comments
 (0)