HTTP e Formulários

O que frequentemente 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 um computador central ‘controlando’ a web, nenhuma rede única na qual esses protocolos funcionavam, nem mesmo uma organização em algum lugar que ‘administrava’ a Web. A Web não era uma ‘coisa’ física que existia em um certo ‘lugar’. Era um ‘espaço’ no qual informações podiam existir.

Tim Berners-Lee
Illustration showing a web sign-up form on a parchment scroll

O Protocolo de Transferência de Hipertexto, introduzido no Capítulo 13, é o mecanismo pelo 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 com 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 da 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>
... the rest of the document

O navegador pega a parte da resposta após a linha em branco, seu 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. 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 realizar toda requisição que recebe. Se você chegar a um site qualquer e dizer para 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. Um recurso pode ser qualquer coisa que possa ser transferida como se fosse um arquivo. Muitos servidores geram as respostas que produzem dinamicamente. Por exemplo, se você abrir https://github.com/marijnh, o servidor procura em seu banco de dados um usuário chamado “marijnh”, e se encontrar um, gerará 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á usando.

Na prática, muitos sites usam HTTP versão 2, que suporta os mesmos conceitos da versão 1.1, mas é muito mais complicado para poder ser mais rápido. Os navegadores automaticamente mudam para a versão de protocolo apropriada ao falar com um determinado servidor, e o resultado de uma requisição é o mesmo independentemente de qual versão é usada. Como a versão 1.1 é mais direta e mais fácil de experimentar, usaremos essa para ilustrar o protocolo.

A resposta do servidor começará com uma versão também, 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 teve sucesso. 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 404, que significa que o recurso não pôde ser encontrado. Códigos que começam com 5 significam que um erro aconteceu no servidor e a requisição não é culpada.

A primeira linha de uma requisição ou resposta pode ser seguida por qualquer número de cabeçalhos. Estas são linhas na forma nome: valor que especificam informações extras sobre a requisição ou resposta. Estes cabeçalhos faziam parte da resposta de exemplo:

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

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

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

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

Navegadores e HTTP

Como vimos, um navegador fará uma requisição quando digitamos uma URL em sua barra de endereço. Quando a página HTML resultante referencia outros arquivos, como imagemns e arquivos JavaScript, eles também serão recuperados.

Um website moderadamente complexo pode facilmente incluir de 10 a 200 recursos. Para poder buscá-los rapidamente, os navegadores fazem várias requisições GET simultaneamente, em vez de esperar pelas respostas uma de cada vez.

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 Enviar, o formulário é submetido, o que significa que o conteúdo de seus campos é empacotado em uma 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 no formulário é adicionada ao final da URL action como uma string de consulta. O navegador pode fazer uma requisição para esta URL:

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

O ponto de interrogação indica o fim da parte do caminho da URL e o início da consulta. Ele é seguido por pares de nomes e valores, correspondendo ao atributo name nos elementos de campo do formulário 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 strings de consulta precisam ser escapados. O ponto de interrogação, representado como %3F, é um deles. Parece haver uma regra não escrita de que cada formato precisa de sua própria forma de escapar caracteres. Esta, chamada codificação URL, usa um sinal 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 de um caractere de ponto de interrogação. JavaScript fornece as funções encodeURIComponent e decodeURIComponent para codificar e decodificar este formato.

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

Se mudarmos o atributo method do formulário HTML no exemplo que vimos anteriormente para POST, a requisição HTTP feita para submeter o formulário usará o método POST e colocará a string de consulta no corpo da requisição em vez 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 usadas para requisições que não têm efeito colaterals, mas simplesmente pedem informações. Requisições que mudam algo no servidor, por exemplo criando uma nova conta ou postando uma mensagem, devem ser expressas com outros métodos, como POST. Software do lado do cliente, como um navegador, sabe que não deve fazer requisições POST cegamente, mas frequentemente faz requisições GET implicitamente — para pré-buscar um recurso que acredita que o usuário precisará em breve, por exemplo.

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

Fetch

