HTTP e Formulários

O que muitas vezes era difícil para as pessoas entenderem sobre o design era que não havia nada além de URLs, HTTP e HTML. Não havia nenhum computador central “controlando” a web, nenhuma rede única na qual esses protocolos funcionassem, nem mesmo uma organização em qualquer lugar que “administrasse” a Web. A Web não era uma “coisa” física que existia em um determinado “lugar”. Era um “espaço” no qual a informação podia existir.

Tim Berners-Lee
Ilustração mostrando um formulário de inscrição na web em um pergaminho de papiro

O Hypertext Transfer Protocol, introduzido no Capítulo 13, é o mecanismo através do qual dados são solicitados e fornecidos na World Wide Web. Este capítulo descreve o protocolo em mais detalhes e explica a forma como o JavaScript do navegador tem acesso a ele.

O protocolo

Se você digitar eloquentjavascript.net/18_http.html na barra de endereço do seu navegador, o navegador primeiro procura o endereço do servidor associado a eloquentjavascript.net e tenta abrir uma conexão TCP para ele na porta 80, a porta padrão para tráfego HTTP. Se o servidor existir e aceitar a conexão, o navegador pode enviar algo assim:

GET /18_http.html HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Your browser's name

Então o servidor responde, através dessa mesma conexão.

HTTP/1.1 200 OK
Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

<!doctype html>
... o restante do documento

O navegador pega a parte da resposta após a linha em branco, seu body (ou seja, o corpo, não confundir com a tag HTML <body>), e a exibe como um documento HTML.

A informação enviada pelo cliente é chamada de requisição (request). Ela começa com esta linha:

GET /18_http.html HTTP/1.1

A primeira palavra é o método da requisição. GET significa que queremos obter o recurso especificado. Outros métodos comuns são DELETE para deletar um recurso, PUT para criá-lo ou substituí-lo, e POST para enviar informações a ele. Note que o servidor não é obrigado a executar todas as requisições que recebe. Se você acessar um site aleatório e mandar ele DELETE sua página principal, ele provavelmente vai recusar.

A parte após o nome do método é o caminho do recurso ao qual a requisição se aplica. No caso mais simples, um recurso é simplesmente um arquivo no servidor, mas o protocolo não exige que seja assim. Um recurso pode ser qualquer coisa que pode ser transferida como se fosse um arquivo. Muitos servidores geram as respostas que produzem em tempo real. Por exemplo, se você abrir https://github.com/marijnh, o servidor procura em seu banco de dados por um usuário chamado “marijnh”, e se encontrar, gera uma página de perfil para esse usuário.

Após o caminho do recurso, a primeira linha da requisição menciona HTTP/1.1 para indicar a versão do protocolo HTTP que está sendo usada.

Na prática, muitos sites usam a versão 2 do HTTP, que suporta os mesmos conceitos da versão 1.1, mas é muito mais complicada para que possa ser mais rápida. Os navegadores automaticamente alternam para a versão do protocolo apropriada ao conversar com um determinado servidor, e o resultado de uma requisição é o mesmo independentemente da versão usada. Como a versão 1.1 é mais simples e fácil de experimentar, usaremos ela para ilustrar o protocolo.

A resposta do servidor também começa com uma versão, seguida pelo status da resposta, primeiro como um código de status de três dígitos e depois como uma string legível por humanos.

HTTP/1.1 200 OK

Códigos de status que começam com 2 indicam que a requisição foi bem sucedida. Códigos que começam com 4 significam que houve algo errado com a requisição. O código de status HTTP mais famoso é provavelmente o 404, que significa que o recurso não foi encontrado. Códigos que começam com 5 significam que um erro aconteceu no servidor e a requisição não é a culpada.

A primeira linha de um requisição ou resposta pode ser seguida por qualquer número de headers (cabeçalhos). Essas são linhas no formato nome: valor que especificam informações extras sobre a requisição ou resposta. Os headers abaixo fazem parte do exemplo da resposta do servidor que vimos:

Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

Isso nos informa o tamanho e o tipo do documento da resposta. Neste caso, é um documento HTML de 87.320 bytes. Também nos informa quando esse documento foi modificado pela última vez.

O cliente (client) e o servidor (server) são livres para decidir quais headers incluir em suas requisições ou respostas. Mas alguns deles são necessários para que as coisas funcionem. Por exemplo, sem um header Content-Type na resposta, o navegador não saberá como exibir o documento.

Após os headers, tanto requisições quanto respostas podem incluir uma linha em branco seguida de um body (corpo), que contém o documento real enviado. Requisições GET e DELETE não enviam dados, mas requisições PUT e POST enviam. Alguns tipos de resposta, como respostas de erro, também não requerem body.

Navegadores e HTTP

Como vimos, um navegador fará uma requisição quando inserirmos uma URL em sua barra de endereços. Quando a página HTML resultante referencia outros arquivos, como images e arquivos JavaScript, ele também os buscará.

Um website moderadamente complicado pode facilmente incluir de 10 a 200 recursos. Para conseguir buscar esses rapidamente, navegadores farão várias requisições GET simultaneamente, ao invés de esperar pelas respostas uma a uma.

Páginas HTML podem incluir formulários, que permitem ao usuário preencher informações e enviá-las ao servidor. Este é um exemplo de um formulário:

