Pular para o conteúdo

b

Formulários

Vamos continuar expandindo nossa aplicação permitindo que os usuários adicionem novas notas. Você pode encontrar o código para nossa aplicação atual aqui.

Para que a página seja atualizada quando novas notas são adicionadas, armazene as notas no estado do componente App. Vamos importar a função useState e usá-la para definir um pedaço de estado que é inicializado com o array das notas iniciais passado nas props.

import { useState } from 'react'import Note from './components/Note'

const App = (props) => {  const [notes, setNotes] = useState(props.notes)
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
    </div>
  )
}

export default App 

O componente usa a função useState para inicializar o pedaço de estado armazenado em notes com o array de notas passado nas props:

const App = (props) => { 
  const [notes, setNotes] = useState(props.notes) 

  // ...
}

Também podemos usar o "React Developer Tools" para ver o que realmente está acontecendo:

navegador mostrando a janela

Se quiséssemos inicializá-la com uma lista vazia de notas, definiríamos o valor inicial como um array vazio e, como as props não seriam utilizadas, poderíamos omitir o parâmetro props da definição da função:

const App = () => { 
  const [notes, setNotes] = useState([]) 

  // ...
}  

Vamos manter o valor inicial passado nas props por agora.

Em seguida, vamos adicionar um formulário HTML (form) ao componente que será usado para adicionar novas notas.

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)

  const addNote = (event) => {    event.preventDefault()    console.log('button clicked', event.target)  }
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>        <input />        <button type="submit">save</button>      </form>       </div>
  )
}

Adicionamos a função addNote como um gerenciador de evento ao elemento de formulário que será chamado quando o formulário for enviado, clicando no botão de envio save.

Usamos o método discutido na Parte 1 para definir nosso gerenciador de evento:

const addNote = (event) => {
  event.preventDefault()
  console.log('button clicked', event.target)
}

O parâmetro event é o evento que aciona a chamada para a função gerenciadora de evento:

O gerenciador de evento chama imediatamente o método event.preventDefault(), o que "impede a ação padrão" de enviar um formulário. A ação padrão causaria, entre outras coisas, o recarregamento da página.

O alvo (target) do evento armazenado em event.target é registrado no console:

console mostrando o botão clicado com o objeto formulário

O target neste caso é o formulário que definimos em nosso componente.

Como acessamos os dados armazenados no elemento input do formulário?

Um componente controlado

Existem muitas maneiras de fazer isso: o primeiro método que vamos utilizar é através do uso de componentes controlados (controlled components).

Vamos adicionar um novo pedaço de estado chamado newNote para armazenar a entrada fornecida pelo usuário e vamos defini-lo como o atributo value do elemento input (entrada):

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(    'a new note...'  ) 
  const addNote = (event) => {
    event.preventDefault()
    console.log('button clicked', event.target)
  }

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input value={newNote} />        <button type="submit">save</button>
      </form>   
    </div>
  )
}

O texto do espaço reservado armazenado como o valor inicial do estado newNote aparece no elemento input, mas o texto de entrada (input text) não é editável. O console exibe um aviso que nos dá uma dica do que pode estar errado:

console exibindo erro de valor fornecido para prop sem onchange

A partir do momento em que atribuímos um pedaço do estado do componente App como o atributo value do elemento de entrada (input element), o componente App passou a controlar o comportamento do elemento de entrada.

Para habilitar a edição do elemento de entrada, precisamos registrar um gerenciador de evento que sincroniza as mudanças feitas na entrada com o estado do componente:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(
    'a new note...'
  ) 

  // ...

  const handleNoteChange = (event) => {     console.log(event.target.value)    setNewNote(event.target.value)  }
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleNoteChange}        />
        <button type="submit">save</button>
      </form>   
    </div>
  )
}

Agora registramos um gerenciador de evento para o atributo onChange do elemento input do formulário:

<input
  value={newNote}
  onChange={handleNoteChange}
/>

O gerenciador de evento é chamado toda vez que ocorre uma mudança no elemento de entrada. A função do gerenciador de evento recebe o objeto de evento como seu parâmetro event:

const handleNoteChange = (event) => {
  console.log(event.target.value)
  setNewNote(event.target.value)
}

A propriedade target do objeto "event" agora corresponde ao elemento de entrada controlado, e event.target.value se refere ao valor (value) de entrada desse elemento.

Observe que não precisamos chamar o método event.preventDefault() como fizemos no gerenciador de evento onSubmit. Isso ocorre porque não há uma ação padrão em uma mudança de entrada, ao contrário do que ocorre em um envio de formulário.