A interface através da 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 se resolve para um objeto Response contendo informações sobre a resposta do servidor, como seu código de status e seus cabeçalhos. Os cabeçalhos são envolvidos em um objeto semelhante a Map que trata suas chaves (os nomes dos cabeçalhos) sem distinção entre maiúsculas e minúsculas, porque os nomes de cabeçalhos não devem ser sensíveis a maiúsculas. Isso significa que headers.get("Content-Type") e headers.get("content-TYPE") retornarão o mesmo valor.

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

O primeiro argumento para 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 na frente da URL relativa.

Para acessar o conteúdo real de uma resposta, você pode usar seu método text. Como a promise inicial é resolvida assim que os cabeçalhos da resposta são recebidos e porque a leitura do corpo da resposta pode demorar mais, isso novamente retorna uma promise.

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

Um método similar, chamado json, retorna uma promise que se resolve para o valor que você obtém ao analisar o corpo como JSON ou rejeita se não for 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 diferentemente 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”, a forma de um 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 uma opção body. Para definir cabeçalhos, há a opção headers. Por exemplo, esta requisição inclui um cabeçalho 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 cabeçalhos de requisição, como “Host” e aqueles necessários para o servidor descobrir o tamanho do corpo. Mas adicionar seus próprios cabeçalhos é frequentemente útil para incluir coisas como informações de autenticação ou para dizer ao servidor qual formato de arquivo você gostaria de receber.

Sandboxing HTTP

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

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

Isso 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 cabeçalho assim em sua resposta para indicar explicitamente ao navegador que está tudo bem a requisição vir de outro domínio:

Access-Control-Allow-Origin: *

Apreciando o HTTP

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

Um modelo comumente usado é o de chamadas de procedimento remoto. Neste modelo, a comunicação segue os padrões de chamadas de função normais, exceto que a função está na verdade rodando em outra máquina. Chamá-la envolve fazer uma requisição ao servidor que inclui o nome da função e argumentos. A resposta a essa requisição contém o valor retornado.

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

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

Esta segunda abordagem facilita o uso de alguns dos recursos que o HTTP fornece, como suporte para cache de recursos (manter uma cópia de um recurso no cliente para acesso rápido). Os conceitos usados em HTTP, que são bem projetados, podem fornecer um conjunto útil de princípios para projetar sua interface de servidor.

Segurança e HTTPS

Dados viajando pela internet tendem a seguir um caminho longo e perigoso. Para chegar ao seu destino, eles precisam passar por qualquer coisa, desde hotspots Wi-Fi de cafeterias até redes controladas por várias empresas e estados. Em qualquer ponto ao longo de sua rota, eles podem ser inspecionados ou até modificados.

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

O protocolo HTTP seguro, usado para URLs que começam com https://, envolve o tráfego HTTP de uma forma que torna mais difícil ler e adulterar. Antes de trocar dados, o cliente verifica que o servidor é quem ele diz ser pedindo a ele que prove que possui um certificado criptográfico emitido por uma autoridade certificadora que o navegador reconhece. Em seguida, todos os dados passando pela conexão são criptografados de uma forma que deve prevenir espionagem e adulteração.

Assim, quando funciona corretamente, HTTPS impede que outras pessoas se passem pelo website com o qual você está tentando falar e bisbilhotem sua comunicação. Não é perfeito, e houve vários incidentes onde HTTPS falhou por causa de certificados forjados ou roubados e software defeituoso, mas é muito mais seguro que HTTP simples.

Campos de formulário

Formulários foram originalmente projetados para a web pré-JavaScript para permitir que websites enviassem informações submetidas pelo usuário em uma requisição HTTP. Este 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 campos de formulário suportam uma série de propriedades e eventos que não estão presentes em outros elementos. Estes tornam possível inspecionar e manipular tais campos de entrada com programas JavaScript e fazer coisas como adicionar nova funcionalidade a um formulário ou usar formulários e campos como blocos de construção em uma aplicação JavaScript.

Um formulário web consiste em qualquer número de campos de entrada agrupados em uma tag <form>. HTML permite vários estilos diferentes de campos, variando de checkboxes simples liga/desliga a menus drop-down e campos para entrada de texto. Este livro não tentará discutir todos os tipos de campos de forma abrangente, mas vamos começar com uma visão geral aproximada.

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

