Dia 11 — A página: document e querySelector
DOM e a arvore do HTML
O DOM e a árvore do HTML
document é o objeto que dá acesso à página inteira. Tudo que o JavaScript faz no navegador passa por ele: ler um texto, trocar uma classe, criar um elemento. O nome completo é DOM (*Document Object Model*), e ele é a estrutura de objetos que o navegador monta depois de ler o arquivo HTML.
O que o DOM é
O HTML é um arquivo de texto. O DOM é a árvore DOM: a árvore de objetos que o navegador constrói a partir desse arquivo, já com as tags resolvidas, os atributos guardados e os elementos linkados por pai e filho.
Cada elemento do HTML vira um objeto JavaScript. A tag <h1 id="titulo">Olá</h1> vira um objeto com propriedades: o id é "titulo", o texto é "Olá", e o objeto tem pai e filhos. É essa árvore que o document entrega.
A raiz é o elemento html
A árvore tem um único nó raiz, e ele é o elemento <html>. Dentro dele vêm sempre dois filhos, nesta ordem: <head> (metadados, título, <script>, <style>) e <body> (tudo que aparece na tela).
| Nível | Nó | O que é |
|---|---|---|
| 0 | html | a raiz, document.documentElement |
| 1 | head | título, <script>, <style>, meta |
| 1 | body | tudo que o usuário vê na página |
| 2 | h1 id="titulo" | filho do body, com id e texto |
| 2 | p class="destaque" | filho do body, irmão do h1 |
Os dois filhos do <body> são irmãos: mesmo pai, mesmo nível. É essa vizinhança que o querySelector e os métodos de percorredor exploram.
DOM não é o arquivo HTML
O arquivo que chega do servidor é o HTML. O DOM é o que existe na memória depois que o navegador leu esse arquivo — e os dois divergem o tempo todo, porque o JavaScript altera o DOM sem alterar o arquivo. A pergunta "DOM vs HTML" tem resposta curta: HTML é o texto que veio do servidor, DOM é o estado atual da página.
Recarregar a página descarta todo o DOM montado e começa de novo a partir do HTML. Por isso um
appendChildque o aluno não salva em lugar nenhum some ao dar F5.
document, window e globalThis
São três portas de entrada para a mesma página, com escopos diferentes. window é o objeto global do navegador; document é uma das propriedades dele; globalThis é o nome padronizado do global, e vale tanto no navegador quanto no Node.
As três portas de entrada
| Objeto | O que é | Assinatura de acesso | Exemplo |
|---|---|---|---|
window | objeto global do navegador | window.document | window.alert('oi') |
document | a página, raiz do DOM | document.body | document.title |
globalThis | o global, com nome padronizado | globalThis.document | globalThis.setTimeout(...) |
No navegador, globalThis === window. O document também é acessível sem escrever nada: document.querySelector(...) funciona porque document é uma propriedade do global e o JavaScript resolve o nome sozinho.
O que costuma ser lido no document
| Propriedade | Assinatura | O que devolve |
|---|---|---|
document.documentElement | document.documentElement | o <html>, a raiz da árvore |
document.head | document.head | o <head>, com title e link |
document.body | document.body | o <body>, o conteúdo visível |
document.title | document.title | o texto da aba, em string |
document.forms | document.forms | a lista de <form> da página |
document.createElement | document.createElement(nomeDaTag) | um elemento novo, vazio |
document.body é o append habitual: o elemento criado entra na página e pode ser inspecionado no DevTools.
Node não é element
Na linguagem do dia a dia, "node" virou sinônimo de "elemento", mas na API são coisas diferentes. Node é qualquer pedaço da árvore: um elemento, um texto, um comentário. Element é o tipo de node que tem tag.
Os tipos de node
nodeType | Nome do tipo | O que é | Onde aparece |
|---|---|---|---|
1 | ELEMENT_NODE | um elemento com tag | <p>, <div> |
3 | TEXT_NODE | o texto dentro do elemento | o "Olá" de um <h1> |
8 | COMMENT_NODE | um comentário do HTML | <!-- nota --> |
9 | DOCUMENT_NODE | o próprio document | a raiz acima do <html> |
11 | FRAGMENT_NODE | um DocumentFragment | agrupar nós antes de inserir |
O nome do node está em nodeName: P para elemento, #text para texto. O nome da tag, sem cerquilha e em maiúsculas, está em tagName — e tagName só existe em elemento.
childNodes contra children
childNodes traz tudo, incluindo os nós de texto que o navegador criou sozinho a partir dos espaços e quebras de linha do HTML. children traz só elementos.
| Propriedade | Assinatura | O que devolve |
|---|---|---|
el.childNodes | elemento.childNodes | todos os nós filhos, texto incluído |
el.children | elemento.children | só os elementos filhos |
el.childElementCount | elemento.childElementCount | quantos elementos filhos existem |
el.textContent | elemento.textContent | todo o texto da subárvore, junto |
Um
<div>com três linhas de HTML dentro temchildNodes.lengthigual a5(três elementos e dois nós de texto em branco) echildren.lengthigual a3.for...ofsobrechildNodesé a causa clássica de "um item em branco apareceu na lista".
Percorrer a árvore a partir de um elemento
Achou o elemento raiz, a partir dele se chega a qualquer outro sem seletor. A API de navegação é pequena: pai, filhos, irmãos e um closest que sobe.
Vizinhos e parentes
| Propriedade | Assinatura | O que devolve |
|---|---|---|
el.parentElement | elemento.parentElement | o pai, ou null na raiz |
el.children | elemento.children | os elementos filhos |
el.firstElementChild | elemento.firstElementChild | o primeiro filho, ou null |
el.lastElementChild | elemento.lastElementChild | o último filho, ou null |
el.nextElementSibling | elemento.nextElementSibling | o irmão seguinte, ou null |
el.closest(seletor) | elemento.closest(seletorCSS) | o próprio elemento ou o primeiro ancestral que casa |
el.matches(seletor) | elemento.matches(seletorCSS) | true se o elemento casa com o seletor |
closest e matches recebem seletor CSS, exatamente o mesmo texto que vai no querySelector. É o mesmo critério de casamento nos dois casos.
Achar o elemento no DevTools
Inspecionar elemento é o atalho para não adivinhar seletor. Com a aba Elements aberta, clicar num elemento da página o destaca na árvore do DOM e o devolve no console como $0. Digitando $0 no console, o navegador entrega o elemento real para inspeção: $0.textContent, $0.className, $0.getAttribute('id').
Um seletor escrito à mão e colado no console (document.querySelector('h1')) é a forma mais rápida de testar se o seletor acha o que o aluno quer, antes de botá-lo no .js.
Exemplo
'use strict'; // ============================================================ // AULA 1 - DOM e a arvore do HTML: document, window e os tipos de node // ============================================================ // O exemplo monta uma pagina dentro do document e depois navega por ela, // mostrando que cada elemento do HTML e um objeto do DOM. // Esta e a pagina que o navegador entregou. O HTML e um arquivo de texto; // a partir dele o navegador construiu a arvore de objetos abaixo. document.body.innerHTML = ` <main id="conteudo"> <h1 id="titulo">Lista de compras</h1> <p class="destaque">Tres itens na semana</p> <ul id="lista"> <li class="item">Arroz</li> <li class="item">Feijao</li> </ul> </main> `; // --- A raiz da arvore: html, head e body --- console.log('=== a raiz ==='); console.log('documentElement:', document.documentElement.tagName); // HTML console.log('filhos de <html>:', [...document.documentElement.children].map((f) => f.tagName).join(', ')); console.log('head existe:', document.head.tagName, '| body existe:', document.body.tagName); console.log('document === window.document:', document === window.document); console.log('globalThis e a porta padronizada do global:', typeof globalThis.document); // --- Cada elemento e um objeto --- const titulo = document.querySelector('#titulo'); console.log('\n=== o objeto <h1> ==='); console.log('tagName:', titulo.tagName, '| nodeName:', titulo.nodeName, '| nodeType:', titulo.nodeType); console.log('id:', titulo.id, '| textContent:', titulo.textContent); console.log('o pai do <h1> e o:', titulo.parentElement.tagName + '#' + titulo.parentElement.id); // --- Node e element sao coisas diferentes --- console.log('\n=== node contra element ==='); const lista = document.querySelector('#lista'); console.log('tagName so existe em elemento; no texto da lista vale:', lista.firstChild.nodeName); console.log('childNodes (elementos + os textos em branco do HTML):', lista.childNodes.length); console.log('children (so elementos):', lista.children.length); // Um node de texto criado na mao e o tipo 3. Um elemento e o tipo 1. const texto = document.createTextNode('So um texto'); console.log('createTextNode -> nodeType:', texto.nodeType, '| nodeName:', texto.nodeName); // --- A tabela de tipos que a aula mostra --- const tipos = { 1: 'ELEMENT_NODE (um <p>, um <div>)', 3: 'TEXT_NODE (o texto dentro do elemento)', 8: 'COMMENT_NODE (um comentario do HTML)', 9: 'DOCUMENT_NODE (o proprio document)', 11: 'FRAGMENT_NODE (um DocumentFragment)', }; for (const [codigo, nome] of Object.entries(tipos)) { console.log(`nodeType ${codigo}: ${nome}`); } // --- Navegar pela arvore a partir de um elemento --- console.log('\n=== vizinhos e parentes ==='); const primeiro = lista.firstElementChild; const segundo = lista.lastElementChild; console.log('primeiro filho:', primeiro.textContent, '| ultimo filho:', segundo.textContent); console.log('proximo do primeiro e o segundo:', primeiro.nextElementSibling === segundo); console.log('anterior do segundo e o primeiro:', segundo.previousElementSibling === primeiro); console.log('closest("ul") acha o proprio pai:', primeiro.closest('ul') === lista); console.log('matches(".item"):', primeiro.matches('li.item'), '| matches(".outra"):', primeiro.matches('.outra')); console.log('o pai do pai:', primeiro.parentElement.parentElement.tagName + '#' + primeiro.parentElement.parentElement.id);
Saída real
=== a raiz ===
documentElement: HTML
filhos de <html>: HEAD, BODY
head existe: HEAD | body existe: BODY
document === window.document: true
globalThis e a porta padronizada do global: object
=== o objeto <h1> ===
tagName: H1 | nodeName: H1 | nodeType: 1
id: titulo | textContent: Lista de compras
o pai do <h1> e o: MAIN#conteudo
=== node contra element ===
tagName so existe em elemento; no texto da lista vale: #text
childNodes (elementos + os textos em branco do HTML): 5
children (so elementos): 2
createTextNode -> nodeType: 3 | nodeName: #text
nodeType 1: ELEMENT_NODE (um <p>, um <div>)
nodeType 3: TEXT_NODE (o texto dentro do elemento)
nodeType 8: COMMENT_NODE (um comentario do HTML)
nodeType 9: DOCUMENT_NODE (o proprio document)
nodeType 11: FRAGMENT_NODE (um DocumentFragment)
=== vizinhos e parentes ===
primeiro filho: Arroz | ultimo filho: Feijao
proximo do primeiro e o segundo: true
anterior do segundo e o primeiro: true
closest("ul") acha o proprio pai: true
matches(".item"): true | matches(".outra"): false
o pai do pai: MAIN#conteudo
querySelector e alterar o que esta na tela
Buscar elemento: seletor CSS
querySelector recebe um seletor CSS e devolve o primeiro elemento que casa com ele. querySelectorAll recebe o mesmo texto e devolve todos. Os dois se chamam a partir de qualquer elemento, e o mais usado é o document, que procura na página inteira.
Como se usa
const titulo = document.querySelector('h1'); // primeiro <h1> const item = document.querySelector('#titulo'); // elemento com id="titulo" const todos = document.querySelectorAll('li.campo'); // todos os <li class="campo">
O argumento é uma string. '#titulo' é seletor de id, '.campo' é seletor de classe, 'li' é seletor de tag, '[data-id]' é seletor de atributo, e a combinação é livre: 'form label.campo' acha a label de classe campo dentro de um form.
As formas de seletor que resolvem o caso comum
| Seletor | Assinatura | O que acha |
|---|---|---|
tag | querySelector('h1') | o primeiro h1 na ordem do documento |
#id | querySelector('#titulo') | o elemento com aquele id |
.classe | querySelector('.destaque') | o primeiro com aquela classe |
[atributo] | querySelector('[data-id]') | o elemento que tem o atributo |
[atributo="v"] | querySelector('[data-id="7"]') | o elemento com o atributo valendo "7" |
p, h2 | querySelector('p, h2') | o primeiro que casa com qualquer da lista |
div > p | querySelector('div > p') | filho direto, não neto |
.a .b | querySelector('lista .item') | descendente em qualquer nível |
O seletor de atributo, com os colchetes, é o único que alcança o que não tem nem tag nem classe: o data-*, o aria-*, o type de um campo.
O que devolve quando não acha
querySelector devolve null quando nada casa. null não tem propriedade: a linha seguinte quebra com TypeError: Cannot read properties of null.
| Situação | querySelector | querySelectorAll |
|---|---|---|
| achou um elemento | o elemento | NodeList com 1 item |
| achou vários | o primeiro da lista | NodeList com todos |
| não achou nada | null | NodeList vazia, com .length 0 |
O NodeList não é array: tem .length e percorre com for...of, mas não tem push e não tem map. Para transformar em array de verdade, o embrulho é [...document.querySelectorAll('li')].
Confundir os dois retornos gera o bug mais comum do dia:
querySelectorAllnunca devolvenull, então oif (itens)sempre passa e ofor...ofitera uma lista vazia sem avisar nada.
As formas antigas de buscar
getElementById e getElementsByClassName existem há mais tempo e ainda funcionam. A diferença de contrato é a que mais incomoda: os dois devolvem uma coleção viva, que se refaz sozinha quando a página muda, e nunca null.
| Método | Assinatura | Devolve |
|---|---|---|
querySelector | querySelector(seletorCSS) | um elemento ou null |
querySelectorAll | querySelectorAll(seletorCSS) | NodeList estática, pode ser vazia |
getElementById | document.getElementById(id) | um elemento ou null |
getElementsByClassName | document.getElementsByClassName(nome) | HTMLCollection viva, nunca vazia |
getElementsByTagName | document.getElementsByTagName(tag) | HTMLCollection viva |
Nem sempre é preciso seletor. Quando o que se quer é simplesmente o primeiro filho, o firstElementChild faz isso sem escrever texto de seletor: pai.firstElementChild devolve o primeiro filho que é elemento, e null se não houver nenhum.
A distinção que importa é com o firstChild: o firstElementChild pula nós de texto — os espaços e quebras de linha entre as tags — e devolve só elemento. O firstChild devolve o nó de texto em branco se ele vier primeiro, e é por isso que firstChild.nextSibling aparece em código antigo e quebra quando alguém muda a formatação do HTML.
ul.firstElementChild; // o primeiro <li> ul.firstElementChild.className; ul.firstElementChild === null; // true se a lista está vazia
Em código novo, querySelector cobre os dois casos: #id faz o papel do getElementById e devolve null em vez de coleção.
Ler e trocar o texto
Texto é a propriedade mais lida da página. São quatro propriedades, e a diferença entre elas decide se o conteúdo aparece com as tags ou não.
O que cada propriedade devolve
| Propriedade | Assinatura | O que faz |
|---|---|---|
textContent | el.textContent = 'novo' | o texto puro, ignora as tags e o CSS |
innerHTML | el.innerHTML = '<b>oi</b>' | o HTML dentro do elemento, com as tags |
innerText | el.innerText = 'novo' | o texto como aparece na tela, respeita o CSS |
value | campo.value = 'Ana' | o valor de input, textarea e select |
textContent apaga as tags ao ser lido e coloca texto puro ao ser escrito. innerHTML interpreta as tags: ler devolve a string de HTML, escrever monta elementos de verdade.
innerText é o intermediário: devolve o texto visível, então um elemento escondido por CSS não aparece nele. Ele também é mais lento, porque precisa recalcular o desenho da página. Em código que roda a cada tecla, textContent é o certo.
Trocar o texto de uma lista
document.querySelector('#total').textContent = '3 itens'; document.querySelector('#vazio').innerHTML = '<strong>Nada</strong> aqui';
Classes e atributos
Classe é o que o CSS usa para pintar, e por isso é o atributo que o JavaScript mais mexe. Ela tem uma API própria, a classList, que evita reescrever a string inteira e estragar as classes que não são suas.
classList
| Método | Assinatura | Efeito |
|---|---|---|
add | el.classList.add('ativa') | acrescenta a classe, repete não duplica |
remove | el.classList.remove('ativa') | tira a classe |
toggle | el.classList.toggle('ativa') | põe se não está, tira se está |
contains | el.classList.contains('ativa') | true se a classe está lá |
replace | el.classList.replace('velha', 'nova') | troca uma classe existente |
value | el.classList.value | a string de classes, para ler |
el.className = 'a b' reescreve a lista toda e apaga as classes que o CSS do projeto esperava. classList mexe numa só.
getAttribute e setAttribute
Atributo é o que está escrito no HTML. serve para os casos em que não existe propriedade com nome próprio — data-*, aria-*, href, type, disabled.
| Método | Assinatura | Efeito |
|---|---|---|
getAttribute | el.getAttribute('nome') | o valor do atributo, ou null |
setAttribute | el.setAttribute('nome', 'valor') | cria o atributo ou troca o valor |
hasAttribute | el.hasAttribute('nome') | true se o atributo existe |
removeAttribute | el.removeAttribute('nome') | apaga o atributo |
dataset | el.dataset.nomeUsuario | o mesmo atributo, em notação de ponto |
Atributo e propriedade não são a mesma coisa: getAttribute('value') devolve o que está escrito no HTML, enquanto campo.value devolve o que o usuário digitou agora. Depois que alguém digita, os dois divergem.
O dataset é a porta de entrada do data-*: data-id-usuario="7" no HTML vira el.dataset.idUsuario.
classListeclassNameleem sempre a lista de classes emclass, eidé uma propriedade direta:el.id = 'novo'. Nenhum dos dois é atributo de verdade, então não passam porgetAttribute.
Exemplo
'use strict'; // ============================================================ // AULA 2 - querySelector e alterar o que esta na tela // ============================================================ // O exemplo procura elementos por seletor CSS e depois mexe no texto, // nas classes e nos atributos, comparando as formas de ler e de trocar. document.body.innerHTML = ` <h1 id="titulo" data-pagina="1">Lista de compras</h1> <p class="destaque aviso">Arroz, feijao e cafe</p> <ul id="lista"> <li class="item" data-id="1">Arroz</li> <li class="item" data-id="2">Feijao</li> <li class="item" data-id="3">Cafe</li> </ul> `; // --- As formas de seletor --- console.log('=== seletor CSS ==='); console.log('por tag ->', document.querySelector('h1').textContent); console.log('por id ->', document.querySelector('#titulo').textContent); console.log('por classe ->', document.querySelector('.destaque').textContent); console.log('por atributo ->', document.querySelector('[data-id="2"]').textContent); console.log('combinado ->', document.querySelector('ul li.item').textContent); console.log('descendente->', document.querySelector('ul .item').textContent); console.log('filho direto ->', document.querySelector('ul > li').textContent); console.log('lista de seletores ->', document.querySelector('h1, .aviso').textContent); // --- querySelector devolve um; querySelectorAll devolve todos --- const itens = document.querySelectorAll('li.item'); console.log('\n=== querySelectorAll ==='); console.log('quantidade:', itens.length, '| e NodeList, nao array:', Array.isArray(itens)); console.log('o primeiro item:', itens[0].textContent); for (const item of itens) { console.log(' item', item.dataset.id, '=', item.textContent); } // --- O que acontece quando o seletor nao acha nada --- console.log('\n=== quando nao acha ==='); const naoExiste = document.querySelector('#nao-existe'); console.log('querySelector devolve:', naoExiste); console.log('querySelectorAll devolve lista com length:', document.querySelectorAll('.nao-existe').length); // E por isso que o if precisa do guarda: null nao tem propriedade. if (naoExiste) { console.log(naoExiste.textContent); } else { console.log('sem guarda, a linha acima quebraria com TypeError'); } // --- As formas antigas: colecao viva, nunca null --- console.log('\n=== getElementById e companhia ==='); console.log('getElementById("#lista"):', document.getElementById('lista').tagName); console.log('getElementsByClassName("item").length:', document.getElementsByClassName('item').length); // --- Ler e trocar texto --- console.log('\n=== texto ==='); const titulo = document.querySelector('#titulo'); console.log('textContent:', titulo.textContent); console.log('innerHTML:', titulo.innerHTML); titulo.textContent = 'Compras da semana'; console.log('depois de textContent =', titulo.textContent, '| innerHTML:', titulo.innerHTML); // textContent apaga as tags ao escrever; innerHTML interpreta o que veio. const lista = document.querySelector('#lista'); console.log('textContent da lista junta todo o texto:', JSON.stringify(lista.textContent)); const aviso = document.querySelector('.aviso'); aviso.innerHTML = '<strong>Atencao:</strong> falta o cafe'; console.log('depois de innerHTML, innerHTML:', aviso.innerHTML); console.log('e o textContent do mesmo elemento:', JSON.stringify(aviso.textContent)); // --- classList mexe numa classe sem apagar as outras --- console.log('\n=== classList ==='); const item = document.querySelector('li[data-id="2"]'); console.log('className inicial:', item.className); item.classList.add('comprado'); item.classList.remove('item'); console.log('depois de add e remove:', item.className); console.log('contains("comprado"):', item.classList.contains('comprado')); item.classList.toggle('urgente'); console.log('depois do toggle:', item.className); item.classList.replace('comprado', 'pago'); console.log('depois do replace:', item.className); // Escrever className inteiro apaga tudo que o CSS do projeto esperava. item.className = 'zerado'; console.log('className = "zerado" apagou o resto:', item.className); // --- getAttribute, setAttribute e dataset --- console.log('\n=== atributos ==='); const cafe = document.querySelector('li[data-id="3"]'); console.log('getAttribute("data-id"):', cafe.getAttribute('data-id')); console.log('dataset.id:', cafe.dataset.id); cafe.setAttribute('data-status', 'pendente'); console.log('depois do setAttribute:', cafe.outerHTML); console.log('hasAttribute("data-status"):', cafe.hasAttribute('data-status')); cafe.removeAttribute('data-status'); console.log('depois do removeAttribute:', cafe.hasAttribute('data-status')); // --- value e atributo sao coisas diferentes --- console.log('\n=== value ==='); const campo = document.createElement('input'); campo.setAttribute('value', 'Ana'); document.body.appendChild(campo); console.log('value:', campo.value); campo.value = 'Bia Souza'; console.log('depois de digitar, value:', campo.value); console.log('mas o atributo continua:', campo.getAttribute('value'));
Saída real
=== seletor CSS ===
por tag -> Lista de compras
por id -> Lista de compras
por classe -> Arroz, feijao e cafe
por atributo -> Feijao
combinado -> Arroz
descendente-> Arroz
filho direto -> Arroz
lista de seletores -> Lista de compras
=== querySelectorAll ===
quantidade: 3 | e NodeList, nao array: false
o primeiro item: Arroz
item 1 = Arroz
item 2 = Feijao
item 3 = Cafe
=== quando nao acha ===
querySelector devolve: null
querySelectorAll devolve lista com length: 0
sem guarda, a linha acima quebraria com TypeError
=== getElementById e companhia ===
getElementById("#lista"): UL
getElementsByClassName("item").length: 3
=== texto ===
textContent: Lista de compras
innerHTML: Lista de compras
depois de textContent = Compras da semana | innerHTML: Compras da semana
textContent da lista junta todo o texto: "\n Arroz\n Feijao\n Cafe\n "
depois de innerHTML, innerHTML: <strong>Atencao:</strong> falta o cafe
e o textContent do mesmo elemento: "Atencao: falta o cafe"
=== classList ===
className inicial: item
depois de add e remove: comprado
contains("comprado"): true
depois do toggle: comprado urgente
depois do replace: pago urgente
className = "zerado" apagou o resto: zerado
=== atributos ===
getAttribute("data-id"): 3
dataset.id: 3
depois do setAttribute: <li class="item" data-id="3" data-status="pendente">Cafe</li>
hasAttribute("data-status"): true
depois do removeAttribute: false
=== value ===
value: Ana
depois de digitar, value: Bia Souza
mas o atributo continua: Ana