Manipulação do DOM

Manipulação do DOM

Uma exploração no tipo HTMLElement

Nos mais de 20 anos desde sua padronização, o JavaScript tem percorrido um longo caminho. Enquanto em 2020, o JavaScript pode ser usado em servidores, em ciência de dados, e até mesmo em dispositivos Internet das Coisas (IoT), é importante lembrar seu caso de uso mais popular: navegadores web.

Websites são feitos de documentos HTML e/ou XML. Estes documentos são estáticos, eles não mudam. O Modelo de Objeto de Documento (DOM) é uma interface de programação implementada por navegadores para tornar sites estáticos funcionais. A API do DOM pode ser usada para alterar a estrutura do documento, estilo e conteúdo. A API é tão poderosa que inúmeras ferramentas de front-end (jQuery, React, Angular, etc.) foram desenvolvidos em torno dele, a fim de tornar os sites dinâmicos ainda mais fáceis de desenvolver.

TypeScript é um superconjunto do JavaScript, e envia definições de tipo para a API DOM. Essas definições estão prontamente disponíveis em qualquer projeto TypeScript padrão. Das mais de 20.000 linhas de definições em lib.dom.d.ts, uma se destaca entre as demais: HTMLElement. Este tipo é a espinha dorsal para a manipulação do DOM com TypeScript.

Você pode explorar o código-fonte para a definição de tipos do DOM

Exemplo Básico

Dado um arquivo index.html simplificado:

<!DOCTYPE html> <html lang="en"> <head><title>Manipulação do DOM com TypeScript</title></head> <body> <div id="app"></div> <!-- Assumindo que index.js é a saída compilada de index.ts --> <script src="index.js"></script> </body> </html>

Vamos explorar um script TypeScript que adiciona um elemento <p>Olá, Mundo!</p> ao elemento #app

ts
// 1. Seleciona o elemento div usando a propriedade id
const app = document.getElementById("app");
// 2. Cria um novo elemento <p></p> programáticamente
const p = document.createElement("p");
// 3. Adiciona conteúdo de texto
p.textContent = "Olá, Mundo!";
// 4. Acrescenta o elemento p no elemento div
app?.appendChild(p);

Depois de compilado e executando a página index.html, o resultado HTML será:

html
<div id="app">
<p>Olá, mundo!</p>
</div>

A interface Document

A primeira linha do código TypeScript usa uma variável global document. A inspeção da variável mostra que ela é definida pela interface Document do arquivo lib.dom.d.ts. O trecho de código contém chamadas para dois métodos, getElementById e createElement.

Document.getElementById

A definição para este método é a seguinte:

ts
getElementById(elementId: string): HTMLElement | null;

Passe o texto do id de um elemento e ele retornará HTMLElement ou null. Este método introduz um dos mais importantes tipos, HTMLElement. Ele serve como interface base para todas as outras interfaces de elementos. Por exemplo, a variável p no exemplo de código é do tipo HTMLParagraphElement. Também observe que este método pode retornar null. Isso ocorre porque o método não pode determinar em tempo de pré-execução se ele será capaz de encontrar realmente o elemento especificado ou não. Na última linha do trecho de código, o novo operador optional chaining é usado para chamar appendChild.

Document.createElement

A definição para este método é (eu omiti a definição depreciada)

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

Esta é uma definição de função sobrecarregada. A segunda sobrecarga é mais simples e funciona muito como o método getElementById faz. Passe qualquer string e ele irá retornar um HTMLElement padrão. Essa definição é o que permite aos desenvolvedores criar tags de elemento HTML exclusivas.

Por exemplo document.createElement('xyz') retorna um elemento <xyz></xyz>, claramente não é um elemento que esteja especificado pela especificação HTML.

Para os interessados, você pode interagir com tag de elementos customizados usando o document.getElementsByTagName

Para a primeira definição de createElement, é usado alguns padrões genéricos avançados. Ele é melhor entendido quando dividido em partes, começando com a expressão genérica: <K extends keyof HTMLElementTagNameMap>. Essa expressão define um parâmetro genérico K que é restrito às chaves da interface HTMLElementTagNameMap. A interface mapeada conté toda a especificação da tag HTML e seus tipos de interface correspondentes. Por exemplo, aqui estão os 5 primeiros valores mapeados:

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

Alguns elementos não exibem propriedades únicas e, então, eles apenas retornam HTMLElement, mas outros tipos tem propriedades e métodos únicos, então, eles retornam suas interfaces específicas (como irão extender ou implementar HTMLElement).

Agora, para o restante da definição do createElement: (tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K]. O primeiro argumento tagName é definido como um parâmetro genérico K. O interpretador TypeScript é inteligente o suficiente para inferir o parâmetro genérico para este argumento. Isso significa que o desenvolvedor não precisa especificar o parâmetro genérico que utiliza o método; qualquer valor que é passado para o argumento tagName será inferido como K e, portanto, pode ser usado em todo restante da definição. O que acontece exatamente; o valor retornado de HTMLElementTagNameMap[K] pega o argumento tagName e utiliza para retornar o tipo correspondente. Esta definição é como a variável p do trecho de código obtém o tipo HTMLParagraphElement. E se o código tem document.createElement('a'), então ele deve ser um tipo de elemento HTMLAnchorElement.

