Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js): change renderer implementation to virtual DOM #381

Merged
merged 16 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
{
"path": "packages/autocomplete-js/dist/umd/index.production.js",
"maxSize": "10.2 kB"
"maxSize": "13.5 kB"
},
{
"path": "packages/autocomplete-preset-algolia/dist/umd/index.production.js",
Expand All @@ -18,11 +18,11 @@
},
{
"path": "packages/autocomplete-plugin-recent-searches/dist/umd/index.production.js",
"maxSize": "3.25 kB"
"maxSize": "3.5 kB"
},
{
"path": "packages/autocomplete-plugin-query-suggestions/dist/umd/index.production.js",
"maxSize": "2.3 kB"
"maxSize": "2.5 kB"
}
]
}
69 changes: 35 additions & 34 deletions examples/js/app.ts → examples/js/app.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
/** @jsx h */
import {
autocomplete,
getAlgoliaHits,
reverseHighlightHit,
highlightHit,
} from '@algolia/autocomplete-js';
import { createAlgoliaInsightsPlugin } from '@algolia/autocomplete-plugin-algolia-insights';
import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions';
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
import { Hit } from '@algolia/client-search';
import algoliasearch from 'algoliasearch';
import { h } from 'preact';
import insightsClient from 'search-insights';

import '@algolia/autocomplete-theme-classic';

type Product = { name: string; image: string };
type ProductHit = Hit<Product>;

