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

Commit 1217694

Browse files
authored
feat(DynamicWidgets): add fallbackComponent (#3066)
* feat(DynamicWidgets): add fallbackWidget This was brought up by @Shipow as a possible use case: you want to have dynamic widgets, but don't know what the facets could be in advance. The fallbackWidget prop caters for this use case, allowing you to special-case some widgets (like hierarchical for example), but use the same widget for the rest. ```jsx <ExperimentalDynamicWidgets fallbackWidget={RefinementList}> <HierarchicalMenu attributes={['hierarchy', 'subHierarchy']} /> </ExperimentalDynamicWidgets> <ExperimentalDynamicWidgets fallbackWidget={({attribute}) => <CustomWidget facet={attribute} />}> <HierarchicalMenu attributes={['hierarchy', 'subHierarchy']} /> </ExperimentalDynamicWidgets> ``` * replace to fallbackComponent
1 parent 731d9ba commit 1217694

File tree

2 files changed

+116
-7
lines changed

2 files changed

+116
-7
lines changed

packages/react-instantsearch-core/src/widgets/DynamicWidgets.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Fragment, ReactChild, ReactNode } from 'react';
1+
import React, { Fragment, ReactChild, ComponentType, ReactNode } from 'react';
22
import { getDisplayName } from '../core/utils';
33
import connectDynamicWidgets from '../connectors/connectDynamicWidgets';
44