<form method="GET" action="example/message.html">
  <p>Name: <input type="text" name="name"></p>
  <p>Message:<br><textarea name="message"></textarea></p>
  <p><button type="submit">Send</button></p>
</form>

Este código descreve um formulário com dois campos: um pequeno pedindo um nome e um maior para escrever uma mensagem. Quando você clica no botão Send, o formulário é enviado, significando que o conteúdo de seus campos é empacotado numa requisição HTTP e o navegador navega para o resultado dessa requisição.

Quando o atributo method do elemento <form> é GET (ou omitido), a informação do form é adicionada ao fim da URL do action como uma query string. O navegador pode fazer uma requisição para esta URL:

GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1

A interrogação indica o fim da parte do caminho do URL e o início da query. Ele é seguido por pares de nomes e valores, correspondendo ao atributo name nos elementos dos campos do form e ao conteúdo desses elementos, respectivamente. Um caractere e comercial (&) é usado para separar os pares.

A mensagem real codificada na URL é “Yes?” mas o ponto de interrogação é substituído por um código estranho. Alguns caracteres em query strings precisam ser escapados. O ponto de interrogação, representado como %3F, é um desses. Parece haver uma regra não escrita de que cada formato precisa de sua própria forma de escapar caracteres. Este, chamado URL encoding, usa um símbolo de porcentagem seguido por dois dígitos hexadecimais (base 16) que codificam o código do caractere. Neste caso, 3F, que é 63 em notação decimal, é o código do caractere ponto de interrogação. JavaScript fornece as funções encodeURIComponent e decodeURIComponent para codificar e decodificar esse formato.

console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?

Se mudarmos o atributo method do form HTML no exemplo anterior para POST, a requisição HTTP feito para submeter o form usará o método POST e colocará a _query string_ no body da requisição ao invés de adicioná-la à URL.

POST /example/message.html HTTP/1.1
Content-length: 24
Content-type: application/x-www-form-urlencoded

name=Jean&message=Yes%3F

Requisições GET devem ser usados para requisições que não tenham side effects mas que apenas solicitem informação. Requisições que mudam algo no servidor, por exemplo criar uma nova conta ou postar uma mensagem, devem ser expressos com outros métodos, como POST. Software no lado do cliente, como um navegador, sabe que não deve fazer requisições POST cegamente, mas frequentemente fará implicitamente requisições GET — para pré-buscar (prefetch) um recurso que acredita que o usuário logo precisará, por exemplo.

Voltaremos aos formulários e como interagir com eles a partir do JavaScript mais adiante no capítulo.

Fetch

A interface pela qual o JavaScript do navegador pode fazer requisições HTTP é chamada fetch.

fetch("example/data.txt").then(response => {
  console.log(response.status);
  // → 200
  console.log(response.headers.get("Content-Type"));
  // → text/plain
});

Chamar fetch retorna uma promise que resolve para um objeto Response que contém informações sobre a resposta do servidor, como o código de status e seus headers. Os headers são embrulhados em um objeto semelhante a um Map que trata suas chaves (os nomes dos headers) como case insensitive porque os nomes dos headers não devem ser sensíveis a maiúsculas e minúsculas. Isso significa que headers.get("Content-Type") e headers.get("content-TYPE") retornam o mesmo valor.

Note que a promise retornada por fetch resolve com sucesso mesmo que o servidor tenha respondido com um código de erro. Ela também pode ser rejeitada se houver um erro de rede ou se o servidor para o qual a requisição foi feita não puder ser encontrado.

O primeiro argumento de fetch é a URL que deve ser requisitada. Quando essa URL não começa com um nome de protocolo (como http:), ela é tratada como relativa, o que significa que é interpretada em relação ao documento atual. Quando começa com uma barra (/), ela substitui o caminho atual, que é a parte após o nome do servidor. Quando não começa, a parte do caminho atual até e incluindo seu último caractere de barra é colocada antes da URL relativa.

Para obter o conteúdo real de uma resposta, você pode usar seu método text. Porque a promise inicial é resolvida assim que os headers da resposta são recebidos e porque ler o corpo da resposta pode levar um pouco mais de tempo, isso retorna outra promise.

fetch("example/data.txt")
  .then(resp => resp.text())
  .then(text => console.log(text));
// → This is the content of data.txt

Um método semelhante, chamado json, retorna uma promise que resolve para o valor que você obtém ao analisar o corpo como JSON ou rejeita se não for um JSON válido.

Por padrão, fetch usa o método GET para fazer sua requisição e não inclui um corpo de requisição. Você pode configurá-lo de forma diferente passando um objeto com opções extras como segundo argumento. Por exemplo, esta requisição tenta deletar example/data.txt:

fetch("example/data.txt", {method: "DELETE"}).then(resp => {
  console.log(resp.status);
  // → 405
});

O código de status 405 significa “método não permitido”, uma forma do servidor HTTP dizer “receio que não posso fazer isso”.

Para adicionar um corpo de requisição para uma requisição PUT ou POST, você pode incluir a opção body. Para configurar headers, existe a opção headers. Por exemplo, esta requisição inclui um header Range, que instrui o servidor a retornar apenas parte de um documento.

fetch("example/data.txt", {headers: {Range: "bytes=8-19"}})
  .then(resp => resp.text())
  .then(console.log);
// → the content

