Skip to content

Commit

Permalink
refactor(js): modularize package
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour committed Nov 14, 2020
1 parent bdbf309 commit e33e6ca
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 190 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module.exports = {
},
overrides: [
{
files: ['packages/autocomplete-js/src/setProperties.ts'],
files: ['packages/autocomplete-js/**/*/setProperties.ts'],
rules: {
'eslint-comments/no-unlimited-disable': 'off',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ describe('autocomplete-js', () => {
<div
class="aa-Panel"
hidden=""
style="top: 0px; left: 0px; right: 0px; max-width: unset;"
/>
</div>
</div>
Expand Down
228 changes: 59 additions & 169 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,67 @@
import {
createAutocomplete,
AutocompleteState as AutocompleteCoreState,
AutocompleteState,
} from '@algolia/autocomplete-core';

import { concatClassNames } from './concatClassNames';
import { debounce } from './debounce';
import { getHTMLElement } from './getHTMLElement';
import { createAutocompleteDom } from './createAutocompleteDom';
import { createEffectWrapper } from './createEffectWrapper';
import { getPanelPositionStyle } from './getPanelPositionStyle';
import { resetIcon, searchIcon } from './icons';
import { renderTemplate } from './renderTemplate';
import { setProperties, setPropertiesWithoutEvents } from './setProperties';
import {
AutocompleteOptions,
AutocompleteApi,
InternalAutocompleteSource,
} from './types';
import { render } from './render';
import { AutocompleteApi, AutocompleteOptions } from './types';
import { debounce, getHTMLElement, setProperties } from './utils';

function defaultRender({ root, sections }) {
function defaultRenderer({ root, sections }) {
for (const section of sections) {
root.appendChild(section);
}
}

export function autocomplete<TItem>({
container,
render: renderPanel = defaultRender,
render: renderer = defaultRenderer,
panelPlacement = 'input-wrapper-width',
classNames = {},
...props
}: AutocompleteOptions<TItem>): AutocompleteApi<TItem> {
const containerElement = getHTMLElement(container);
const inputWrapper = document.createElement('div');
const input = document.createElement('input');
const root = document.createElement('div');
const form = document.createElement('form');
const label = document.createElement('label');
const resetButton = document.createElement('button');
const panel = document.createElement('div');

const { effects, triggerEffect } = createEffectWrapper();
const autocomplete = createAutocomplete<TItem>({
...props,
onStateChange(options) {
const { state } = options;
render(state as any);
onStateChange(options.state as any);

if (props.onStateChange) {
props.onStateChange(options);
}
},
});

const onResize = debounce(() => {
setPanelPosition();
}, 100);
const {
inputWrapper,
form,
label,
input,
resetButton,
root,
panel,
} = createAutocompleteDom({
...autocomplete,
classNames,
});

function onStateChange(state: AutocompleteState<TItem>) {
render(renderer, {
state,
...autocomplete,
classNames,
root,
form,
input,
inputWrapper,
label,
panel,
resetButton,
});
}

function setPanelPosition() {
setProperties(panel, {
Expand All @@ -65,153 +74,34 @@ export function autocomplete<TItem>({
});
}

setProperties(window as any, {
...autocomplete.getEnvironmentProps({
searchBoxElement: form,
panelElement: panel,
inputElement: input,
}),
onResize,
});
setProperties(root, {
...autocomplete.getRootProps(),
class: concatClassNames(['aa-Autocomplete', classNames.root]),
});
const formProps = autocomplete.getFormProps({ inputElement: input });
setProperties(form, {
...formProps,
class: concatClassNames(['aa-Form', classNames.form]),
});
setProperties(inputWrapper, {
class: concatClassNames(['aa-InputWrapper', classNames.inputWrapper]),
});
setProperties(input, {
...autocomplete.getInputProps({ inputElement: input }),
class: concatClassNames(['aa-Input', classNames.input]),
});
setProperties(label, {
...autocomplete.getLabelProps(),
class: concatClassNames(['aa-Label', classNames.label]),
innerHTML: searchIcon,
});
setProperties(resetButton, {
type: 'reset',
onClick: formProps.onReset,
class: concatClassNames(['aa-ResetButton', classNames.resetButton]),
innerHTML: resetIcon,
});
setProperties(panel, {
...autocomplete.getPanelProps(),
hidden: true,
class: concatClassNames(['aa-Panel', classNames.panel]),
});

function render(state: AutocompleteCoreState<TItem>) {
setPropertiesWithoutEvents(root, autocomplete.getRootProps());
setPropertiesWithoutEvents(
input,
autocomplete.getInputProps({ inputElement: input })
);

panel.innerHTML = '';

if (state.isOpen) {
setProperties(panel, {
hidden: false,
});
} else {
setProperties(panel, {
hidden: true,
});
return;
}

if (state.status === 'stalled') {
panel.classList.add('aa-Panel--stalled');
} else {
panel.classList.remove('aa-Panel--stalled');
}

const sections = state.collections.map((collection) => {
const items = collection.items;
const source = collection.source as InternalAutocompleteSource<TItem>;

const section = document.createElement('section');
setProperties(section, {
class: concatClassNames(['aa-Source', classNames.source]),
});

if (source.templates.header) {
const header = document.createElement('div');
setProperties(header, {
class: concatClassNames(['aa-SourceHeader', classNames.sourceHeader]),
});
renderTemplate(
source.templates.header({ root: header, state }),
header
);
section.appendChild(header);
}

if (items.length > 0) {
const list = document.createElement('ul');
setProperties(list, {
...autocomplete.getListProps(),
class: concatClassNames(['aa-List', classNames.list]),
});

const listItems = items.map((item) => {
const li = document.createElement('li');
setProperties(li, {
...autocomplete.getItemProps({ item, source }),
class: concatClassNames(['aa-Item', classNames.item]),
});
renderTemplate(source.templates.item({ root: li, item, state }), li);

return li;
});

for (const listItem of listItems) {
list.appendChild(listItem);
}

section.appendChild(list);
}
function destroy() {
effects.forEach((cleanUp) => cleanUp());
}

if (source.templates.footer) {
const footer = document.createElement('div');
setProperties(footer, {
class: concatClassNames(['aa-SourceFooter', classNames.sourceFooter]),
});
renderTemplate(
source.templates.footer({ root: footer, state }),
footer
);
section.appendChild(footer);
}
requestAnimationFrame(() => {
setPanelPosition();
});

return section;
});
triggerEffect(() => {
const containerElement = getHTMLElement(container);
containerElement.appendChild(root);

renderPanel({ root: panel, sections, state });
}
return () => {
containerElement.removeChild(root);
};
});

inputWrapper.appendChild(input);
inputWrapper.appendChild(label);
inputWrapper.appendChild(resetButton);
form.appendChild(inputWrapper);
root.appendChild(form);
root.appendChild(panel);
containerElement.appendChild(root);
triggerEffect(() => {
const onResize = debounce(() => {
setPanelPosition();
}, 100);

setPanelPosition();
window.addEventListener('resize', onResize);

function destroy() {
containerElement.innerHTML = '';
setProperties(window as any, {
onResize: null,
});
}
return () => {
window.removeEventListener('resize', onResize);
};
});

return {
setSelectedItemId: autocomplete.setSelectedItemId,
Expand Down
85 changes: 85 additions & 0 deletions packages/autocomplete-js/src/createAutocompleteDom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { AutocompleteApi as AutocompleteCoreApi } from '@algolia/autocomplete-core';

import { resetIcon, searchIcon } from './icons';
import { AutocompleteClassNames, AutocompleteDom } from './types';
import { concatClassNames, setProperties } from './utils';

type CreateDomProps<TItem> = AutocompleteCoreApi<TItem> & {
classNames: AutocompleteClassNames;
};

export function createAutocompleteDom<TItem>({
getEnvironmentProps,
getRootProps,
getFormProps,
getLabelProps,
getInputProps,
getPanelProps,
classNames,
}: CreateDomProps<TItem>): AutocompleteDom {
const inputWrapper = document.createElement('div');
const input = document.createElement('input');
const root = document.createElement('div');
const form = document.createElement('form');
const label = document.createElement('label');
const resetButton = document.createElement('button');
const panel = document.createElement('div');

setProperties(
window as any,
getEnvironmentProps({
searchBoxElement: form,
panelElement: panel,
inputElement: input,
})
);
setProperties(root, {
...getRootProps(),
class: concatClassNames(['aa-Autocomplete', classNames.root]),
});
const formProps = getFormProps({ inputElement: input });
setProperties(form, {
...formProps,
class: concatClassNames(['aa-Form', classNames.form]),
});
setProperties(inputWrapper, {
class: concatClassNames(['aa-InputWrapper', classNames.inputWrapper]),
});
setProperties(label, {
...getLabelProps(),
class: concatClassNames(['aa-Label', classNames.label]),
innerHTML: searchIcon,
});
setProperties(input, {
...getInputProps({ inputElement: input }),
class: concatClassNames(['aa-Input', classNames.input]),
});
setProperties(resetButton, {
type: 'reset',
onClick: formProps.onReset,
class: concatClassNames(['aa-ResetButton', classNames.resetButton]),
innerHTML: resetIcon,
});
setProperties(panel, {
...getPanelProps(),
hidden: true,
class: concatClassNames(['aa-Panel', classNames.panel]),
});

inputWrapper.appendChild(input);
inputWrapper.appendChild(label);
inputWrapper.appendChild(resetButton);
form.appendChild(inputWrapper);
root.appendChild(form);
root.appendChild(panel);

return {
inputWrapper,
input,
root,
form,
label,
resetButton,
panel,
};
}
Loading

0 comments on commit e33e6ca

Please sign in to comment.