Spec-Zone.ru › TypeScript 5.1

Обработка DOM

Обработка DOM

Исследование типа HTMLElement

За 20+ лет с момента стандартизации JavaScript прошёл долгий путь. Хотя в 2020 году JavaScript можно использовать на серверах, в науке о данных и даже на устройствах Интернета вещей, важно помнить его наиболее популярное применение: веб-браузеры.

Веб-сайты состоят из HTML- и/или XML-документов. Эти документы статичны, они не меняются. Модель объекта документа (DOM) — это программный интерфейс, реализованный браузерами, чтобы сделать статические веб-сайты функциональными. API DOM можно использовать для изменения структуры, стиля и содержимого документа. API настолько мощный, что вокруг него было разработано бесчисленное количество фреймворков для разработки front-end (jQuery, React, Angular и т. д.), чтобы сделать динамические веб-сайты ещё проще разрабатывать.

TypeScript — это типизированный супернабор JavaScript, и он поставляется с определениями типов для API DOM. Эти определения легко доступны в любом стандартном проекте TypeScript. Из более чем 20 000 строк определений в файле lib.dom.d.ts выделяется один: HTMLElement. Этот тип является основой для работы с DOM в TypeScript.

Вы можете изучить исходный код определений типа DOM типов

Базовый пример

Рассмотрим упрощённый файл index.html:

<!DOCTYPE html>
<html lang="en">
  <head><title>TypeScript Dom Manipulation</title></head>
  <body>
    <div id="app"></div>
    <!-- Assume index.js is the compiled output of index.ts -->
    <script src="index.js"></script>
  </body>
</html>

Давайте изучим скрипт TypeScript, который добавляет элемент <p>Hello, World!</p> к элементу #app.

// 1. Select the div element using the id property
const app = document.getElementById("app");

// 2. Create a new <p></p> element programmatically
const p = document.createElement("p");

// 3. Add the text content
p.textContent = "Hello, World!";

// 4. Append the p element to the div element
app?.appendChild(p);

После компиляции и выполнения страницы index.html результатом будет:

<div id="app">
  <p>Hello, World!</p>
</div>

Интерфейс Document

Первая строка кода TypeScript использует глобальную переменную document. Проверка переменной показывает, что она определена интерфейсом Document из файла lib.dom.d.ts. Сниппет кода содержит вызовы двух методов: getElementById и createElement.

Document.getElementById

Определение этого метода следующее:

getElementById(elementId: string): HTMLElement | null;

Передайте ему строку идентификатора элемента, и он вернёт либо HTMLElement либо null. Этот метод вводит один из наиболее важных типов, HTMLElement. Он служит базовым интерфейсом для всех остальных интерфейсов элементов. Например, переменная p в примере кода имеет тип HTMLParagraphElement. Также обратите внимание, что этот метод может возвращать null. Это связано с тем, что метод не может быть уверен до выполнения, сможет ли он фактически найти указанный элемент или нет. В последней строке кода используется новый оператор необязательного цепного вызова для вызова appendChild.

Document.createElement

Определение этого метода (я опустил устаревшее определение):

createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
createElement(tagName: string, options?: ElementCreationOptions): HTMLElement;

Это перегруженное определение функции. Вторая перегрузка проще и работает очень похоже на метод getElementById. Передайте любой string и он вернёт стандартный HTMLElement. Это определение позволяет разработчикам создавать уникальные теги HTML-элементов.

Например, document.createElement('xyz') возвращает элемент <xyz></xyz>, который явно не является элементом, определённым спецификацией HTML.

Для тех, кто заинтересовался, вы можете взаимодействовать с элементами пользовательских тегов с помощью document.getElementsByTagName

Для первого определения createElement используется несколько продвинутых шаблонов дженериков. Его лучше понимать, разбив на части, начиная с дженерикового выражения: <K extends keyof HTMLElementTagNameMap>. Это выражение определяет дженерический параметр K, который ограничен ключами интерфейса HTMLElementTagNameMap. Интерфейс map содержит все указанные имена HTML-тегов и соответствующие интерфейсы типов. Например, вот первые 5 сопоставленных значений:

interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "abbr": HTMLElement;
    "address": HTMLElement;
    "applet": HTMLAppletElement;
    "area": HTMLAreaElement;
        ...
}

Некоторые элементы не демонстрируют уникальных свойств, и поэтому возвращают HTMLElement, но другие типы имеют уникальные свойства и методы, поэтому они возвращают свои специфические интерфейсы (которые будут расширять или реализовывать HTMLElement).

