Skip to content

Commit

Permalink
fix(js): update HTML elements properties at every render
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour committed Sep 3, 2020
1 parent 1e76ff5 commit b00878c
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 65 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
99 changes: 52 additions & 47 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -73,6 +73,7 @@ export function autocomplete<TItem>({
}: AutocompleteOptions<TItem>): AutocompleteApi<TItem> {
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');
Expand All @@ -92,25 +93,26 @@ export function autocomplete<TItem>({
...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 = `<svg
setProperties(form, {
...formProps,
class: 'aa-Form',
});
setProperties(label, {
...autocomplete.getLabelProps(),
class: 'aa-Label',
innerHTML: `<svg
width="20"
height="20"
viewBox="0 0 20 20"
Expand All @@ -123,30 +125,32 @@ export function autocomplete<TItem>({
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>`;
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', '');
</svg>`,
});
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<TItem>) {
input.value = state.query;
setPropertiesWithoutEvents(root, autocomplete.getRootProps());
setPropertiesWithoutEvents(
input,
autocomplete.getInputProps({ inputElement: input })
);

if (props.enableCompletion) {
completion.textContent = state.completion;
Expand All @@ -155,9 +159,13 @@ export function autocomplete<TItem>({
dropdown.innerHTML = '';

if (state.isOpen) {
dropdown.removeAttribute('hidden');
setProperties(dropdown, {
hidden: false,
});
} else {
dropdown.setAttribute('hidden', '');
setProperties(dropdown, {
hidden: true,
});
return;
}

Expand All @@ -184,14 +192,11 @@ export function autocomplete<TItem>({

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;
Expand Down
88 changes: 70 additions & 18 deletions packages/autocomplete-js/src/setProperties.ts
Original file line number Diff line number Diff line change
@@ -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' &&
Expand All @@ -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
Expand All @@ -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);
}
}
}
Expand All @@ -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]);
}
}
}

0 comments on commit b00878c

Please sign in to comment.