Пример создания собственного веб-компонента без использования фрэймворков
Учебный пример проведёт вас сквозь создание собственного HTML5 тэга <howto-checkbox>
реализующего элемент ввода "флажок", который позволяет пользователю переключать себя между двумя состояниями.
Исходный пример вы можете посмотреть здесь.
При создании элемент устанавливает атрибуты role = "checkbox" и tabindex = "0", которые позволяют использовать его с клавиатурой и такими технологиями, как устройство чтения с экрана. Более подробное описание можно найти по ссылкам в исходном примере.
В примере не реализована отправка данных в составе блока <form>
.
Когда флажок установлен, он добавляет логический атрибут checked и устанавливает его значение в true. Также, элемент устанавливает атрибут aria-checked. Щелчок по элементу с помощью мыши или пробела переключает состояние между true и false.
Флажок также поддерживает отключенное состояние. Если указано свойство disabled = "true" или атрибут disabled, флажок устанавливает aria-disabled = "true", удаляет атрибут tabindex и убирает фокус с себя.
Полный JavaScript код элемента с комментариями:
// Инкапсулируем код в анонимную функцию
(function() {
// Установим код кнопки клавиатуры для переключения
const KEYCODE = {
SPACE: 32,
};
// Клонирование содержимого из шаблона лучше использования
// innerHTML при создании элемента тк не требуется многократно
// парсить содержимое.
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: inline-block;
// Картинки необходимо взять по ссылке из исходного примера
background: url('../images/unchecked-checkbox.svg') no-repeat;
background-size: contain;
width: 24px;
height: 24px;
}
:host([hidden]) {
display: none;
}
:host([checked]) {
background: url('../images/checked-checkbox.svg') no-repeat;
background-size: contain;
}
:host([disabled]) {
background:
url('../images/unchecked-checkbox-disabled.svg') no-repeat;
background-size: contain;
}
:host([checked][disabled]) {
background:
url('../images/checked-checkbox-disabled.svg') no-repeat;
background-size: contain;
}
</style>
`;
class HowToCheckbox extends HTMLElement {
static get observedAttributes() {
return ['checked', 'disabled'];
}
// Конструктор запускается при создании элемента в документе.
// Объекты класса могут быть созданы:
// - при разборе HTML, если встречен тэг <howto-checkbox>,
// - при вызове document.createElement('howto-checkbox'),
// - при вызове конструктора new HowToCheckbox();
// Конструктор - хорошее место для создания теневого DOM, но не следует
// использовать атрибуты и DOM потомков, тк они ещё не доступны.
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
// connectedCallback() исполняется при вставке элемента в DOM.
// Это хорошее место для инициализации атрибутов и обработчиков
// событий.
connectedCallback() {
if (!this.hasAttribute('role'))
this.setAttribute('role', 'checkbox');
if (!this.hasAttribute('tabindex'))
this.setAttribute('tabindex', 0);
// Пользователь может установить свойства элемента до того, как
// прототип будет соединён с классом. Метод _upgradeProperty()
// проверяет свойства элемента и запускает для них верные сэттеры.
this._upgradeProperty('checked');
this._upgradeProperty('disabled');
this.addEventListener('keyup', this._onKeyUp);
this.addEventListener('click', this._onClick);
}
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
// disconnectedCallback() исполняется при удалении элемента из DOM.
// Это хорошее место для удаления ссылок и обработчиков событий.
disconnectedCallback() {
this.removeEventListener('keyup', this._onKeyUp);
this.removeEventListener('click', this._onClick);
}
// Свойства и связанные с ними атрибуты тэгов должны отражать друг друга.
// Сэттер связывающий свойство логического состояния и атрибут тэга.
set checked(value) {
const isChecked = Boolean(value);
if (isChecked)
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}
get checked() {
return this.hasAttribute('checked');
}
set disabled(value) {
const isDisabled = Boolean(value);
if (isDisabled)
this.setAttribute('disabled', '');
else
this.removeAttribute('disabled');
}
get disabled() {
return this.hasAttribute('disabled');
}
// attributeChangedCallback() исполняется когда любой из атрибутов
// в списке был изменён. Это хорошее место для обработки побочных
// эффектов изменения данных, таких как установка ARIA атрибутов.
attributeChangedCallback(name, oldValue, newValue) {
const hasValue = newValue !== null;
switch (name) {
case 'checked':
this.setAttribute('aria-checked', hasValue);
break;
case 'disabled':
this.setAttribute('aria-disabled', hasValue);
// Атрибут tabindex мешает удалению фокуса c элемента.
// Элементы с tabindex=-1 могут остаться в фокусе из-за указателя
// мыши или по вызову focus(). Для уверенности что элемент
// отключен и не в фокусе удаляем атрибут tabindex.
if (hasValue) {
this.removeAttribute('tabindex');
// Если элемент в фокусе, вызываем HTMLElement.blur()
this.blur();
} else {
this.setAttribute('tabindex', '0');
}
break;
}
}
_onKeyUp(event) {
// Не обрабатывать вспомогательные комбинации клавиш
if (event.altKey)
return;
switch (event.keyCode) {
case KEYCODE.SPACE:
event.preventDefault();
this._toggleChecked();
break;
// Игнорируем не указанные коды кнопок
default:
return;
}
}
_onClick(event) {
this._toggleChecked();
}
// _toggleChecked() вызывает сэттер и переворачивает состояние флага.
// Т.к. _toggleChecked() исполняется только по действию пользователя,
// то метод также генерирует событие изменения состояния.
// Режим bubbles используется для большей схожести с <input type=checkbox>.
_toggleChecked() {
if (this.disabled)
return;
this.checked = !this.checked;
this.dispatchEvent(new CustomEvent('change', {
detail: {
checked: this.checked,
},
bubbles: true,
}));
}
}
window.customElements.define('howto-checkbox', HowToCheckbox);
})();
Пример использования в HTML:
<style>
howto-checkbox {
vertical-align: middle;
}
howto-label {
vertical-align: middle;
display: inline-block;
font-weight: bold;
font-family: sans-serif;
font-size: 20px;
margin-left: 8px;
}
</style>
<howto-checkbox id="join-checkbox"></howto-checkbox>
<howto-label for="join-checkbox">Join Newsletter</howto-label>