@@ -23,9 +23,14 @@ function getAttribute(component: ReactChild): string | undefined {
2323
type DynamicWidgetsProps = {
2424
children: ReactNode;
2525
attributesToRender: string[];
26+
fallbackComponent?: ComponentType<{ attribute: string }>;
2627
};
2728

28-
function DynamicWidgets({ children, attributesToRender }: DynamicWidgetsProps) {
29+
function DynamicWidgets({
30+
children,
31+
attributesToRender,
32+
fallbackComponent: Fallback = () => null,
33+
}: DynamicWidgetsProps) {
2934
const widgets: Map<string, ReactChild> = new Map();
3035

3136
React.Children.forEach(children, child => {
@@ -43,7 +48,9 @@ function DynamicWidgets({ children, attributesToRender }: DynamicWidgetsProps) {
4348
return (
4449
<>
4550
{attributesToRender.map(attribute => (
46-
<Fragment key={attribute}>{widgets.get(attribute)}</Fragment>
51+
<Fragment key={attribute}>
52+
{widgets.get(attribute) || <Fallback attribute={attribute} />}
53+
</Fragment>
4754
))}
4855
</>
4956
);

packages/react-instantsearch-core/src/widgets/__tests__/DynamicWidgets.test.tsx

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import { ErrorBoundary } from 'react-error-boundary';
44
import {
55
connectHierarchicalMenu,
6+
connectMenu,
67
connectPagination,
78
connectRefinementList,
89
} from '../..';
@@ -39,7 +40,12 @@ const RefinementList = connectRefinementList(
3940
);
4041

4142
const HierarchicalMenu = connectHierarchicalMenu(
42-
({ attributes }) => `HierarchicalMenu(${attributes.join(',')})`
43+
({ attributes }) => `HierarchicalMenu([${attributes.join(', ')}])`
44+
);
45+
46+
const Menu = connectMenu(
47+
({ attribute, otherProp }) =>
48+
`Menu(${attribute})${otherProp ? ` ${JSON.stringify({ otherProp })}` : ''}`
4349
);
4450

4551
const Pagination = connectPagination(() => {
@@ -184,7 +190,7 @@ describe('DynamicWidgets', () => {
184190
expect(container).toMatchInlineSnapshot(`
185191
<div>
186192
RefinementList(test1)
187-
HierarchicalMenu(test2,test3)
193+
HierarchicalMenu([test2, test3])
188194
</div>
189195
`);
190196
}
@@ -198,7 +204,7 @@ describe('DynamicWidgets', () => {
198204

199205
expect(container).toMatchInlineSnapshot(`
200206
<div>
201-
HierarchicalMenu(test2,test3)
207+
HierarchicalMenu([test2, test3])
202208
RefinementList(test1)
203209
</div>
204210
`);
@@ -229,7 +235,7 @@ describe('DynamicWidgets', () => {
229235
// @ts-ignore resultsState in InstantSearch is typed wrongly to deal with multi-index
230236
resultsState={resultsState}
231237
>
232-
<DynamicWidgets transformItems={() => ['test1', 'test3']}>
238+
<DynamicWidgets transformItems={() => ['test1', 'test3', 'test5']}>
233239
<RefinementList attribute="test1" />
234240
<RefinementList attribute="test2" />
235241
<Panel>
@@ -238,6 +244,12 @@ describe('DynamicWidgets', () => {
238244
<Panel>
239245
<RefinementList attribute="test4" />
240246
</Panel>
247+
<Panel>
248+
<HierarchicalMenu attributes={['test5', 'test5.1']} />
249+
</Panel>
250+
<Panel>
251+
<HierarchicalMenu attributes={['test6', 'test6.1']} />
252+
</Panel>
241253
</DynamicWidgets>
242254
</InstantSearch>
243255
);
@@ -254,6 +266,15 @@ describe('DynamicWidgets', () => {
254266
RefinementList(test3)
255267
</div>
256268
</div>
269+
<div
270+
class="ais-Panel"
271+
>
272+
<div
273+
class="ais-Panel-body"
274+
>
275+
HierarchicalMenu([test5, test5.1])
276+
</div>
277+
</div>
257278
</div>
258279
`);
259280
});
@@ -327,5 +348,86 @@ describe('DynamicWidgets', () => {
327348
`[Error: Could not find "attribute" prop for UnknownComponent.]`
328349
);
329350
});
351+
352+
test('does not render attributes without widget by default', () => {
353+
const searchClient = createSearchClient();
354+
355+
const { container } = render(
356+
<InstantSearch
357+
searchClient={searchClient}
358+
indexName="test"
359+
// @ts-ignore resultsState in InstantSearch is typed wrongly to deal with multi-index
360+
resultsState={resultsState}
361+
>
362+
<DynamicWidgets transformItems={() => ['test1', 'test2', 'test3']}>
363+
<RefinementList attribute="test1" />
364+
</DynamicWidgets>
365+
</InstantSearch>
366+
);
367+
368+
expect(container).toMatchInlineSnapshot(`
369+
<div>
370+
RefinementList(test1)
371+
</div>
372+
`);
373+
});
374+
375+
test("uses fallbackComponent component to create widgets that aren't explicitly declared", () => {
376+
const searchClient = createSearchClient();
377+
378+
const { container } = render(
379+
<InstantSearch
380+
searchClient={searchClient}
381+
indexName="test"
382+
// @ts-ignore resultsState in InstantSearch is typed wrongly to deal with multi-index
383+
resultsState={resultsState}
384+
>
385+
<DynamicWidgets
386+
transformItems={() => ['test1', 'test2', 'test3']}
387+
fallbackComponent={Menu}
388+
>
389+
<RefinementList attribute="test1" />
390+
</DynamicWidgets>
391+
</InstantSearch>
392+
);
393+
394+
expect(container).toMatchInlineSnapshot(`
395+
<div>
396+
RefinementList(test1)
397+
Menu(test2)
398+
Menu(test3)
399+
</div>
400+
`);
401+
});
402+
403+
test("uses fallbackComponent callback to create widgets that aren't explicitly declared", () => {
404+
const searchClient = createSearchClient();
405+
406+
const { container } = render(
407+
<InstantSearch
408+
searchClient={searchClient}
409+
indexName="test"
410+
// @ts-ignore resultsState in InstantSearch is typed wrongly to deal with multi-index
411+
resultsState={resultsState}
412+
>
413+
<DynamicWidgets
414+
transformItems={() => ['test1', 'test2', 'test3']}
415+
fallbackComponent={({ attribute }) => (
416+
<Menu attribute={attribute} otherProp />
417+
)}
418+
>
419+
<RefinementList attribute="test1" />
420+
</DynamicWidgets>
421+
</InstantSearch>
422+
);
423+
424+
expect(container).toMatchInlineSnapshot(`
425+
<div>
426+
RefinementList(test1)
427+
Menu(test2) {"otherProp":true}
428+
Menu(test3) {"otherProp":true}
429+
</div>
430+
`);
431+
});
330432
});
331433
});

0 commit comments

Comments
 (0)