Теперь для оставшейся части определения createElement: (tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K]. Первый аргумент tagName определён как дженерический параметр K. Интерпретатор TypeScript достаточно умен, чтобы вывести дженерический параметр из этого аргумента. Это означает, что разработчику не нужно указывать дженерический параметр при использовании метода; какое значение передаётся в аргумент tagName, выводится как K и, таким образом, может использоваться на протяжении всего остального определения. Именно это происходит; возвращаемое значение HTMLElementTagNameMap[K] берёт аргумент tagName и использует его для возвращения соответствующего типа. Это определение — то, как переменная p из кода получает тип HTMLParagraphElement. А если бы код был document.createElement('a'), тогда это был бы элемент типа HTMLAnchorElement.

Интерфейс Node

Функция document.getElementById возвращает HTMLElement. Интерфейс HTMLElement расширяет интерфейс Element, который расширяет интерфейс Node. Это прототипное расширение позволяет всем HTMLElements использовать подмножество стандартных методов. В примере кода мы используем свойство, определённое в интерфейсе Node, чтобы добавить новый элемент p на веб-сайт.

Node.appendChild

Последняя строка кода — app?.appendChild(p). В предыдущем разделе document.getElementById было показано, что здесь используется оператор необязательного цепного вызова, потому что app потенциально может быть null во время выполнения. Метод appendChild определяется так:

appendChild<T extends Node>(newChild: T): T;

Этот метод работает аналогично методу createElement, так как дженерический параметр T выводится из аргумента newChild. T ограничен другим базовым интерфейсом Node.

Различия между children и childNodes

Ранее в этом документе говорилось, что интерфейс HTMLElement расширяет интерфейс Element, который расширяет интерфейс Node. В API DOM есть понятие элементов children. Например, в следующем HTML-коде теги p являются дочерними элементами элемента div

<div>
  <p>Hello, World</p>
  <p>TypeScript!</p>
</div>;

const div = document.getElementsByTagName("div")[0];

div.children;
// HTMLCollection(2) [p, p]

div.childNodes;
// NodeList(2) [p, p]

После захвата элемента div свойство children вернёт список HTMLCollection, содержащий HTMLParagraphElements. Свойство childNodes вернёт аналогичный список узлов NodeList. Каждый тег p по-прежнему будет типа HTMLParagraphElements, но NodeList может содержать дополнительные узлы HTML, которых нет в списке HTMLCollection.

Измените HTML, удалив один из тегов p, но сохраните текст.

<div>
  <p>Hello, World</p>
  TypeScript!
</div>;

const div = document.getElementsByTagName("div")[0];

div.children;
// HTMLCollection(1) [p]

div.childNodes;
// NodeList(2) [p, text]

Посмотрите, как изменяются оба списка. children теперь содержит только элемент <p>Hello, World</p>, а childNodes содержит узел text, а не два узла p. Часть text в NodeList — это буквальный Node, содержащий текст TypeScript!. Список children не содержит этот узел Node, потому что он не считается элементом HTMLElement.

Методы querySelector и querySelectorAll

Оба этих метода являются отличными инструментами для получения списков элементов dom, которые соответствуют более уникальному набору ограничений. Они определены в lib.dom.d.ts так:

/**
 * Returns the first element that is a descendant of node that matches selectors.
 */
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;

/**
 * Returns all element descendants of node that match selectors.
 */
querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;

Определение querySelectorAll похоже на getElementsByTagName, за исключением того, что возвращает новый тип: NodeListOf. Этот возвращаемый тип по существу представляет собой пользовательскую реализацию стандартного элемента списка JavaScript. Вероятно, замена NodeListOf<E> на E[] привела бы к очень похожему пользовательскому опыту. NodeListOf реализует только следующие свойства и методы: length, item(index), forEach((value, key, parent) => void), и числовую индексацию. Кроме того, этот метод возвращает список элементов, а не узлов, что и возвращалось NodeList методом .childNodes. Хотя это может показаться расхождением, обратите внимание, что интерфейс Element расширяет Node.

Чтобы увидеть эти методы в действии, измените существующий код на:

<ul>
  <li>First :)</li>
  <li>Second!</li>
  <li>Third times a charm.</li>
</ul>;

const first = document.querySelector("li"); // returns the first li element
const all = document.querySelectorAll("li"); // returns the list of all li elements

Хотите узнать больше?

Лучшая часть определений типов в lib.dom.d.ts заключается в том, что они отражают типы, анимированные на сайте документации Mozilla Developer Network (MDN). Например, интерфейс HTMLElement документирован на этой странице HTMLElement на MDN. Эти страницы перечисляют все доступные свойства, методы и иногда даже примеры. Ещё одна замечательная особенность страниц заключается в том, что они предоставляют ссылки на соответствующие стандартные документы. Вот ссылка на рекомендацию W3C для HTMLElement.

Источники:

  • Стандарт ECMA-262
  • Введение в DOM

© 2012-2024 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/dom-manipulation.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API