Skip to content

Commit 8fa038b

Browse files
authored
fix(autocomplete-js): update components with new renderer (#946)
1 parent 5fbae0d commit 8fa038b

File tree

12 files changed

+323
-19
lines changed

12 files changed

+323
-19
lines changed

bundlesize.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
{
88
"path": "packages/autocomplete-js/dist/umd/index.production.js",
9-
"maxSize": "17.5 kB"
9+
"maxSize": "17.75 kB"
1010
},
1111
{
1212
"path": "packages/autocomplete-preset-algolia/dist/umd/index.production.js",

packages/autocomplete-js/src/__tests__/api.test.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,10 @@ describe('api', () => {
398398
return [{ label: query }];
399399
},
400400
templates: {
401-
item({ item, html }) {
402-
return html`<div>${item.label}</div>`;
401+
item({ item, components, html }) {
402+
return html`<div>
403+
${components.Highlight({ hit: item, attribute: 'label' })}
404+
</div>`;
403405
},
404406
},
405407
},
@@ -445,6 +447,75 @@ describe('api', () => {
445447
expect(mockCreateElement2).toHaveBeenCalled();
446448
});
447449
});
450+
451+
test('preserves all user `components` when not updated', async () => {
452+
const container = document.createElement('div');
453+
const panelContainer = document.createElement('div');
454+
document.body.appendChild(container);
455+
document.body.appendChild(panelContainer);
456+
457+
const CustomFragment = (props: any) => props.children;
458+
const mockCreateElement1 = jest.fn(preactCreateElement);
459+
const mockCreateElement2 = jest.fn(preactCreateElement);
460+
const mockRender = jest.fn().mockImplementation(preactRender);
461+
const CustomHighlight = jest.fn((props: { hit: { label: string } }) =>
462+
mockCreateElement1(CustomFragment, null, props.hit.label)
463+
);
464+
const MyComponent = (props: any) => props.children;
465+
466+
const { update } = autocomplete<{ label: string }>({
467+
container,
468+
panelContainer,
469+
getSources() {
470+
return [
471+
{
472+
sourceId: 'testSource',
473+
getItems({ query }) {
474+
return [{ label: query }];
475+
},
476+
templates: {
477+
item({ item, components, html }) {
478+
return html`<div>
479+
${components.Highlight({ hit: item, attribute: 'label' })}
480+
${components.MyComponent({ children: item.label })}
481+
</div>`;
482+
},
483+
},
484+
},
485+
];
486+
},
487+
components: { Highlight: CustomHighlight, MyComponent },
488+
renderer: {
489+
Fragment: CustomFragment,
490+
render: mockRender,
491+
createElement: mockCreateElement1,
492+
},
493+
});
494+
495+
update({
496+
renderer: {
497+
Fragment: CustomFragment,
498+
render: mockRender,
499+
createElement: mockCreateElement2,
500+
},
501+
});
502+
503+
mockCreateElement1.mockClear();
504+
505+
const input = container.querySelector<HTMLInputElement>('.aa-Input');
506+
507+
fireEvent.input(input, { target: { value: 'iphone' } });
508+
509+
await waitFor(() => {
510+
expect(
511+
panelContainer.querySelector<HTMLElement>('.aa-Panel')
512+
).toHaveTextContent('iphone');
513+
// The custom `Highlight` component wasn't updated, so the previous
514+
// `createElement` implementation is still being called.
515+
expect(mockCreateElement1).toHaveBeenCalledTimes(1);
516+
expect(mockCreateElement2).toHaveBeenCalled();
517+
});
518+
});
448519
});
449520

450521
describe('destroy', () => {

packages/autocomplete-js/src/__tests__/components.test.tsx

Lines changed: 174 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/** @jsx h */
22
import { Hit } from '@algolia/client-search';
33
import { fireEvent, waitFor } from '@testing-library/dom';
4-
import { h } from 'preact';
4+
import {
5+
Fragment,
6+
createElement as preactCreateElement,
7+
h,
8+
render,
9+
} from 'preact';
510

611
import { createSource } from '../../../../test/utils';
712
import { autocomplete } from '../autocomplete';
@@ -121,7 +126,48 @@ describe('components', () => {
121126
});
122127
});
123128