textUm campo de texto de linha única
passwordIgual a text mas esconde o texto digitado
checkboxUm interruptor liga/desliga
colorUma cor
dateUma data do calendário
radio(Parte de) um campo de múltipla escolha
filePermite ao usuário escolher um arquivo de seu 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. Tais campos sem formulário não podem ser submetidos (apenas um formulário inteiro pode), mas ao responder a entrada com JavaScript, frequentemente não queremos submeter nossos campos normalmente de qualquer forma.

<p><input type="text" value="abc"> (text)</p>
<p><input type="password" value="abc"> (password)</p>
<p><input type="checkbox" checked> (checkbox)</p>
<p><input type="color" value="orange"> (color)</p>
<p><input type="date" value="2023-10-13"> (date)</p>
<p><input type="radio" value="A" name="choice">
   <input type="radio" value="B" name="choice" checked>
   <input type="radio" value="C" name="choice"> (radio)</p>
<p><input type="file"> (file)</p>

A interface JavaScript para tais elementos difere com o tipo do elemento.

Campos de texto multilinha têm 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, em vez do atributo value, como texto inicial.

<textarea>
one
two
three
</textarea>

Finalmente, a tag <select> é usada para criar um campo que permite ao usuário selecionar entre várias opções predefinidas.

<select>
  <option>Pancakes</option>
  <option>Pudding</option>
  <option>Ice cream</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. Quando clicados, movidos com tab, ou ativados de alguma outra forma, eles se tornam o elemento atualmente ativo e o receptor de entrada do teclado.

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

Podemos controlar o foco a partir de 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 com foco.

<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 dar foco a este campo quando o documento é carregado, mas 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 é 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 mover para o próximo elemento focalizá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 esta ordem. O exemplo de documento a seguir permitirá que o foco pule do campo de texto para o botão OK, em vez 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 receber foco. Você pode adicionar um atributo tabindex a qualquer elemento para torná-lo focalizável. Um tabindex de 0 torna um elemento focalizável sem afetar a ordem de foco.

Campos desabilitados

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

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

Campos desabilitados não podem receber foco ou ser alterados, e os navegadores os fazem parecer cinza e desbotados.

Quando um programa está no processo de tratar uma ação causada por algum botão ou outro controle que pode requerer comunicação com o servidor e, portanto, demorar um pouco, pode ser uma boa ideia desabilitar o controle até que a ação termine. Dessa forma, quando o usuário fica impaciente e clica novamente, não repete acidentalmente sua ação.

O formulário como um todo

Quando um campo está contido em um elemento <form>, seu elemento DOM terá uma propriedade form ligando 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 a forma como seu valor será identificado quando o formulário for submetido. Ele também pode ser usado como nome de propriedade ao acessar a propriedade elements do formulário, que age tanto como um objeto semelhante a array (acessível por número) quanto como 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 um atributo type de submit irá, quando pressionado, fazer com que o formulário seja submetido. Pressionar enter quando um campo de formulário está com foco tem o mesmo efeito.

Submeter 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 que isso aconteça, um evento "submit" é disparado. Você pode tratar este evento com JavaScript e prevenir este comportamento padrão chamando preventDefault no objeto do 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 inseriu fazem sentido e imediatamente mostrar uma mensagem de erro em vez de submeter o formulário. Ou podemos desabilitar a forma regular de submeter o formulário inteiramente, como no exemplo, e ter nosso programa tratando 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 tipo text ou password, compartilham uma interface comum. Seus elementos DOM têm uma propriedade value que contém seu conteúdo atual como um valor de string. Definir esta propriedade para outra string muda o conteúdo do campo.

As propriedades selectionStart e selectionEnd dos campo de textos 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 serão diferentes, nos dando o início e o fim do texto selecionado. Como value, essas propriedades também podem ser escritas.

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 eventos 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);
    // Colocar 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 após essa 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. Em vez disso, ele é disparado quando o campo perde o foco após seu conteúdo ter sido alterado. Para responder imediatamente a mudanças em um campo de texto, você deve registrar um manipulador para o evento "input", que é disparado toda vez que o usuário digita um caractere, deleta texto ou de 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>