const appId = 'latency';
const apiKey = '6be0576ff61c053d5f9a3225e2a90f76';
const searchClient = algoliasearch(appId, apiKey);
Expand All @@ -24,16 +30,18 @@ const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
const querySuggestionsPlugin = createQuerySuggestionsPlugin({
searchClient,
indexName: 'instant_search_demo_query_suggestions',
getSearchParams() {
getSearchParams({ state }) {
return recentSearchesPlugin.data.getAlgoliaSearchParams({
clickAnalytics: true,
hitsPerPage: state.query ? 5 : 10,
});
},
});

autocomplete({
container: '#autocomplete',
placeholder: 'Search',
debug: true,
openOnFocus: true,
plugins: [
algoliaInsightsPlugin,
Expand All @@ -48,47 +56,40 @@ autocomplete({
return [
{
getItems() {
return getAlgoliaHits({
return getAlgoliaHits<Product>({
searchClient,
queries: [{ indexName: 'instant_search', query }],
});
},
templates: {
item({ item, root }) {
const itemContent = document.createElement('div');
const ItemSourceIcon = document.createElement('div');
const itemTitle = document.createElement('div');
const sourceIcon = document.createElement('img');

sourceIcon.width = 20;
sourceIcon.height = 20;
sourceIcon.src = item.image;

ItemSourceIcon.classList.add('aa-ItemSourceIcon');
ItemSourceIcon.appendChild(sourceIcon);

itemTitle.innerHTML = reverseHighlightHit({
hit: item,
attribute: 'name',
});
itemTitle.classList.add('aa-ItemTitle');

itemContent.classList.add('aa-ItemContent');
itemContent.appendChild(ItemSourceIcon);
itemContent.appendChild(itemTitle);

root.appendChild(itemContent);
item({ item }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does an example using a provided pragma / createElement look like (without using jsx directly)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether the main example should be using jsx, since it might confuse people unfamiliar with it (Shopify, magento...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does an example using a provided pragma / createElement look like (without using jsx directly)?

You can see in the plugins implementations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether the main example should be using jsx, since it might confuse people unfamiliar with it (Shopify, magento...)

The main example is used as a playground. We'll provide code samples for people relying on HTML.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created this sandbox to demo how you can use Autocomplete with HTML.

return <ProductItem hit={item} />;
},
empty({ root }) {
const itemContent = document.createElement('div');

itemContent.innerHTML = 'No results for this query';
itemContent.classList.add('aa-ItemContent');

root.appendChild(itemContent);
empty() {
return (
<div className="aa-ItemContent">No results for this query.</div>
);
},
},
},
];
},
});

type ProductItemProps = {
hit: ProductHit;
};

function ProductItem({ hit }: ProductItemProps) {
return (
<div className="aa-ItemContent">
<div className="aa-ItemSourceIcon">
<img src={hit.image} alt={hit.name} width="20" height="20" />
</div>

<div className="aa-ItemTitle">
{highlightHit<ProductHit>({ hit, attribute: 'name' })}
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion examples/js/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@
</div>

<script src="env.ts"></script>
<script src="app.ts"></script>
<script src="app.tsx"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions examples/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"@algolia/autocomplete-plugin-recent-searches": "1.0.0-alpha.38",
"@algolia/autocomplete-preset-algolia": "1.0.0-alpha.38",
"@algolia/autocomplete-theme-classic": "1.0.0-alpha.38",
"@algolia/client-search": "4.8.3",
"algoliasearch": "4.8.3",
"preact": "10.5.7",
"search-insights": "1.6.3"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion packages/autocomplete-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"dependencies": {
"@algolia/autocomplete-core": "1.0.0-alpha.38",
"@algolia/autocomplete-preset-algolia": "1.0.0-alpha.38",
"@algolia/autocomplete-shared": "1.0.0-alpha.38"
"@algolia/autocomplete-shared": "1.0.0-alpha.38",
"preact": "^10.0.0"
},
"devDependencies": {
"@algolia/client-search": "4.8.3"
Expand Down
21 changes: 14 additions & 7 deletions packages/autocomplete-js/src/__tests__/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ describe('autocomplete-js', () => {
</label>
<div
class="aa-LoadingIndicator"
hidden=""
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
>
<svg
class="aa-LoadingIcon"
Expand Down Expand Up @@ -131,6 +132,7 @@ describe('autocomplete-js', () => {
>
<button
class="aa-ResetButton"
hidden=""
type="reset"
>
<svg
Expand Down Expand Up @@ -205,7 +207,7 @@ describe('autocomplete-js', () => {
test('calls renderEmpty without empty template on no results', async () => {
const container = document.createElement('div');
const panelContainer = document.createElement('div');
const renderEmpty = jest.fn(({ root }) => {
const renderEmpty = jest.fn((_params, root) => {
const div = document.createElement('div');
div.innerHTML = 'No results render';

Expand Down Expand Up @@ -246,11 +248,16 @@ describe('autocomplete-js', () => {
).toBeInTheDocument();
});

expect(renderEmpty).toHaveBeenCalledWith({
root: expect.anything(),
state: expect.anything(),
sections: expect.anything(),
});
expect(renderEmpty).toHaveBeenCalledWith(
{
state: expect.anything(),
children: expect.anything(),
sections: expect.any(Array),
createElement: expect.anything(),
Fragment: expect.anything(),
},
expect.any(HTMLElement)
);

expect(
panelContainer.querySelector<HTMLElement>('.aa-Panel')
Expand Down Expand Up @@ -282,7 +289,7 @@ describe('autocomplete-js', () => {
},
];
},
renderEmpty({ root }) {
renderEmpty(_params, root) {
const div = document.createElement('div');
div.innerHTML = 'No results render';

Expand Down
22 changes: 19 additions & 3 deletions packages/autocomplete-js/src/__tests__/reverseHighlightHit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,23 @@ describe('reverseHighlightHit', () => {
},
attribute: 'query',
})
).toMatchInlineSnapshot(`"<mark>amazon </mark>fire tablet<mark>s</mark>"`);
).toEqual([
expect.objectContaining({
type: 'mark',
props: {
children: 'amazon ',
},
}),
'fire',
' ',
'tablet',
expect.objectContaining({
type: 'mark',
props: {
children: 's',
},
}),
]);
});

test('returns a reversed fully highlighted hit', () => {
Expand All @@ -40,7 +56,7 @@ describe('reverseHighlightHit', () => {
},
attribute: 'query',
})
).toMatchInlineSnapshot(`"amazon fire tablets"`);
).toEqual(['amazon', ' ', 'fire', ' ', 'tablets']);
});

test('returns a reversed empty highlighted query hit', () => {
Expand All @@ -60,6 +76,6 @@ describe('reverseHighlightHit', () => {
},
attribute: 'query',
})
).toMatchInlineSnapshot(`"amazon fire tablets"`);
).toEqual(['amazon fire tablets']);
});
});
29 changes: 13 additions & 16 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ export function autocomplete<TItem extends BaseItem>(
}),
})
);

const renderRequestIdRef = createRef<number | null>(null);
const lastStateRef = createRef<AutocompleteState<TItem>>({
collections: [],
completion: null,
Expand Down Expand Up @@ -95,13 +93,13 @@ export function autocomplete<TItem extends BaseItem>(

const dom = reactive(() =>
createAutocompleteDom({
placeholder: props.value.core.placeholder,
isTouch: isTouch.value,
state: lastStateRef.current,
autocomplete: autocomplete.value,
autocompleteScopeApi,
classNames: props.value.renderer.classNames,
isTouch: isTouch.value,
placeholder: props.value.core.placeholder,
propGetters,
autocompleteScopeApi,
state: lastStateRef.current,
})
);

Expand All @@ -120,16 +118,19 @@ export function autocomplete<TItem extends BaseItem>(

function runRender() {
const renderProps = {
isTouch: isTouch.value,
state: lastStateRef.current,
autocomplete: autocomplete.value,
propGetters,
dom: dom.value,
autocompleteScopeApi,
classNames: props.value.renderer.classNames,
container: props.value.renderer.container,
createElement: props.value.renderer.renderer.createElement,
francoischalifour marked this conversation as resolved.
Show resolved Hide resolved
dom: dom.value,
Fragment: props.value.renderer.renderer.Fragment,
isTouch: isTouch.value,
panelContainer: isTouch.value
? dom.value.touchOverlay
: props.value.renderer.panelContainer,
autocompleteScopeApi,
propGetters,
state: lastStateRef.current,
};

hasEmptySourceTemplateRef.current = renderProps.state.collections.some(
Expand All @@ -147,12 +148,8 @@ export function autocomplete<TItem extends BaseItem>(
}

function scheduleRender(state: AutocompleteState<TItem>) {
if (renderRequestIdRef.current !== null) {
cancelAnimationFrame(renderRequestIdRef.current);
}

lastStateRef.current = state;
renderRequestIdRef.current = requestAnimationFrame(runRender);
runRender();
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
}

runEffect(() => {
Expand Down
20 changes: 0 additions & 20 deletions packages/autocomplete-js/src/components/Element.ts

This file was deleted.

15 changes: 7 additions & 8 deletions packages/autocomplete-js/src/components/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@ import {
AutocompleteScopeApi,
} from '@algolia/autocomplete-core';

import { createDomElement } from '../createDomElement';
import { AutocompletePropGetters, AutocompleteState } from '../types';
import { Component } from '../types/Component';
import { setProperties } from '../utils';

import { Element } from './Element';

type InputProps = {
onTouchEscape?(): void;
state: AutocompleteState<any>;
autocompleteScopeApi: AutocompleteScopeApi<any>;
getInputProps: AutocompletePropGetters<any>['getInputProps'];
getInputPropsCore: AutocompleteCoreApi<any>['getInputProps'];
autocompleteScopeApi: AutocompleteScopeApi<any>;
onTouchEscape?(): void;
state: AutocompleteState<any>;
};

export const Input: Component<InputProps, HTMLInputElement> = ({
autocompleteScopeApi,
classNames,
getInputProps,
getInputPropsCore,
state,
autocompleteScopeApi,
onTouchEscape,
state,
...props
}) => {
const element = Element('input', props);
const element = createDomElement('input', props);
const inputProps = getInputProps({
state,
props: getInputPropsCore({ inputElement: element }),
Expand Down
1 change: 0 additions & 1 deletion packages/autocomplete-js/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './Element';
export * from './Input';
export * from './LoadingIcon';
export * from './ResetIcon';
Expand Down
Loading