O navegador adicionará automaticamente alguns headers de requisição, como Host e aqueles necessários para o servidor calcular o tamanho do corpo. Mas adicionar seus próprios headers é frequentemente útil para incluir coisas como informação de autenticação ou para informar ao servidor qual formato de arquivo você gostaria de receber.

Sandboxing HTTP

Fazer requisições HTTP em scripts de páginas web novamente levanta preocupações sobre segurança. A pessoa que controla o script pode não ter os mesmos interesses da pessoa cujo computador está rodando o script. Mais especificamente, se eu visito themafia.org, eu não quero que seus scripts possam fazer uma requisição para mybank.com, usando informações identificadoras do meu navegador, com instruções para transferir todo o meu dinheiro.

Por esse motivo, os navegadores nos protegem proibindo scripts de fazer requisições HTTP para outros domínios (nomes como themafia.org e mybank.com).

Esse pode ser um problema irritante ao construir sistemas que querem acessar vários domínios por razões legítimas. Felizmente, servidores podem incluir um header assim em sua resposta para indicar explicitamente ao navegador que está tudo bem o pedido ser feito de outro domínio:

Access-Control-Allow-Origin: *

Apreciando HTTP

Ao construir um sistema que requer comunicação entre um programa JavaScript rodando no navegador (lado cliente) e um programa em um servidor (lado servidor), há várias maneiras de modelar essa comunicação.

Um modelo comumente usado é o de chamadas remotas de procedimento (rpc). Nesse modelo, a comunicação segue os padrões das chamadas normais de função, exceto que a função está de fato rodando em outra máquina. Chamá-la envolve fazer um pedido ao servidor que inclui o nome da função e os argumentos. A resposta a esse pedido contém o valor retornado.

Ao pensar em termos de chamadas remotas de procedimento, HTTP é apenas um veículo para comunicação, e você muito provavelmente escreverá uma camada de abstração que o esconda completamente.

Outra abordagem é construir sua comunicação em torno do conceito de recursos e métodos HTTP. Em vez de uma função remota chamada addUser, você usa um pedido PUT para /users/larry. Em vez de codificar as propriedades daquele usuário nos argumentos da função, você define um formato de documento JSON (ou usa um formato existente) que representa um usuário. O corpo do pedido PUT para criar um novo recurso é então tal documento. Um recurso é buscado fazendo um pedido GET para a URL do recurso (por exemplo, /users/larry), que novamente retorna o documento representando o recurso.

Essa segunda abordagem facilita o uso de algumas das funcionalidades que o HTTP oferece, como suporte a cache de recursos (manter uma cópia de um recurso no cliente para acesso rápido). Os conceitos usados no HTTP, que são bem desenhados, podem fornecer um conjunto útil de princípios para você desenhar a interface do seu servidor ao redor.

Segurança e HTTPS

Dados viajando pela internet tendem a seguir um caminho longo e perigoso. Para chegar ao seu destino, ele deve passar por qualquer coisa desde pontos de Wi-Fi em cafeterias até redes controladas por várias empresas e estados. Em qualquer ponto do trajeto, ele pode ser inspecionado ou mesmo modificado.

Se for importante que algo permaneça secreto, como a senha da sua conta de email, ou que chegue ao seu destino sem modificações, como o número da conta para a qual você transfere dinheiro via site do seu banco, o HTTP simples não é suficiente.

O protocolo seguro HTTP, usado para URLs que começam com https://, encapsula o tráfego HTTP de um jeito que dificulta sua leitura e modificação. Antes de trocar dados, o cliente verifica que o servidor é quem diz ser pedindo que ele prove que tem um certificado criptográfico emitido por uma autoridade certificadora que o navegador reconhece. Depois, todos os dados trafegando pela conexão são criptografados de forma que deve impedir espionagem e adulteração.

Assim, quando funciona direito, HTTPS impede que outras pessoas se passem pelo site com o qual você quer se comunicar e de bisbilhotar sua comunicação. Não é perfeito, e houve vários incidentes em que o HTTPS falhou por causa de certificados falsificados ou roubados e softwares com problemas, mas é muito mais seguro que o HTTP simples.

Campos de formulário

Os formulários foram originalmente projetados para a web pré-JavaScript para permitir que sites enviassem informações submetidas pelo usuário em um pedido HTTP. Esse design assume que a interação com o servidor sempre acontece navegando para uma nova página.

No entanto, os elementos de formulário fazem parte do DOM, como o resto da página, e os elementos DOM que representam os campos de formulário suportam várias propriedades e eventos que não estão presentes em outros elementos. Isso torna possível inspecionar e controlar esses campos de entrada com programas JavaScript e fazer coisas como adicionar novas funcionalidades a um formulário ou usar formulários e campos como blocos construtivos em uma aplicação JavaScript.

Um formulário web consiste em qualquer número de campos de entrada agrupados em uma tag <form>. O HTML permite vários estilos diferentes de campos, desde caixas de seleção simples on/off até menus drop-down e campos para entrada de texto. Este livro não tentará discutir todos os tipos de campo de forma abrangente, mas começaremos com uma visão geral básica.

Muitos tipos de campo usam a tag <input>. O atributo type dessa tag é usado para selecionar o estilo do campo. Estes são alguns tipos de <input> comumente usados:

textUm text field de uma linha
passwordIgual ao text, mas esconde o texto digitado
checkboxUm botão liga/desliga
colorUma cor
dateUma data no calendário
radio(Parte de) um campo de múltipla escolha
filePermite ao usuário escolher um arquivo do computador

Campos de formulário não precisam necessariamente aparecer dentro de uma tag <form>. Você pode colocá-los em qualquer lugar na página. Esses campos sem formulário não podem ser submitdos (apenas um formulário inteiro pode), mas ao responder a entradas com JavaScript, muitas vezes não queremos submeter nossos campos da forma tradicional mesmo.

<p><input type="text" value="abc"> (texto)</p>
<p><input type="password" value="abc"> (senha)</p>
<p><input type="checkbox" checked> (caixa de seleção)</p>
<p><input type="color" value="orange"> (cor)</p>
<p><input type="date" value="2023-10-13"> (data)</p>
<p><input type="radio" value="A" name="choice">
   <input type="radio" value="B" name="choice" checked>
   <input type="radio" value="C" name="choice"> (botão de opção)</p>
<p><input type="file"> (arquivo)</p>

A interface JavaScript para esses elementos varia conforme o tipo do elemento.

Campos de texto multilinha possuem sua própria tag, <textarea>, principalmente porque usar um atributo para especificar um valor inicial multilinha seria estranho. A tag <textarea> requer uma tag de fechamento </textarea> correspondente e usa o texto entre essas duas, ao invés do atributo value, como texto inicial.

<textarea>
um
dois
três
</textarea>

Por fim, a tag <select> é usada para criar um campo que permite ao usuário selecionar entre várias opções pré-definidas.

<select>
  <option>Panquecas</option>
  <option>Pudim</option>
  <option>Sorvete</option>
</select>

Sempre que o valor de um campo de formulário muda, ele dispara um evento "change".

Foco

Diferente da maioria dos elementos em documentos HTML, campos de formulário podem receber foco do teclado. Ao serem clicados, acessados via tab ou ativados de outra forma, eles se tornam o elemento ativo atual e o receptor da entrada do teclado.

Assim, você pode digitar em um campo de texto apenas quando ele está focado. Outros campos respondem de forma diferente a eventos de teclado. Por exemplo, um menu <select> tenta mover-se para a opção que contém o texto que o usuário digitou e responde às setas do teclado movendo sua seleção para cima e para baixo.

Podemos controlar o foco via JavaScript com os métodos focus e blur. O primeiro move o foco para o elemento DOM no qual é chamado, e o segundo remove o foco. O valor em document.activeElement corresponde ao elemento atualmente focado.

<input type="text">
<script>
  document.querySelector("input").focus();
  console.log(document.activeElement.tagName);
  // → INPUT
  document.querySelector("input").blur();
  console.log(document.activeElement.tagName);
  // → BODY
</script>

Para algumas páginas, espera-se que o usuário queira interagir com um campo de formulário imediatamente. JavaScript pode ser usado para focar esse campo quando o documento é carregado, mas o HTML também fornece o atributo autofocus, que produz o mesmo efeito enquanto deixa o navegador saber o que estamos tentando alcançar. Isso dá ao navegador a opção de desabilitar o comportamento quando não for apropriado, como quando o usuário colocou o foco em outra coisa.

Navegadores permitem que o usuário mova o foco pelo documento pressionando tab para ir para o próximo elemento focável, e shift-tab para voltar ao elemento anterior. Por padrão, os elementos são visitados na ordem em que aparecem no documento. É possível usar o atributo tabindex para mudar essa ordem. O documento de exemplo a seguir vai permitir que o foco pule do campo de texto para o botão OK, ao invés de passar pelo link de ajuda primeiro:

<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>

Por padrão, a maioria dos tipos de elementos HTML não pode ser focada. Você pode adicionar o atributo tabindex a qualquer elemento para torná-lo focável. Um tabindex de 0 torna um elemento focável sem afetar a ordem do foco.

Campos desabilitados

Todos os campos de formulário podem ser desabilitados através do seu atributo disabled. É um atributo que pode ser especificado sem valor—o fato de estar presente desabilita o elemento.

<button>I'm all right</button>
<button disabled>I'm out</button>

Campos desabilitados não podem ser focados ou alterados, e navegadores os mostram com aparência cinza e apagada.

Quando um programa está no processo de lidar com uma ação causada por algum botão ou outro controle que pode exigir comunicação com o servidor e assim levar um tempo, pode ser uma boa ideia desabilitar o controle até que a ação termine. Dessa forma, quando o usuário ficar impaciente e clicar novamente, ele não repete a ação sem querer.

O formulário como um todo

Quando um campo está contido em um elemento <form>, seu elemento DOM terá uma propriedade form que liga de volta ao elemento DOM do formulário. O elemento <form>, por sua vez, tem uma propriedade chamada elements que contém uma coleção semelhante a um array dos campos dentro dele.

O atributo name de um campo de formulário determina como seu valor será identificado quando o formulário for enviado. Ele também pode ser usado como nome de propriedade ao acessar a propriedade elements do formulário, que atua tanto como um objeto semelhante a array (acessível por número) quanto um mapa (acessível por nome).

<form action="example/submit.html">
  Name: <input type="text" name="name"><br>
  Password: <input type="password" name="password"><br>
  <button type="submit">Log in</button>
</form>
<script>
  let form = document.querySelector("form");
  console.log(form.elements[1].type);
  // → password
  console.log(form.elements.password.type);
  // → password
  console.log(form.elements.name.form == form);
  // → true