É possível acompanhar no console como o gerenciador de evento é chamado:

múltiplas chamadas no console ao digitar typing text

Você se lembrou de instalar o React devtools (Ferramentas do Desenvolvedor React), certo? Ótimo. Você pode ver diretamente como o estado muda na guia "Componentes":

mudanças de estado no react devtools mostrando a digitação

Agora, o estado newNote do componente App reflete o valor atual do elemento de entrada, o que significa que podemos completar a função addNote para criar novas notas:

const addNote = (event) => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    important: Math.random() < 0.5,
    id: notes.length + 1,
  }

  setNotes(notes.concat(noteObject))
  setNewNote('')
}

Primeiramente, criamos um novo objeto para a nota (variável que cria as notas com suas propriedades) chamada noteObject que receberá seu conteúdo do estado newNote do componente. O identificador único id é gerado com base no número total de notas. Este método funciona para a nossa aplicação, já que as nossas notas nunca são excluídas. Com a ajuda da função Math.random(), a nossa nota tem 50% de chance de ser marcada como importante.

A nova nota é adicionada à lista de notas usando o método de array concat, apresentado na Parte 1:

setNotes(notes.concat(noteObject))

O método não muda o array original notes, porém, cria uma nova cópia do array com o novo item adicionado ao final. Isso é importante, já que nós nunca devemos mudar diretamente o estado em React!

O gerenciador de evento também limpa o valor do elemento de entrada controlado chamando a função setNewNote do estado newNote:

setNewNote('')

É possível encontrar o código atual completo da nossa aplicação na branch part2-2 neste repositório do GitHub.

Filtrando os elementos exibidos

Vamos adicionar algumas novas funcionalidades à nossa aplicação que nos permitam visualizar apenas as notas importantes.

Vamos adicionar um estado ao componente App que vai manter o registro das notas que devem ser exibidas:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)
  // ...
}

Modifiquemos o componente para que ele armazene uma lista de todas as notas a serem exibidas na variável notesToShow. Os itens na lista dependem do estado do componente:

import { useState } from 'react'
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState('') 
  const [showAll, setShowAll] = useState(true)

  // ...

  const notesToShow = showAll    ? notes    : notes.filter(note => note.important === true)
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notesToShow.map(note =>          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...
    </div>
  )
}

A definição da variável notesToShow é bastante compacta:

const notesToShow = showAll
  ? notes
  : notes.filter(note => note.important === true)

A definição utiliza o operador condicional (ou operador ternário) encontrado também em muitas outras linguagens de programação.

O operador funciona da seguinte maneira. Se tivermos

const result = condition ? val1 : val2

a variável result será definida com o valor de val1 se condition for verdadeiro. Se condition for falso, a variável result será definida com o valor de val2.

Se o valor de showAll for falso, a variável notesToShow será atribuída a uma lista que contém somente as notas que possuem a propriedade important definida como verdadeira. A filtragem é feita com a ajuda do método de array filter (filtrar):

notes.filter(note => note.important === true)

O operador ternário é redundante aqui, já que o valor de note.important é verdadeiro ou falso, o que significa que podemos simplesmente escrever:

notes.filter(note => note.important)

A razão de mostrarmos o operador de comparação primeiro foi para enfatizar um detalhe importante: em JavaScript, val1 == val2 não funciona como esperado em todas as situações e é mais seguro usar val1 === val2 exclusivamente em comparações. Leia mais sobre o assunto aqui.

É possível testar a funcionalidade de filtragem mudando o valor inicial do estado showAll.

Em seguida, vamos adicionar a funcionalidade que permite aos usuários alternar o estado showAll da aplicação a partir da interface de usuário (GUI).

As alterações relevantes são mostradas abaixo:

import { useState } from 'react' 
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  return (
    <div>
      <h1>Notes</h1>
      <div>        <button onClick={() => setShowAll(!showAll)}>          show {showAll ? 'important' : 'all' }                    </button>      </div>      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...    
    </div>
  )
}

As notas exibidas ("all" versus "important") são controladas com um botão. O gerenciador de evento do botão é tão simples que foi definido diretamente no atributo do elemento do botão. O gerenciador de evento alterna o valor de showAll de verdadeiro para falso e vice-versa:

() => setShowAll(!showAll)

O texto do botão depende do valor do estado showAll:

show {showAll ? 'important' : 'all'}

Você pode encontrar o código da nossa aplicação atual na íntegra na branch part2-3 neste repositório GitHub.