Saltar al contenido

b

Formularios

Continuemos expandiendo nuestra aplicación permitiendo a los usuarios agregar nuevas notas. Puedes encontrar el código de nuestra aplicación actual aquí.

Guardando las notas en el estado del componente

Para que nuestra página se actualice cuando se agregan nuevas notas, es mejor almacenar las notas en el estado del componente App. Importemos la función useState y usémosla para definir una parte del estado que se inicializa con la matriz de notas inicial pasada en los 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 

El componente usa la función useState para inicializar la parte de estado almacenada en notes con la matriz de notas pasadas en los props:

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

  // ...
}

También podemos utilizar React Developer Tools para comprobar que esto realmente sucede:

navegador mostrando la ventana de herramientas de desarrollo de React

Si quisiéramos comenzar con una lista vacía de notas, estableceríamos el valor inicial como una matriz vacía, y dado que los props no se usarían, podríamos omitir el parámetro props de la definición de la función:

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

  // ...
}  

Sigamos con el valor inicial pasado en los props por el momento.

A continuación, agreguemos un formulario HTML al componente que se utilizará para agregar nuevas 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>
  )
}

Hemos agregado la función addNote como un controlador de eventos al elemento del formulario que se llamará cuando se envíe el formulario, haciendo clic en el botón submit.

Usamos el método discutido en la parte 1 para definir nuestro controlador de eventos:

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

El parámetro event es el evento que activa la llamada a la función del controlador de eventos:

El controlador de eventos llama inmediatamente al método event.preventDefault(), que evita la acción predeterminada de enviar un formulario. La acción predeterminada, entre otras cosas, haría que la página se recargara.

El objetivo del evento almacenado en event.target se registra en la consola:

botón clickeado con objeto de formulario en la consola

El objetivo (target) en este caso es el formulario que hemos definido en nuestro componente.

¿Cómo accedemos a los datos contenidos en el elemento input del formulario?

Componentes controlados

Hay muchas maneras de lograr esto; el primer método que veremos es mediante el uso de los llamados componentes controlados.

Agreguemos un nuevo estado llamado newNote para almacenar la entrada enviada por el usuario y configurémoslo como el atributo value del elemento input:

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>
  )
}

El texto del placeholder almacenado como valor inicial del estado newNote aparece en el elemento input, pero el input no se puede editar. La consola muestra una advertencia que nos da una pista de lo que podría estar mal:

error de consola al proporcionar un valor a la propiedad sin onchange

Dado que asignamos una parte del estado del componente App como el atributo value del elemento input, el componente App ahora controla el comportamiento del input.

Para habilitar la edición del input, tenemos que registrar un controlador de eventos que sincronice los cambios realizados en la entrada con el estado del 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>
  )
}

Ahora hemos registrado un controlador de eventos en el atributo onChange del elemento input del formulario:

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

Se llama al controlador de eventos cada vez que ocurre un cambio en el elemento input. La función del controlador de eventos recibe el objeto de evento como su parámetro event:

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

La propiedad target del objeto de evento ahora corresponde al elemento input controlado y event.target.value se refiere al valor de entrada de ese elemento.

Ten en cuenta que no necesitamos llamar al método event.preventDefault() como hicimos en el controlador de eventos onSubmit. Esto se debe a que no se produce una acción predeterminada en un cambio de input, a diferencia de lo que ocurre con el envío de un formulario.

Puedes ver en la consola cómo se llama al controlador de eventos:

multiples llamados en la consola al escribir texto

Has recordado instalar React devtools, ¿verdad? Bien. Puedes ver directamente cómo cambia el estado desde la pestaña React Devtools:!

cambios en el estado al escribir texto en react devtools

Ahora el estado del componente newNote de App refleja el valor actual del input, lo que significa que podemos completar la función addNote para crear nuevas notas:

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

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

Primero creamos un nuevo objeto para la nota llamado noteObject que recibirá su contenido del estado del componente newNote. El identificador único id se genera en función del número total de notas. Este método funciona para nuestra aplicación ya que las notas nunca se eliminan. Con la ayuda de la función Math.random(), nuestra nota tiene un 50% de posibilidades de ser marcada como importante.

La nueva nota se agrega a la lista de notas usando el método de matriz concat, introducido en la parte 1:

setNotes(notes.concat(noteObject))

El método no muta la matriz notes original, sino que crea una nueva copia de la matriz con el nuevo elemento agregado al final. Esto es importante ya que nunca debemos mutar el estado directamente en React!

El controlador de eventos también restablece el valor del elemento de entrada controlado llamando a la función setNewNote del estado de newNote:

setNewNote('')

Puedes encontrar el código para nuestra aplicación actual en su totalidad en la rama part2-2 de este repositorio de GitHub.

Filtrado de elementos mostrados

Agreguemos una nueva funcionalidad a nuestra aplicación que nos permita ver solo las notas importantes.

Agreguemos un fragmento de estado al componente App que realiza un seguimiento de las notas que deben mostrarse:

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

Cambiemos el componente para que almacene una lista de todas las notas que se mostrarán en la variable notesToShow. Los elementos de la lista dependen del estado del componente:

import React, { 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>
  )
}

La definición de la variable notesToShow es bastante compacta:

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

La definición usa el operador condicional que también se encuentra en muchos otros lenguajes de programación.

El operador funciona de la siguiente manera. Si tenemos:

const result = condition ? val1 : val2

la variable result se establecerá en el valor de val1 si la condición (condition) es verdadera. Si la condition es falsa, la variable result se establecerá en el valor de val2.

Si el valor de showAll es falso, la variable notesToShow se asignará a una lista que solo contiene notas que tienen la propiedad important establecida en true. El filtrado se realiza con la ayuda del método de matriz filter:

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

El operador de comparación es de hecho redundante, ya que el valor de note.important es true o false lo que significa que simplemente podemos escribir:

notes.filter(note => note.important)

La razón por la que mostramos el operador de comparación primero fue para enfatizar un detalle importante: en JavaScript val1 == val2 no funciona como se esperaba en todas las situaciones y es más seguro utilizar val1 === val2 exclusivamente en las comparaciones. Puedes leer más sobre el tema aquí.

Puedes probar la funcionalidad de filtrado cambiando el valor inicial del estado showAll.

A continuación, agreguemos una funcionalidad que permita a los usuarios alternar el estado showAll de la aplicación desde la interfaz de usuario.

Los cambios relevantes se muestran a continuación:

import React, { 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>
  )
}

Las notas mostradas (todas versus las importantes) se controlan con un botón. El controlador de eventos para el botón es tan simple que se ha definido directamente en el atributo del elemento del botón. El controlador de eventos cambia el valor de showAll de verdadero a falso y viceversa:

() => setShowAll(!showAll)

El texto del botón depende del valor del estado de showAll:

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

Puedes encontrar el código para nuestra aplicación actual en su totalidad en la rama part2-3 de este repositorio de GitHub.