</script>

Um botão com o atributo type definido como submit fará, ao ser pressionado, com que o formulário seja enviado. Pressionar enter quando um campo do formulário estiver focado tem o mesmo efeito.

Enviar um formulário normalmente significa que o navegador navega para a página indicada pelo atributo action do formulário, usando uma requisição GET ou POST. Mas antes disso acontecer, um evento "submit" é disparado. Você pode tratar esse evento com JavaScript e impedir esse comportamento padrão chamando preventDefault no objeto de evento.

<form>
  Value: <input type="text" name="value">
  <button type="submit">Save</button>
</form>
<script>
  let form = document.querySelector("form");
  form.addEventListener("submit", event => {
    console.log("Saving value", form.elements.value.value);
    event.preventDefault();
  });
</script>

Interceptar eventos "submit" em JavaScript tem vários usos. Podemos escrever código para verificar se os valores que o usuário entrou fazem sentido e mostrar imediatamente uma mensagem de erro em vez de enviar o formulário. Ou podemos desabilitar completamente a forma regular de enviar o formulário, como no exemplo, e fazer com que nosso programa lide com a entrada, possivelmente usando fetch para enviá-la a um servidor sem recarregar a página.

Campos de texto

Campos criados por tags <textarea>, ou tags <input> com o tipo text ou password, compartilham uma interface comum. Seus elementos DOM têm uma propriedade value que mantém seu conteúdo atual como um valor string. Definir essa propriedade para outra string altera o conteúdo do campo.

As propriedades selectionStart e selectionEnd dos campos de texto nos dão informações sobre o cursor e a seleção no texto. Quando nada está selecionado, essas duas propriedades contêm o mesmo número, indicando a posição do cursor. Por exemplo, 0 indica o início do texto, e 10 indica que o cursor está após o 10º caractere. Quando parte do campo está selecionada, as duas propriedades diferem, mostrando o início e o fim do texto selecionado. Como value, essas propriedades também podem ser atribuídas.

Imagine que você está escrevendo um artigo sobre Khasekhemwy, último faraó da Segunda Dinastia, mas tem alguma dificuldade em soletrar seu nome. O código a seguir conecta uma tag <textarea> com um manipulador de evento que, quando você pressiona F2, insere a string “Khasekhemwy” para você.

<textarea></textarea>
<script>
  let textarea = document.querySelector("textarea");
  textarea.addEventListener("keydown", event => {
    if (event.key == "F2") {
      replaceSelection(textarea, "Khasekhemwy");
      event.preventDefault();
    }
  });
  function replaceSelection(field, word) {
    let from = field.selectionStart, to = field.selectionEnd;
    field.value = field.value.slice(0, from) + word +
                  field.value.slice(to);
    // Coloca o cursor após a palavra
    field.selectionStart = from + word.length;
    field.selectionEnd = from + word.length;
  }
</script>

A função replaceSelection substitui a parte atualmente selecionada do conteúdo de um campo de texto pela palavra dada e então move o cursor para depois dessa palavra, para que o usuário possa continuar digitando.

O evento "change" para um campo de texto não é disparado toda vez que algo é digitado. Ele é disparado quando o campo perde o foco depois que seu conteúdo foi alterado. Para responder imediatamente a mudanças em um campo de texto, você deve registrar um manipulador para o evento "input", que acontece toda vez que o usuário digita um caractere, apaga texto ou de qualquer outra forma manipula o conteúdo do campo.

O exemplo a seguir mostra um campo de texto e um contador exibindo o comprimento atual do texto no campo:

<input type="text"> length: <span id="length">0</span>
<script>
  let text = document.querySelector("input");
  let output = document.querySelector("#length");
  text.addEventListener("input", () => {
    output.textContent = text.value.length;
  });
</script>

Caixas de seleção e botões de opção

Um campo de checkbox é um interruptor binário. Seu valor pode ser extraído ou alterado através de sua propriedade checked, que contém um valor Booleano.

<label>
  <input type="checkbox" id="purple"> Deixar esta página roxa
</label>
<script>
  let checkbox = document.querySelector("#purple");
  checkbox.addEventListener("change", () => {
    document.body.style.background =
      checkbox.checked ? "mediumpurple" : "";
  });
</script>

A tag <label> associa um pedaço do documento com um campo de input. Clicar em qualquer lugar do label ativará o campo, que o foca e alterna seu valor quando ele é uma checkbox ou botão de rádio.

Um radio button é similar a uma caixa de seleção, mas está implicitamente ligado a outros botões de rádio com o mesmo atributo name, de modo que apenas um deles pode estar ativo por vez.

Color:
<label>
  <input type="radio" name="color" value="orange"> Laranja
</label>
<label>
  <input type="radio" name="color" value="lightgreen"> Verde
</label>
<label>
  <input type="radio" name="color" value="lightblue"> Azul
</label>
<script>
  let buttons = document.querySelectorAll("[name=color]");
  for (let button of Array.from(buttons)) {
    button.addEventListener("change", () => {
      document.body.style.background = button.value;
    });
  }
</script>

Os colchetes na query CSS passada para querySelectorAll são usados para casar atributos. Ela seleciona elementos cujo atributo name seja "color".

Campos select