A interface Node

A função document.getElementById retorna um HTMLElement. A interface HTMLElement extende a interface Element que, por sua vez, extende a interface Node. Essa extensão a nível de protótipo permite a todos HTMLElements a utilizar um subconjunto de métodos padrão. No trecho de código, nós usamos uma propriedade definida na interface Node para anexar o novo elemento p ao website.

Node.appendChild

A última linha do trecho de código é app?.appendChild(p). A seção anterior, document.getElementById, detalha o que o operador optional chaining é usado aqui porque app pode ser potencialmente nulo durante a execução. O método appendChild é definido por:

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

Este método funciona de forma semelhante ao método createElement com o parâmetro genérico T sendo inferido do argumento newChild. T é restrito a outra interface base Node.

Diferença entre children e childNodes

Anteriormente, este documento detalhou a interface HTMLElement extendendo de Element que estende de Node. Na API DOM existe um conceito de elementos filhos. Por exemplo no HTML seguinte, as tags p são filhas do elemento div

tsx
<div>
<p>Olá, Mundo</p>
<p>TypeScript!</p>
</div>;
const div = document.getElementsByTagName("div")[0];
div.children;
// HTMLCollection(2) [p, p]
div.childNodes;
// NodeList(2) [p, p]

Depois de capturar o elemento div, a propriedade children irá retornar uma lista HTMLCollection contendo os HTMLParagraphElements. A propriedade childNodes irá retornar uma lista similar de nodes NodeList. Cada tag p irá permanecer sendo do tipo HTMLParagraphElements, mas o NodeList pode conter adicionalmente nós HTML que a lista HTMLCollection não contém.

Modifique o html para remover uma das tags p, mas deixe o texto.

tsx
<div>
<p>Olá, Mundo</p>
TypeScript!
</div>;
const div = document.getElementsByTagName("div")[0];
div.children;
// HTMLCollection(1) [p]
div.childNodes;
// NodeList(2) [p, text]

Veja como os duas listas mudaram. children agora contém apenas o elemento <p>Hello, World</p>, e o childNodes contém um nó text em vez de dois nós p. A parte text do NodeList é o Node literal contendo o texto TypeScript!. A lista children não contém este Node porque não é considerado um HTMLElement.

Os métodos querySelector e querySelectorAll

Ambos os métodos são ótimas ferramentas para obter listas de elementos do DOM que se encaixam em um conjunto mais exclusivo de restrições. Eles são definidos em lib.dom.d.ts como:

ts
/**
* Retorna o primeiro elemento do nó que é descendente do nó que corresponde aos seletores.
*/
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;
/**
* Retorna todos os elementos descendentes do nó que corresponde ao seletor
*/
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>;

A definição de querySelectorAll é similar a de getElementsByTagName, exceto que ele retorna um novo tipo: NodeListOf. Este tipo de retorno é essencialmente uma implementação customizada do elemento de lista padrão do JavaScript. Discutivelmente, substituindo NodeListOf<E> com E[] deve resultar em uma experiência do usuário muito similar. NodeListOf apenas implementa as seguintes propriedades e métodos: length , item(index), forEach((value, key, parent) => void), e indexação numérica. Adicionalmente, este método retorno uma lista de elementos, não nós, que é o que NodeList estava retornando para o método .childNodes. Enquanto isto pode parecer como uma discrepância, pegue nota que a interface Element extende de Node.

Para ver estes métodos em ação modifique o código existente para:

tsx
<ul>
<li>Primeiro :)</li>
<li>Segundo!</li>
<li>Terceira vez um encanto.</li>
</ul>;
const primeiro = document.querySelector("li"); // retorna o primeiro elemento 'li'
const todos = document.querySelectorAll("li"); // retorna a lista de todos os elementos 'li'

Interessado em aprender mais?

A melhor parte sobre as definições de tipo lib.dom.d.ts é que elas refletem os tipos anotados no site de documentação da Rede de Desenvolvedores Mozilla (Mozilla Developer Network - MDN). Por exemplo, a interface HTMLElement é documentada pela página HTMLElement na MDN. Estas páginas listam todas as propriedades disponíveis, métodos, e até mesmo alguns exemplos. Outro grande aspecto das páginas é que elas fornecem links para os documentos padrão correspondentes. Este é o link para a Recomendação da W3C para HTMLElement.

Recursos:

The TypeScript docs are an open source project. Help us improve these pages by sending a Pull Request

Contributors to this page:
OTOrta Therox  (6)
EAEthan Arrowood  (6)
DADSDavid Alves de Souza  (5)
Ddavidalves1  (2)
SRShane Riley  (1)
3+

Last updated: 24 de jun. de 2022