124-
test.todo('provides Highlight component with custom createElement');
129+
test('provides Highlight component with custom createElement', async () => {
130+
const container = document.createElement('div');
131+
const panelContainer = document.createElement('div');
132+
133+
const mockCreateElement = jest.fn(preactCreateElement);
134+
135+
document.body.appendChild(panelContainer);
136+
autocomplete<ProductHit>({
137+
container,
138+
panelContainer,
139+
getSources() {
140+
return [
141+
{
142+
...createSource({
143+
getItems() {
144+
return productHits;
145+
},
146+
}),
147+
templates: {
148+
item({ item, components }) {
149+
return (
150+
<components.Highlight
151+
hit={item}
152+
attribute="name"
153+
tagName="em"
154+
/>
155+
);
156+
},
157+
},
158+
},
159+
];
160+
},
161+
renderer: { createElement: mockCreateElement, Fragment, render },
162+
});
163+
164+
const input = container.querySelector<HTMLInputElement>('.aa-Input');
165+
fireEvent.input(input, { target: { value: 'a' } });
166+
167+
await waitFor(() => {
168+
expect(mockCreateElement).toHaveBeenCalled();
169+
});
170+
});
125171

126172
test('provides Snippet component', async () => {
127173
const container = document.createElement('div');
@@ -205,7 +251,48 @@ describe('components', () => {
205251
});
206252
});
207253

208-
test.todo('provides Snippet component with custom createElement');
254+
test('provides Snippet component with custom createElement', async () => {
255+
const container = document.createElement('div');
256+
const panelContainer = document.createElement('div');
257+
258+
const mockCreateElement = jest.fn(preactCreateElement);
259+
260+
document.body.appendChild(panelContainer);
261+
autocomplete<ProductHit>({
262+
container,
263+
panelContainer,
264+
getSources() {
265+
return [
266+
{
267+
...createSource({
268+
getItems() {
269+
return productHits;
270+
},
271+
}),
272+
templates: {
273+
item({ item, components }) {
274+
return (
275+
<components.Snippet
276+
hit={item}
277+
attribute="name"
278+
tagName="em"
279+
/>
280+
);
281+
},
282+
},
283+
},
284+
];
285+
},
286+
renderer: { createElement: mockCreateElement, Fragment, render },
287+
});
288+
289+
const input = container.querySelector<HTMLInputElement>('.aa-Input');
290+
fireEvent.input(input, { target: { value: 'a' } });
291+
292+
await waitFor(() => {
293+
expect(mockCreateElement).toHaveBeenCalled();
294+
});
295+
});
209296

210297
test('provides ReverseHighlight component', async () => {
211298
const container = document.createElement('div');
@@ -291,7 +378,48 @@ describe('components', () => {
291378
});
292379
});
293380

294-
test.todo('provides ReverseHighlight component with custom createElement');
381+
test('provides ReverseHighlight component with custom createElement', async () => {
382+
const container = document.createElement('div');
383+
const panelContainer = document.createElement('div');
384+
385+
const mockCreateElement = jest.fn(preactCreateElement);
386+
387+
document.body.appendChild(panelContainer);
388+
autocomplete<ProductHit>({
389+
container,
390+
panelContainer,
391+
getSources() {
392+
return [
393+
{
394+
...createSource({
395+
getItems() {
396+
return productHits;
397+
},
398+
}),
399+
templates: {
400+
item({ item, components }) {
401+
return (
402+
<components.ReverseHighlight
403+
hit={item}
404+
attribute="name"
405+
tagName="em"
406+
/>
407+
);
408+
},
409+
},
410+
},
411+
];
412+
},
413+
renderer: { createElement: mockCreateElement, Fragment, render },
414+
});
415+
416+
const input = container.querySelector<HTMLInputElement>('.aa-Input');
417+
fireEvent.input(input, { target: { value: 'a' } });
418+
419+
await waitFor(() => {
420+
expect(mockCreateElement).toHaveBeenCalled();
421+
});
422+
});
295423