Campos select são conceitualmente similares aos botões de rádio—eles também permitem que o usuário escolha entre um conjunto de opções. Mas enquanto um botão de rádio coloca a disposição das opções sob nosso controle, a aparência de uma tag <select> é determinada pelo navegador.

Campos select também possuem uma variante mais parecida com uma lista de checkboxes ao invés de caixas de rádio. Quando dado o atributo multiple, uma tag <select> permitirá que o usuário selecione qualquer número de opções, e não apenas uma única opção. Enquanto um campo select comum é desenhado como um controle drop-down, que mostra as opções inativas apenas quando você o abre, um campo com multiple ativo mostra várias opções ao mesmo tempo, permitindo que o usuário habilite ou desabilite elas individualmente.

Cada tag <option> tem um valor. Esse valor pode ser definido com um atributo value. Quando ele não é fornecido, o texto dentro da opção conta como seu valor. A propriedade value de um elemento <select> reflete a opção atualmente selecionada. Para um campo multiple, porém, essa propriedade não significa muito, já que ela dará o valor de apenas uma das opções atualmente selecionadas.

As tags <option> de um campo <select> podem ser acessadas como um objeto parecido com array através da propriedade options do campo. Cada opção tem uma propriedade chamada selected, que indica se essa opção está atualmente selecionada. A propriedade também pode ser escrita para selecionar ou desselecionar uma opção.

Este exemplo extrai os valores selecionados de um campo select multiple e os usa para compor um número binário a partir de bits individuais. Segure ctrl (ou command no Mac) para selecionar múltiplas opções.

<select multiple>
  <option value="1">0001</option>
  <option value="2">0010</option>
  <option value="4">0100</option>
  <option value="8">1000</option>
</select> = <span id="output">0</span>
<script>
  let select = document.querySelector("select");
  let output = document.querySelector("#output");
  select.addEventListener("change", () => {
    let number = 0;
    for (let option of Array.from(select.options)) {
      if (option.selected) {
        number += Number(option.value);
      }
    }
    output.textContent = number;
  });
</script>

Campos de arquivo

Campos de arquivo foram originalmente criados como uma forma de upload de arquivos da máquina do usuário através de um formulário. Em navegadores modernos, eles também fornecem uma forma de ler esses arquivos a partir de programas JavaScript. O campo atua como um tipo de guardião. O script não pode simplesmente começar a ler arquivos privados do computador do usuário, mas se o usuário selecionar um arquivo em tal campo, o navegador interpreta essa ação como permissão para o script ler o arquivo.

Um campo de arquivo normalmente se parece com um botão com um rótulo como “escolher arquivo” ou “navegar”, com informações sobre o arquivo escolhido ao lado.

<input type="file">
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    if (input.files.length > 0) {
      let file = input.files[0];
      console.log("Você escolheu", file.name);
      if (file.type) console.log("Ele tem o tipo", file.type);
    }
  });
</script>

A propriedade files de um campo de arquivo é um objeto parecido com um array (mais uma vez, não um array real) contendo os arquivos escolhidos no campo. Inicialmente, ele está vazio. O motivo pelo qual não há simplesmente uma propriedade file é que campos de arquivo também suportam o atributo multiple, que torna possível selecionar múltiplos arquivos ao mesmo tempo.

Os objetos em files têm propriedades como name (o nome do arquivo), size (o tamanho do arquivo em bytes, que são pedaços de 8 bits) e type (o tipo de mídia do arquivo, como text/plain ou image/jpeg).

O que eles não têm é uma propriedade que contenha o conteúdo do arquivo. Acessar isso é um pouco mais complicado. Como ler um arquivo do disco pode levar tempo, a interface é assíncrona para evitar travar a janela.

<input type="file" multiple>
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    for (let file of Array.from(input.files)) {
      let reader = new FileReader();
      reader.addEventListener("load", () => {
        console.log("Arquivo", file.name, "começa com",
                    reader.result.slice(0, 20));
      });
      reader.readAsText(file);
    }
  });
</script>

Ler um arquivo é feito criando um objeto FileReader, registrando um manipulador de evento "load" para ele e chamando seu método readAsText, passando o arquivo que queremos ler. Quando o carregamento termina, a propriedade result do reader contém o conteúdo do arquivo.

FileReaders também disparam um evento "error" quando a leitura do arquivo falha por qualquer motivo. O objeto de erro em si ficará na propriedade error do reader. Essa interface foi criada antes das promises fazerem parte da linguagem. Você pode encapsulá-la em uma promise assim:

function readFileText(file) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.addEventListener(
      "load", () => resolve(reader.result));
    reader.addEventListener(
      "error", () => reject(reader.error));
    reader.readAsText(file);
  });
}

Armazenando dados no lado cliente

Páginas simples de HTML com um pouco de JavaScript podem ser um ótimo formato para “mini aplicações” — pequenos programas auxiliares que automatizam tarefas básicas. Ligando alguns campos de formulário com manipuladores de evento, você pode fazer desde converter entre centímetros e polegadas até gerar senhas a partir de uma senha mestra e o nome de um site.

Quando tal aplicação precisa lembrar algo entre sessões, você não pode usar ligações JavaScript — eles são descartados toda vez que a página é fechada. Você poderia configurar um servidor, conectá-lo à internet e fazer sua aplicação armazenar algo lá (vamos ver como fazer isso no Capítulo 20). Mas isso é muito trabalho extra e complexidade. Às vezes, basta apenas manter os dados no navegador.