Checkboxes e botões de rádio

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"> Make this page purple
</label>
<script>
  let checkbox = document.querySelector("#purple");
  checkbox.addEventListener("change", () => {
    document.body.style.background =
      checkbox.checked ? "mediumpurple" : "";
  });
</script>

A tag <label> associa um trecho do documento a um campo de entrada. Clicar em qualquer lugar no rótulo ativará o campo, que o foca e alterna seu valor quando é um checkbox ou botão de rádio.

Um botão de rádio é semelhante a um checkbox, mas está implicitamente ligado a outros botões de rádio com o mesmo atributo name para que apenas um deles possa estar ativo a qualquer momento.

Color:
<label>
  <input type="radio" name="color" value="orange"> Orange
</label>
<label>
  <input type="radio" name="color" value="lightgreen"> Green
</label>
<label>
  <input type="radio" name="color" value="lightblue"> Blue
</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 consulta CSS dada a querySelectorAll são usados para corresponder atributos. Ela seleciona elementos cujo atributo name é "color".

Campos de seleção

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

Campos de seleção também têm uma variante mais parecida com uma lista de checkboxes do que com botões de rádio. Quando recebe o atributo multiple, uma tag <select> permitirá ao usuário selecionar qualquer número de opções, em vez de apenas uma opção. Enquanto um campo de seleção regular é desenhado como um controle drop-down, que mostra as opções inativas apenas quando você o abre, um campo com multiple habilitado mostra múltiplas opções ao mesmo tempo, permitindo ao usuário habilitá-las ou desabilitá-las individualmente.

Cada tag <option> tem um valor. Este valor pode ser definido com um atributo value. Quando isso não é dado, o texto dentro da opção contará como seu valor. A propriedade value de um elemento <select> reflete a opção atualmente selecionada. Para um campo multiple, porém, esta propriedade não significa muito, pois dará o valor de apenas uma das opções atualmente selecionadas.

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

Este exemplo extrai os valores selecionados de um campo de seleção multiple e os usa para compor um número binário a partir de bits individuais. Mantenha ctrl (ou command em um Mac) pressionado 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 projetados como uma forma de enviar arquivos da máquina do usuário através de um formulário. Em navegadores modernos, eles também fornecem uma forma de ler tais arquivos a partir de programas JavaScript. O campo age como uma espécie de porteiro. 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 significando que o script pode ler o arquivo.

Um campo de arquivo normalmente se parece com um botão rotulado com algo como “escolher arquivo” ou “procurar”, 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("You chose", file.name);
      if (file.type) console.log("It has type", file.type);
    }
  });
</script>

A propriedade files de um elemento de campo de arquivo é um objeto semelhante a array (novamente, não um array real) contendo os arquivos escolhidos no campo. É inicialmente vazio. A razão de não haver simplesmente uma propriedade file é que campos de arquivo também suportam um 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 blocos de 8 bits) e type (o tipo de mídia do arquivo, como text/plain ou image/jpeg).

O que ele não tem é 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 congelar 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("File", file.name, "starts with",
                    reader.result.slice(0, 20));
      });
      reader.readAsText(file);
    }
  });
</script>

A leitura de um arquivo é feita criando um objeto FileReader, registrando um manipulador de evento "load" para ele, e chamando seu método readAsText, dando-lhe o arquivo que queremos ler. Uma vez que o carregamento termina, a propriedade result do leitor contém o conteúdo do arquivo.

FileReaders também disparam um evento "error" quando a leitura do arquivo falha por qualquer razão. O objeto de erro em si acabará na propriedade error do leitor. Esta interface foi projetada antes de promises se tornarem parte da linguagem. Você poderia envolvê-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 do cliente

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

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

O objeto localStorage pode ser usado para armazenar dados de uma forma que sobrevive a recarregamento de páginas. Este objeto permite arquivar valores de string sob nomes.

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