296424
test('provides ReverseSnippet component', async () => {
297425
const container = document.createElement('div');
@@ -377,7 +505,48 @@ describe('components', () => {
377505
});
378506
});
379507

380-
test.todo('provides ReverseSnippet component with custom createElement');
508+
test('provides ReverseSnippet component with custom createElement', async () => {
509+
const container = document.createElement('div');
510+
const panelContainer = document.createElement('div');
511+
512+
const mockCreateElement = jest.fn(preactCreateElement);
513+
514+
document.body.appendChild(panelContainer);
515+
autocomplete<ProductHit>({
516+
container,
517+
panelContainer,
518+
getSources() {
519+
return [
520+
{
521+
...createSource({
522+
getItems() {
523+
return productHits;
524+
},
525+
}),
526+
templates: {
527+
item({ item, components }) {
528+
return (
529+
<components.ReverseSnippet
530+
hit={item}
531+
attribute="name"
532+
tagName="em"
533+
/>
534+
);
535+
},
536+
},
537+
},
538+
];
539+
},
540+
renderer: { createElement: mockCreateElement, Fragment, render },
541+
});
542+
543+
const input = container.querySelector<HTMLInputElement>('.aa-Input');
544+
fireEvent.input(input, { target: { value: 'a' } });
545+
546+
await waitFor(() => {
547+
expect(mockCreateElement).toHaveBeenCalled();
548+
});
549+
});
381550

382551
test('allows registering custom components', async () => {
383552
const container = document.createElement('div');

packages/autocomplete-js/src/autocomplete.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
VNode,
2626
} from './types';
2727
import { userAgents } from './userAgents';
28-
import { mergeDeep, setProperties } from './utils';
28+
import { mergeDeep, pickBy, setProperties } from './utils';
2929

3030
export function autocomplete<TItem extends BaseItem>(
3131
options: AutocompleteOptions<TItem>
@@ -341,10 +341,23 @@ export function autocomplete<TItem extends BaseItem>(
341341
function update(updatedOptions: Partial<AutocompleteOptions<TItem>> = {}) {
342342
cleanupEffects();
343343

344+
const { components, ...rendererProps } = props.value.renderer;
345+
344346
optionsRef.current = mergeDeep(
345-
props.value.renderer,
347+
rendererProps,
346348
props.value.core,
347-
{ initialState: lastStateRef.current },
349+
{
350+
// We need to filter out default components so they can be replaced with
351+
// a new `renderer`, without getting rid of user components.
352+
// @MAJOR Deal with registering components with the same name as the
353+
// default ones. If we disallow overriding default components, we'd just
354+
// need to pass all `components` here.
355+
components: pickBy(
356+
components,
357+
({ value }) => !value.hasOwnProperty('__autocomplete_componentName')
358+
),
359+
initialState: lastStateRef.current,
360+
},
348361
updatedOptions
349362
);
350363

packages/autocomplete-js/src/components/Highlight.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function createHighlightComponent({
66
createElement,
77
Fragment,
88
}: AutocompleteRenderer) {
9-
return function Highlight<THit>({
9+
function Highlight<THit>({
1010
hit,
1111
attribute,
1212
tagName = 'mark',
@@ -20,5 +20,9 @@ export function createHighlightComponent({
2020
: x.value
2121
)
2222
);
23-
};
23+
}
24+
25+
Highlight.__autocomplete_componentName = 'Highlight';
26+
27+
return Highlight;
2428
}

packages/autocomplete-js/src/components/ReverseHighlight.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function createReverseHighlightComponent({
66
createElement,
77
Fragment,
88
}: AutocompleteRenderer) {
9-
return function ReverseHighlight<THit>({
9+
function ReverseHighlight<THit>({
1010
hit,
1111
attribute,
1212
tagName = 'mark',
@@ -23,5 +23,9 @@ export function createReverseHighlightComponent({
2323
: x.value
2424
)
2525
);
26-
};
26+
}
27+
28+
ReverseHighlight.__autocomplete_componentName = 'ReverseHighlight';
29+
30+
return ReverseHighlight;
2731
}

0 commit comments

Comments
 (0)