O objeto localStorage pode ser usado para armazenar dados de forma a sobreviver a reloads de página. Esse objeto permite que você armazene valores string sob nomes.

localStorage.setItem("username", "marijn");
console.log(localStorage.getItem("username"));
// → marijn
localStorage.removeItem("username");

Um valor no localStorage permanece até que seja sobrescrito ou removido com removeItem, ou até que o usuário limpe seus dados locais.

Sites com domínios diferentes possuem compartimentos de armazenamento diferentes. Isso significa que dados armazenados em localStorage por um dado site podem, em princípio, ser lidos (e sobrescritos) apenas por scripts desse mesmo site.

Navegadores impõem um limite no tamanho dos dados que um site pode armazenar no localStorage. Essa restrição, junto com o fato de que encher o armazenamento das pessoas com lixo não é muito lucrativo, impede que o recurso consuma muito espaço.

O código a seguir implementa uma aplicação simples de anotações. Ele mantém um conjunto de notas nomeadas e permite ao usuário editar notas e criar novas.

Notes: <select></select> <button>Add</button><br>
<textarea style="width: 100%"></textarea>

<script>
  let list = document.querySelector("select");
  let note = document.querySelector("textarea");

  let state;
  function setState(newState) {
    list.textContent = "";
    for (let name of Object.keys(newState.notes)) {
      let option = document.createElement("option");
      option.textContent = name;
      if (newState.selected == name) option.selected = true;
      list.appendChild(option);
    }
    note.value = newState.notes[newState.selected];

    localStorage.setItem("Notes", JSON.stringify(newState));
    state = newState;
  }
  setState(JSON.parse(localStorage.getItem("Notes")) ?? {
    notes: {"shopping list": "Carrots\nRaisins"},
    selected: "shopping list"
  });

  list.addEventListener("change", () => {
    setState({notes: state.notes, selected: list.value});
  });
  note.addEventListener("change", () => {
    let {selected} = state;
    setState({
      notes: {...state.notes, [selected]: note.value},
      selected
    });
  });
  document.querySelector("button")
    .addEventListener("click", () => {
      let name = prompt("Note name");
      if (name) setState({
        notes: {...state.notes, [name]: ""},
        selected: name
      });
    });
</script>

O script obtém seu estado inicial a partir do valor "Notes" armazenado em localStorage ou, se isso estiver faltando, cria um estado exemplo que contém apenas uma lista de compras. Ler um campo que não existe no localStorage devolverá null. Passar null para o JSON.parse fará com que ele analise a string "null" e retorne null. Assim, o operador ?? pode ser usado para fornecer um valor padrão em uma situação como essa.

O método setState garante que o DOM esteja mostrando um estado dado e armazena o novo estado no localStorage. Os manipuladores de eventos chamam essa função para mover para um novo estado.

A sintaxe ... no exemplo é usada para criar um novo objeto que é um clone do antigo state.notes, mas com uma propriedade adicionada ou sobrescrita. Ele usa a sintaxe de espalhamento (spread) para primeiro adicionar as propriedades do objeto antigo e depois definir uma nova propriedade. A notação de colchetes no literal do objeto é usada para criar uma propriedade cujo nome é baseado em algum valor dinâmico.

Existe outro objeto, semelhante ao localStorage, chamado sessionStorage. A diferença entre os dois é que o conteúdo do sessionStorage é esquecido no final de cada sessão, o que para a maioria dos navegadores significa sempre que o navegador é fechado.

Resumo

Neste capítulo, discutimos como o protocolo HTTP funciona. Um cliente envia uma requisição, que contém um método (geralmente GET) e um caminho que identifica um recurso. O servidor então decide o que fazer com a requisição e responde com um código de status e um corpo de resposta. Ambas as requisições e respostas podem conter cabeçalhos que fornecem informações adicionais.

A interface pela qual o JavaScript do navegador pode fazer requisições HTTP é chamada fetch. Fazer uma requisição é assim:

fetch("/18_http.html").then(r => r.text()).then(text => {
  console.log(`A página começa com ${text.slice(0, 15)}`);
});

Navegadores fazem requisições GET para obter os recursos necessários para exibir uma página web. Uma página também pode conter formulários, que permitem que informações inseridas pelo usuário sejam enviadas como uma requisição para uma nova página quando o formulário é submetido.

O HTML pode representar vários tipos de campos de formulário, como campos de texto, caixas de seleção, campos de múltipla escolha e seletor de arquivos. Esses campos podem ser inspecionados e manipulados com JavaScript. Eles disparam o evento "change" quando alterados, disparam o evento "input" quando texto é digitado e recebem eventos de teclado quando têm foco no teclado. Propriedades como value (para campos de texto e select) ou checked (para caixas de seleção e botões de rádio) são usadas para ler ou definir o conteúdo do campo.

Quando um formulário é submetido, um evento "submit" é disparado nele. Um manipulador JavaScript pode chamar preventDefault nesse evento para desabilitar o comportamento padrão do navegador. Elementos de campo de formulário também podem ocorrer fora de uma tag de formulário.

Quando o usuário seleciona um arquivo do seu sistema local em um campo seletor de arquivos, a interface FileReader pode ser usada para acessar o conteúdo desse arquivo a partir de um programa JavaScript.

