b

Formularios

Continuemos expandiendo nuestra aplicación permitiendo a los usuarios agregar nuevas notas.

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 React, { 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 = () => { 
  const [notes, setNotes] = useState([]) 

  // ...
}  

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:

fullstack content

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?

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 marcador de posición almacenado como valor inicial del estado newNote aparece en el elemento input, pero el texto de entrada no se puede editar. La consola muestra una advertencia que nos da una pista de lo que podría estar mal:

fullstack content

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

Para habilitar la edición del elemento de entrada, 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:

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

Se llama al controlador de eventos cada vez que ocurre un cambio en el elemento de entrada. 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.

Tenga 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 entrada, a diferencia de lo que ocurre con el envío de un formulario.

Puede seguir en la consola para ver cómo se llama el controlador de eventos:

fullstack content

Se acordó de instalar React devtools, ¿verdad? Bueno. Puede ver directamente cómo cambia el estado desde la pestaña React Devtools:!

fullstack content

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

const addNote = (event) => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    date: new Date().toISOString(),
    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 esatdo 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('')

Puede 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 permite 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. Puede leer más sobre el tema aquí.

Puede 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'}

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