При разработке современных веб-сайтов интенсивно используются возможности JavaScript по работе с DOM. Скрипты позволяют отображать и скрывать элементы, из которых строятся страницы, настраивать свойства этих элементов. У объектов DOM, с которыми взаимодействуют из программ, имеются свойства и методы. О некоторых из них, по мнению автора материала, перевод которого мы сегодня публикуем, знают практически все веб-программисты. А вот некоторые, о которых он и хочет здесь рассказать, пользуются куда меньшей известностью.
HTML-код и DOM
Для начала поговорим о разнице между HTML-кодом и DOM. Например, обычный элемент <table<
— это, очевидно, HTML-код. Этот элемент можно использовать в html-файлах, у него есть набор атрибутов, который определяет внешний вид и поведение создаваемой с его помощью таблицы. Собственно говоря, сам по себе тег <table>
не имеет никакого отношения к JavaScript. Связь между HTML-элементами, присутствующими в документе, и JavaScript-кодом, обеспечивает DOM (Document Object Model, объектная модель документа). DOM даёт возможность взаимодействовать с HTML-элементами из JavaScript-кода так, как будто они являются объектами.
У всех HTML-элементов есть собственные «DOM-интерфейсы», определяющие свойства (они обычно связаны с атрибутами HTML-элементов) и методы. Например, у элемента <table>
есть интерфейс, который называется HTMLTableElement.
Получить ссылку на некий элемент можно, например, воспользовавшись такой конструкцией:
const searchBox = document.getElementById('search-box');
После того, как ссылка на элемент получена, у программиста есть доступ к свойствам и методам, которые есть у подобных элементов. Например, работать со свойством value
некоего текстового поля можно, учитывая то, что ссылка на него хранится в переменной searchBox
, с помощью конструкции вида searchBox.value
. Поместить курсор в это текстовое поле можно, вызвав его метод searchBox.focus()
.
Пожалуй, на этом можно завершить наш «краткий курс по DOM» и перейти, собственно, к малоизвестным свойствам и методам DOM-интерфейсов HTML-элементов.
Если вы хотите читать и сразу же экспериментировать — откройте инструменты разработчика браузера. В частности, для того, чтобы получить ссылку на некий элемент страницы, можно выделить его в дереве элементов, а затем воспользоваться конструкцией $0
в консоли. Для того чтобы просмотреть элемент в виде объекта, введите в консоли команду dir($0)
. И, кстати, если вы наткнётесь на что-то для вас новое, попробуйте исследовать это с помощью консоли.
№1: методы таблиц
Скромный элемент <table>
(который всё ещё держит первое место среди технологий, используемых в деле разработки макетов веб-страниц) обладает порядочным числом очень хороших методов, которые значительно упрощают процесс конструирования таблиц.
Вот некоторые из них.
const tableEl = document.querySelector('table');
const headerRow = tableEl.createTHead().insertRow();
headerRow.insertCell().textContent = 'Make';
headerRow.insertCell().textContent = 'Model';
headerRow.insertCell().textContent = 'Color';
const newRow = tableEl.insertRow();
newRow.insertCell().textContent = 'Yes';
newRow.insertCell().textContent = 'No';
newRow.insertCell().textContent = 'Thank you';
Как видите, тут мы не пользуемся командами вроде document.createElement()
. А метод .insertRow()
, если вызвать его непосредственно для таблицы, даже обеспечит добавление <tbody>
. Разве не замечательно?
№2: метод scrollIntoView()
Вероятно вы знаете, что если в ссылке имеется конструкция вида #something
, то, после загрузки страницы, браузер автоматически прокрутит её к элементу с соответствующим ID
? Приём это удобный, но, в том случае, если интересующий нас элемент рендерится после загрузки страницы, работать он не будет. Вот как можно самостоятельно воссоздать такую схему поведения:
document.querySelector(document.location.hash).scrollIntoView();
№3: свойство hidden
Тут мы рассматриваем свойство, однако, вероятнее всего, при обращении к этому свойству будет вызван некий сеттер, который является методом. В любом случае, вспомните, доводилось ли вам, для того, чтобы скрыть элемент, использовать конструкцию, показанную ниже?
myElement.style.display = 'none'
Если вы ей пользуетесь — больше так делать не стоит. Для того чтобы скрыть элемент, достаточно записать true
в его свойство hidden
:
myElement.hidden = true
№4: метод toggle()
На самом деле, это — не метод некоего элемента. Это — метод свойства элемента. В частности, этот метод позволяет добавлять к элементу классы и удалять их из него, пользуясь следующей конструкцией:
myElement.classList.toggle('some-class')
Кстати, если вы когда-нибудь добавляли классы, пользуясь конструкцией if
, знайте, что больше вам так делать не придётся, и об этой конструкции забудьте. Тот же механизм реализуется с помощью второго параметра метода toggle()
. Если это — выражение, при вычислении которого получается true
, то класс, переданный toggle()
, будет добавлен к элементу.
el.classList.toggle('some-orange-class', theme === 'orange');
Вероятно, тут у вас могут появиться сомнения в адекватности этой конструкции. Ведь название метода, «toggle», которое, учитывая то, что в нём скрыта суть выполняемого им действия, можно перевести как «переключить», не содержит никаких упоминаний о том, что «переключение» подразумевает выполнение некоего условия. Однако вышеописанная конструкция существует именно в таком виде, хотя, разработчики Internet Explorer, вероятно, тоже считают её странной. В их реализации toggle()
второй параметр не предусмотрен. Поэтому, хотя выше и говорилось о том, что тем, кто знает о toggle()
, можно забыть о конструкции if
, забывать о ней, всё же, не стоит.
№5: метод querySelector()
О существовании этого метода вы, определённо, уже знаете, но есть подозрение, что в точности 17% из вас не знает о том, что использовать его можно в применении к любому элементу.
Например, конструкция myElement.querySelector('.my-class')
выберет лишь те элементы, которые имеют класс my-class
и при этом являются потомками элемента myElement
.
№6: метод closest()
Этот метод есть у всех элементов, которые поддерживают поиск по родительским элементам. Это нечто вроде обратного варианта querySelector()
. Пользуясь этим методом можно, например, получить заголовок для текущего раздела:
myElement.closest('article').querySelector('h1');
Тут, в ходе поиска, сначала обнаруживается первый родительский элемент <article>
, а потом — первый входящий в него элемент <h1>
.
№7: метод getBoundingClientRect()
Метод getBoundingClientRect()
возвращает приятно оформленный маленький объект, содержащий сведения о размерах элемента, для которого был вызван этот метод.
{
x: 604.875,
y: 1312,
width: 701.625,
height: 31,
top: 1312,
right: 1306.5,
bottom: 1343,
left: 604.875
}
Однако, пользуясь этим методом нужно проявлять осторожность, в частности, обращая внимание на два момента:
- Вызов этого метода приводит к перерисовке страницы. В зависимости от устройства, на котором просматривают страницу, и от сложности страницы, эта операция может занять несколько миллисекунд. Учитывайте это, если собираетесь вызывать этот метод в неких повторяющихся фрагментах кода, например — при выполнении анимации.
- Не все браузеры поддерживают этот метод.
№8: метод matches()
Предположим, нам надо проверить, имеет ли некий элемент некий класс.
Вот как решить эту задачу, видимо, самым сложным способом:
if (myElement.className.indexOf('some-class') > -1) {
// выполняем какие-то действия
}
Вот ещё один вариант, он лучше, но тоже далёк от идеала:
if (myElement.className.includes('some-class')) {
// выполняем какие-то действия
}
А вот — самый лучший способ решить эту задачу:
if (myElement.matches('.some-class')) {
// выполняем какие-то действия
}
№9: метод insertAdjacentElement()
Этот метод похож на appendChild()
, но даёт немного больше власти над тем, куда именно будет добавлен элемент-потомок.
Так, команда parentEl.insertAdjacentElement('beforeend', newEl)
аналогична команде parentEl.appendChild(newEl)
, но, используя метод insertAdjacentElement()
можно, помимо аргумента beforeend
, передавать ему аргументы beforebegin
, afterbegin
и afterend
, указывающие на место, куда надо добавить элемент.
№10: метод contains()
Вам когда-нибудь хотелось узнать, находится ли один элемент внутри другого? Мне это нужно постоянно. Например, если при обработке события щелчка мыши нужно узнать, произошло ли оно внутри модального окна или за его пределами (что говорит о том, что его можно закрыть), можно воспользоваться следующей конструкцией:
const handleClick = e => {
if (!modalEl.contains(e.target)) modalEl.hidden = true;
};
Здесь modalEl
— ссылка на модальное окно, а e.target
— это любой элемент, который щёлкнули мышью. Что интересно, когда я пользуюсь этим приёмом, то у меня никогда не получается с первого раза написать всё правильно, даже тогда, когда я вспоминаю, что постоянно тут ошибаюсь и пытаюсь заранее исправить возможные ошибки.
№11: метод getAttribute()
Пожалуй, этот метод можно назвать самым бесполезным, однако, есть одна ситуация, в которой он, определённо, может пригодиться.
Помните, выше мы говорили о том, что свойства объектов DOM обычно связаны с атрибутами HTML-элементов?
Один из случаев, когда это не так, представлен атрибутом href
, например, таким, как здесь: <a href="/animals/cat">Cat</a>
.
Конструкция el.href
не вернёт, как можно ожидать, /animals/cat
. Происходит так из-за того, что элемент <a>
реализует интерфейс HTMLHyperlinkElementUtils, у которого имеется множество вспомогательных свойств вроде protocol
и hash
, которые помогают выяснять подробности о ссылках.
Одним из таких вспомогательных свойств является свойство href
, которое даёт полный URL, включающий в себя всё то, чего нет у относительного URL в атрибуте.
В результате, для того, чтобы получить именно то, что записано в атрибут href
, нужно пользоваться конструкцией el.getAttribute('href')
.
№12: три метода элемента <dialog>
Сравнительно новый элемент <dialog>
обладает двумя полезными, но вполне обычными методами, и одним методом, который можно назвать просто замечательным. Итак, методы show()
и close()
выполняют в точности то, чего от них можно ожидать, показывая и скрывая окно. Их мы и называем полезными, но обычными. А вот метод showModal()
покажет элемент <dialog>
поверх всего остального, выведя его по центру окна. Собственно говоря, именно такого поведения обычно и ожидают от модальных окон. При работе с такими элементами не нужно задумываться о свойстве z-index
, вручную добавлять размытый фон, или прослушивать событие нажатия на клавишу Escape
для того, чтобы закрыть соответствующее окно. Браузер знает, как должны работать модальные окна и позаботится о том, чтобы всё действовало как надо.
№13: метод forEach()
Иногда, когда вы получаете ссылку на список элементов, перебирать эти элементы можно с помощью метода forEach()
. Циклы for()
— это вчерашний день. Предположим, нам надо вывести в лог ссылки всех элементов <a>
со страницы. Если сделать это так, как показано ниже, мы столкнёмся с сообщением об ошибке:
document.getElementsByTagName('a').forEach(el => {
console.log(el.href);
});
Для того чтобы решить эту задачу, можно воспользоваться следующей конструкцией:
document.querySelectorAll('a').forEach(el => {
console.log(el.href);
});
Дело тут в том, что методы наподобие getElementsByTagName()
возвращают объект типа HTMLCollection
, а querySelectorAll
— объект NodeList
. Именно интерфейс объекта NodeList
даёт нам доступ к методу forEach()
(а также — к методам keys()
, values()
и entries()
).
На самом деле, куда лучше было бы, если бы подобные методы просто возвращали бы обычные массивы, а не предлагали бы нам нечто, обладающее какими-то, вроде бы полезными, методами, не вполне похожее на массивы. Однако не стоит из-за этого расстраиваться, так как умные люди из ECMA дали нам отличный метод — Array.from()
, который позволяет превращать в массивы всё, что внешне похоже на массивы.
В результате можно написать следующее:
Array.from(document.getElementsByTagName('a')).forEach(el => {
console.log(el.href);
});
И вот ещё приятная мелочь. Преобразуя в массив то, что раньше было лишь на него похоже, мы получаем возможность пользоваться множеством методов массивов, таких, как map()
, filter()
и reduce()
. Вот, например, как сформировать массив внешних ссылок, имеющихся на странице:
Array.from(document.querySelectorAll('a'))
.map(el => el.origin)
.filter(origin => origin !== document.origin)
.filter(Boolean);
Кстати, конструкция .filter(Boolean)
очень нравится мне тем, что когда она встретится мне когда-нибудь в давно написанном мной коде, я далеко не сразу смогу понять её смысл.
№14: работа с формами
Вы, весьма вероятно, знаете о том, что у элемента <form>
есть метод submit()
. Однако, менее вероятно то, что вы знаете о наличии у форм метода reset()
, и о том, что у них есть метод reportValidity()
, который применим в тех случаях, когда используется проверка правильности заполнения элементов форм.
При работе с формами, кроме того, можно пользоваться их свойством elements
, которое, через точку, позволяет обращаться к элементам формы, используя их атрибуты name
. Например, конструкция myFormEl.elements.email
вернёт элемент <input name="email" />
, принадлежащий форме («принадлежащий» не обязательно означает «являющийся потомком»).
Тут надо отметить, что само свойство elements
не возвращает список обычных элементов. Оно возвращает список элементов управления (и этот список, конечно, не является массивом).
Вот пример. Если на форме имеются три радиокнопки и все они имеют одно и то же имя (animal
), то конструкция formEl.elements.animal
даст ссылку на набор радиокнопок (1 элемент управления, 3 HTML-элемента). А если воспользоваться конструкцией formEl.elements.animal.value
, то она выдаст значение выбранной пользователем радиокнопки.
Если над этим поразмыслить, то выглядит всё это довольно странно, поэтому разберёмся с предыдущим примером:
formEl
— это элемент.elements
— это объект HTMLFormControlsCollection, напоминающий массив, но им не являющийся. Его элементы не обязательно являются HTML-элементами.animal
— это набор из нескольких радиокнопок, представленных в виде набора из-за того, что все они имеют один и тот же атрибутname
(существует интерфейс RadioNodeList, предназначенный специально для работы с радиокнопками).value
используется для доступа к атрибутуvalue
активной радиокнопки, находящейся в коллекции.
№15: метод select()
Возможно, в самом конце материала лучше было бы рассказать о каком-нибудь совершенно потрясающем методе, хотя, может быть, и этот метод для кого-то станет открытием. Итак, метод .select()
позволяет выделять текст в полях ввода, для которых он вызывается.
Итоги
В этом материале мы рассказали о малоизвестных методах и свойствах, которые можно использовать для работы с содержимым веб-страниц. Надеемся, что вы нашли здесь что-то новое для себя, а, возможно — не только новое, но ещё и полезное.
Уважаемые читатели! Пользуетесь ли вы какими-нибудь способами программного взаимодействия с содержимым веб-страниц, не обладающими широкой известностью?
Автор: ru_vds