Os objetos localStorage e sessionStorage podem ser usados para salvar informações de uma forma que persiste recarregamentos de página. O primeiro objeto salva os dados para sempre (ou até o usuário decidir limpá-los), e o segundo os salva até o navegador ser fechado.

Exercícios

Negociação de conteúdo

Uma das coisas que o HTTP pode fazer é chamada de content negotiation. O cabeçalho de requisição Accept é usado para dizer ao servidor qual tipo de documento o cliente gostaria de receber. Muitos servidores ignoram esse cabeçalho, mas quando um servidor conhece várias formas de codificar um recurso, ele pode olhar para esse cabeçalho e enviar o que o cliente prefere.

A URL https://eloquentjavascript.net/author está configurada para responder com plaintext, HTML ou JSON, dependendo do que o cliente pede. Esses formatos são identificados pelos tipos de mídia padronizados text/plain, text/html e application/json.

Envie requisições para buscar os três formatos desse recurso. Use a propriedade headers no objeto de opções passado para fetch para configurar o cabeçalho chamado Accept para o tipo de mídia desejado.

Finalmente, tente pedir pelo tipo de mídia application/rainbows+unicorns e veja qual código de status isso produz.

// Your code here.
Mostrar dicas...

Baseie seu código nos exemplos de fetch mais cedo no capítulo.

Pedir por um tipo de mídia inválido retornará uma resposta com código 406, “Not acceptable”, que é o código que um servidor deve retornar quando não consegue cumprir o cabeçalho Accept.

A JavaScript workbench

Construa uma interface que permita aos usuários digitar e executar pedaços de código JavaScript.

Coloque um botão próximo a um campo <textarea> que, quando pressionado, use o construtor Function que vimos em Capítulo 10 para envolver o texto em uma função e chamá-la. Converta o valor de retorno dessa função, ou qualquer erro que ela gere, para uma string e exiba abaixo do campo de texto.

<textarea id="code">return "hi";</textarea>
<button id="button">Run</button>
<pre id="output"></pre>

<script>
  // Your code here.
</script>
Mostrar dicas...

Use document.querySelector ou document.getElementById para acessar os elementos definidos no seu HTML. Um manipulador de evento para eventos "click" ou "mousedown" no botão pode obter a propriedade value do campo de texto e chamar Function com ela.

Certifique-se de envolver tanto a chamada Function quanto a chamada do seu resultado em um bloco try para capturar as exceções produzidas. Neste caso, realmente não sabemos qual tipo de exceção esperar, então capture tudo.

A propriedade textContent do elemento de saída pode ser usada para preenchê-lo com uma mensagem em string. Ou, se quiser manter o conteúdo antigo, crie um novo nó de texto usando document.createTextNode e o adicione ao elemento. Lembre-se de adicionar um caractere de nova linha no final para que as saídas não apareçam todas numa única linha.

O Jogo da Vida de Conway

O Jogo da Vida de Conway é uma simulação simples que cria “vida” artificial em uma grade, cada célula da qual está viva ou não. Em cada geração (turno), as seguintes regras são aplicadas:

Um vizinho é definido como qualquer célula adjacente, incluindo as diagonalmente adjacentes.

Note que essas regras são aplicadas a toda a grade de uma vez, não um quadrado por vez. Isso significa que a contagem dos vizinhos é baseada na situação no início da geração, e mudanças ocorridas em células vizinhas durante esta geração não devem influenciar o novo estado de uma dada célula.

Implemente este jogo usando qualquer estrutura de dados que você achar apropriada. Use Math.random para popular a grade com um padrão aleatório inicialmente. Exiba-a como uma grade de campos de checkboxs, com um botão ao lado para avançar para a próxima geração. Quando o usuário marcar ou desmarcar os checkboxes, suas mudanças devem ser incluídas no cálculo da próxima geração.

<div id="grid"></div>
<button id="next">Next generation</button>

<script>
  // Your code here.
</script>
Mostrar dicas...

Para resolver o problema de ter as mudanças ocorrendo conceitualmente ao mesmo tempo, tente ver o cálculo de uma geração como uma função pura, que recebe uma grade e produz uma nova grade que representa o próximo turno.

Representar a matriz pode ser feito com um único array de elementos largura × altura, armazenando valores linha por linha, então, por exemplo, o terceiro elemento na quinta linha está (usando indexação começando em zero) armazenado na posição 4 × largura + 2. Você pode contar os vizinhos vivos com dois loops aninhados, iterando sobre coordenadas adjacentes em ambas as dimensões. Tome cuidado para não contar células fora do campo e para ignorar a célula central, cujos vizinhos estamos contando.

Garantir que mudanças nos checkboxes tenham efeito na próxima geração pode ser feito de duas formas. Um manipulador de evento pode perceber essas mudanças e atualizar a grade atual para refletí-las, ou você pode gerar uma nova grade a partir dos valores dos checkboxes antes de calcular o próximo turno.

Se você optar por usar manipuladores de evento, pode ser útil anexar atributos que identifiquem a posição que cada checkbox corresponde para que seja fácil descobrir qual célula mudar.

Para desenhar a grade de checkboxes, você pode usar um elemento <table> (veja Capítulo 14) ou simplesmente colocá-los todos no mesmo elemento e usar elementos <br> (quebra de linha) entre as linhas.