Um valor em localStorage permanece até ser sobrescrito ou removido com removeItem, ou o usuário limpar seus dados locais.

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

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

O código a seguir implementa uma aplicação rústica de anotações. Ela 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 estiver faltando, cria um estado de exemplo que tem apenas uma lista de compras. Ler um campo que não existe do localStorage retornará null. Passar null para 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 esta.

O método setState garante que o DOM esteja mostrando um determinado estado e armazena o novo estado no localStorage. Manipuladores de eventos chamam esta 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. Ela usa a sintaxe de spread para primeiro adicionar as propriedades do objeto antigo e depois definir uma nova propriedade. A notação de colchetes no literal de objeto é usada para criar uma propriedade cujo nome é baseado em algum valor dinâmico.

Existe outro objeto, semelhante a localStorage, chamado sessionStorage. A diferença entre os dois é que o conteúdo de sessionStorage é esquecido no final de cada sessã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. Tanto requisições quanto respostas podem conter cabeçalhos que fornecem informações adicionais.

A interface através da qual o JavaScript do navegador pode fazer requisições HTTP é chamada fetch. Fazer uma requisição se parece com isto:

fetch("/18_http.html").then(r => r.text()).then(text => {
  console.log(`The page starts with ${text.slice(0, 15)}`);
});

Navegadores fazem requisições GET para buscar 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.

HTML pode representar vários tipos de campos de formulário, como campos de texto, checkboxes, campos de múltipla escolha e seletores de arquivo. Tais 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 do teclado. Propriedades como value (para campos de texto e select) ou checked (para checkboxes 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 campos de formulário também podem ocorrer fora de uma tag de formulário.

Quando o usuário selecionou um arquivo de seu sistema de arquivos local em um campo de seleção de arquivo, a interface FileReader pode ser usada para acessar o conteúdo deste arquivo a partir de um programa JavaScript.

Os objetos localStorage e sessionStorage podem ser usados para salvar informações de uma forma que sobrevive a 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é que o navegador seja fechado.

Exercícios

Negociação de conteúdo

Uma das coisas que o HTTP pode fazer é chamada negociação de conteúdo. O cabeçalho de requisição Accept é usado para dizer ao servidor que tipo de documento o cliente gostaria de obter. Muitos servidores ignoram este cabeçalho, mas quando um servidor conhece várias formas de codificar um recurso, ele pode olhar este cabeçalho e enviar a que o cliente preferir.

A URL https://eloquentjavascript.net/author é configurada para responder com texto simples, HTML ou JSON, dependendo do que o cliente pedir. Esses formatos são identificados pelos tipo de mídias padronizados text/plain, text/html e application/json.

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

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

// Seu código aqui.
Display hints...

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

Pedir um tipo de mídia falso retornará uma resposta com código 406, “Not acceptable”, que é o código que um servidor deve retornar quando não pode atender ao cabeçalho Accept.

Uma bancada de JavaScript

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

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

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

<script>
  // Seu código aqui.
</script>
Display hints...

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

Certifique-se de envolver tanto a chamada a Function quanto a chamada ao seu resultado em um bloco try para poder capturar as exceções que ele produz. Neste caso, realmente não sabemos que tipo de exceção estamos procurando, então capture tudo.

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

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 morta. A cada geração (turno), as seguintes regras são aplicadas:

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

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

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

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

<script>
  // Seu código aqui.
</script>
Display hints...

Para resolver o problema de ter as mudanças conceitualmente acontecendo ao mesmo tempo, tente ver a computação 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 largura x altura elementos, armazenando valores linha por linha, então, por exemplo, o terceiro elemento na quinta linha é (usando indexação baseada em zero) armazenado na posição 4 x largura + 2. Você pode contar 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 no centro, 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 eventos poderia notar essas mudanças e atualizar a grade atual para refleti-las, ou você poderia gerar uma grade nova a partir dos valores nos checkboxes antes de calcular o próximo turno.

Se você optar por manipuladores de eventos, pode querer 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 colocar elementos <br> (quebra de linha) entre as linhas.