diff --git a/.eslintrc.js b/.eslintrc.js index 1f647ae2b..9b6f4239c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,6 +49,12 @@ module.exports = { ], }, overrides: [ + { + files: ['packages/autocomplete-js/src/setProperties.ts'], + rules: { + 'eslint-comments/no-unlimited-disable': 'off', + }, + }, { files: ['**/rollup.config.js', 'stories/**/*', '**/__tests__/**'], rules: { diff --git a/packages/autocomplete-js/src/autocomplete.ts b/packages/autocomplete-js/src/autocomplete.ts index 140663ce7..fff2b00e1 100644 --- a/packages/autocomplete-js/src/autocomplete.ts +++ b/packages/autocomplete-js/src/autocomplete.ts @@ -8,7 +8,7 @@ import { } from '@algolia/autocomplete-core'; import { getHTMLElement } from './getHTMLElement'; -import { setProperties } from './setProperties'; +import { setProperties, setPropertiesWithoutEvents } from './setProperties'; /** * Renders the template in the root element. @@ -73,6 +73,7 @@ export function autocomplete({ }: AutocompleteOptions): AutocompleteApi { const containerElement = getHTMLElement(container); const inputWrapper = document.createElement('div'); + const completion = document.createElement('span'); const input = document.createElement('input'); const root = document.createElement('div'); const form = document.createElement('form'); @@ -92,25 +93,26 @@ export function autocomplete({ ...props, }); - const environmentProps = autocomplete.getEnvironmentProps({ - searchBoxElement: form, - dropdownElement: dropdown, - inputElement: input, + setProperties(window as any, { + ...autocomplete.getEnvironmentProps({ + searchBoxElement: form, + dropdownElement: dropdown, + inputElement: input, + }), + }); + setProperties(root, { + ...autocomplete.getRootProps(), + class: 'aa-Autocomplete', }); - - setProperties(window, environmentProps); - - const rootProps = autocomplete.getRootProps(); - setProperties(root, rootProps); - root.classList.add('aa-Autocomplete'); - const formProps = autocomplete.getFormProps({ inputElement: input }); - setProperties(form, formProps); - form.classList.add('aa-Form'); - - const labelProps = autocomplete.getLabelProps(); - setProperties(label, labelProps); - label.innerHTML = `({ strokeLinecap="round" strokeLinejoin="round" /> -`; - label.classList.add('aa-Label'); - - inputWrapper.classList.add('aa-InputWrapper'); - - const inputProps = autocomplete.getInputProps({ inputElement: input }); - setProperties(input, inputProps); - input.classList.add('aa-Input'); - - const completion = document.createElement('span'); - completion.classList.add('aa-Completion'); - - resetButton.setAttribute('type', 'reset'); - resetButton.textContent = 'x'; - resetButton.classList.add('aa-Reset'); - resetButton.addEventListener('click', formProps.onReset); - - const dropdownProps = autocomplete.getDropdownProps({}); - setProperties(dropdown, dropdownProps); - dropdown.classList.add('aa-Dropdown'); - dropdown.setAttribute('hidden', ''); +`, + }); + setProperties(inputWrapper, { class: 'aa-InputWrapper' }); + setProperties(input, { + ...autocomplete.getInputProps({ inputElement: input }), + class: 'aa-Input', + }); + setProperties(completion, { class: 'aa-Completion' }); + setProperties(resetButton, { + type: 'reset', + textContent: 'x', + onClick: formProps.onReset, + class: 'aa-Reset', + }); + setProperties(dropdown, { + ...autocomplete.getDropdownProps(), + hidden: true, + class: 'aa-Dropdown', + }); function render(state: AutocompleteState) { - input.value = state.query; + setPropertiesWithoutEvents(root, autocomplete.getRootProps()); + setPropertiesWithoutEvents( + input, + autocomplete.getInputProps({ inputElement: input }) + ); if (props.enableCompletion) { completion.textContent = state.completion; @@ -155,9 +159,13 @@ export function autocomplete({ dropdown.innerHTML = ''; if (state.isOpen) { - dropdown.removeAttribute('hidden'); + setProperties(dropdown, { + hidden: false, + }); } else { - dropdown.setAttribute('hidden', ''); + setProperties(dropdown, { + hidden: true, + }); return; } @@ -184,14 +192,11 @@ export function autocomplete({ if (items.length > 0) { const menu = document.createElement('ul'); - const menuProps = autocomplete.getMenuProps(); - setProperties(menu, menuProps); + setProperties(menu, autocomplete.getMenuProps()); const menuItems = items.map((item) => { const li = document.createElement('li'); - const itemProps = autocomplete.getItemProps({ item, source }); - setProperties(li, itemProps); - + setProperties(li, autocomplete.getItemProps({ item, source })); renderTemplate(source.templates.item({ root: li, item, state }), li); return li; diff --git a/packages/autocomplete-js/src/setProperties.ts b/packages/autocomplete-js/src/setProperties.ts index 07d73e411..e0ab31deb 100644 --- a/packages/autocomplete-js/src/setProperties.ts +++ b/packages/autocomplete-js/src/setProperties.ts @@ -1,20 +1,65 @@ -// Taken from Preact -// https://github.com/preactjs/preact/blob/6ab49d9020740127577bf4af66bf63f4af7f9fee/src/diff/props.js#L58-L151 -function setProperty(element: HTMLElement | Window, name: string, value: any) { +/* eslint-disable */ + +/* + * Taken from Preact + * + * See https://github.com/preactjs/preact/blob/6ab49d9020740127577bf4af66bf63f4af7f9fee/src/diff/props.js#L58-L151 + */ + +function setStyle(style: object, key: string, value: any) { + if (value === null) { + style[key] = ''; + } else if (typeof value !== 'number') { + style[key] = value; + } else { + style[key] = value + 'px'; + } +} + +/** + * Proxy an event to hooked event handlers + */ +function eventProxy(this: any, event: Event) { + this._listeners[event.type](event); +} + +/** + * Set a property value on a DOM node + */ +export function setProperty(dom: HTMLElement, name: string, value: any) { let useCapture: boolean; let nameLower: string; + let oldValue = dom[name]; + if (name === 'style') { + if (typeof value == 'string') { + (dom as any).style = value; + } else { + if (value === null) { + (dom as any).style = ''; + } else { + for (name in value) { + if (!oldValue || value[name] !== oldValue[name]) { + setStyle(dom.style, name, value[name]); + } + } + } + } + } // Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6 - if (name[0] === 'o' && name[1] === 'n') { + else if (name[0] === 'o' && name[1] === 'n') { useCapture = name !== (name = name.replace(/Capture$/, '')); nameLower = name.toLowerCase(); - if (nameLower in element) name = nameLower; + if (nameLower in dom) name = nameLower; name = name.slice(2); + if (!(dom as any)._listeners) (dom as any)._listeners = {}; + (dom as any)._listeners[name] = value; + if (value) { - element.addEventListener(name, value, useCapture); + if (!oldValue) dom.addEventListener(name, eventProxy, useCapture); } else { - element.removeEventListener(name, value, useCapture); + dom.removeEventListener(name, eventProxy, useCapture); } } else if ( name !== 'list' && @@ -26,15 +71,12 @@ function setProperty(element: HTMLElement | Window, name: string, value: any) { name !== 'size' && name !== 'download' && name !== 'href' && - name in element - ) { - element[name] = value === null ? '' : value; - } else if ( - typeof value !== 'function' && - name !== 'dangerouslySetInnerHTML' + name in dom ) { + dom[name] = value == null ? '' : value; + } else if (typeof value != 'function' && name !== 'dangerouslySetInnerHTML') { if ( - value === null || + value == null || (value === false && // ARIA-attributes have a different notion of boolean values. // The value `false` is different from the attribute not @@ -44,9 +86,9 @@ function setProperty(element: HTMLElement | Window, name: string, value: any) { // that other VDOM frameworks also always stringify `false`. !/^ar/.test(name)) ) { - (element as HTMLElement).removeAttribute(name); + dom.removeAttribute(name); } else { - (element as HTMLElement).setAttribute(name, value); + dom.setAttribute(name, value); } } } @@ -60,9 +102,19 @@ function getNormalizedName(name: string): string { } } -export function setProperties(dom: HTMLElement | Window, props: object): void { - // eslint-disable-next-line guard-for-in +export function setProperties(dom: HTMLElement, props: object): void { for (const name in props) { setProperty(dom, getNormalizedName(name), props[name]); } } + +export function setPropertiesWithoutEvents( + dom: HTMLElement, + props: object +): void { + for (const name in props) { + if (!(name[0] === 'o' && name[1] === 'n')) { + setProperty(dom, getNormalizedName(name), props[name]); + } + } +}