b

Lomakkeiden käsittely

Jatketaan sovelluksen laajentamista siten, että se mahdollistaa uusien muistiinpanojen lisäämisen.

Jotta saisimme sivun päivittymään uusien muistiinpanojen lisäyksen yhteydessä, on parasta sijoittaa muistiinpanot komponentin App tilaan. Eli importataan funktio useState ja määritellään sen avulla komponentille tila, joka saa aluksi arvokseen propsina välitettävän muistiinpanot alustavan taulukon:

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 

Komponentti siis alustaa funktion useState avulla tilan notes arvoksi propseina välitettävän alustavan muistiinpanojen listan:

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

  // ...
}

Voimme vielä havainnollsitaa tilanteen React Developer Toolsin avulla:

fullstack content

Jos haluaisimme lähteä liikkeelle tyhjästä muistiinpanojen listasta, annettaisiin tilan alkuarvoksi tyhjä taulukko, ja koska komponentti ei käyttäisi ollenkaan propseja, voitaisiin parametri props jättää kokonaan määrittelemättä:

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

  // ...
}  

Jätetään kuitenkin toistaiseksi tilalle alkuarvon asettava määrittely voimaan.

Lisätään seuraavaksi komponenttiin lomake eli HTML form uuden muistiinpanon lisäämistä varten:

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

Lomakkeelle on lisätty myös tapahtumankäsittelijäksi funktio addNote reagoimaan sen "lähettämiseen" eli napin painamiseen.

Tapahtumankäsittelijä on osasta 1 tuttuun tapaan määritelty seuraavasti:

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

Parametrin event arvona on metodin kutsun aiheuttama tapahtuma.

Tapahtumankäsittelijä kutsuu heti tapahtuman metodia event.preventDefault() jolla se estää lomakkeen lähetyksen oletusarvoisen toiminnan, joka aiheuttaisi mm. sivun uudelleenlatautumisen.

Tapahtuman kohde eli event.target on tulostettu konsoliin:

fullstack content

Kohteena on siis komponentin määrittelemä lomake.

Miten pääsemme käsiksi lomakkeen input-komponenttiin syötettyyn dataan?

Kontrolloitu komponentti

Tapoja on useampia, joista tutustumme ensin kontrolloituina komponentteina toteutettuihin lomakkeisiin.

Lisätään komponentille App tila newNote lomakkeen syötettä varten ja määritellään se input-komponentin attribuutin value arvoksi:

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

Tilaan newNote määritelty "placeholder"-teksti a new note... ilmestyy syötekomponenttiin, mutta tekstiä ei voi muuttaa. Konsoliin tuleekin ikävä varoitus joka kertoo mistä on kyse:

fullstack content

Koska määrittelimme syötekomponentille value-attribuutiksi komponentin App tilassa olevan muuttujan, alkaa App kontrolloimaan syötekomponentin toimintaa.

Jotta kontrolloidun syötekomponentin editoiminen olisi mahdollista, täytyy sille rekisteröidä tapahtumankäsittelijä, joka synkronoi syötekenttään tehdyt muutokset komponentin App tilaan:

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

Lomakkeen input-komponentille on nyt rekisteröity tapahtumankäsittelijä tilanteeseen onChange:

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

Tapahtumankäsittelijää kutsutaan aina kun syötekomponentissa tapahtuu jotain. Tapahtumankäsittelijämetodi saa parametriksi tapahtumaolion event.

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

Tapahtumaolion kenttä target vastaa nyt kontrolloitua input-kenttää ja event.target.value viittaa inputin syötekentän arvoon.

Huomaa, että toisin kuin lomakkeen lähettämistä vastaavan tapahtuman onSubmit käsittelijässä, nyt oletusarvoisen toiminnan estävää metodikutsua event.preventDefault() ei tarvita, sillä syötekentän muutoksella ei ole oletusarvoista toimintaa toisin kuin lomakkeen lähettämisellä.

Voit seurata konsolista miten tapahtumankäsittelijää kutsutaan:

fullstack content

Olethan jo asentanut React Developer Toolsin? Developer Toolsista näet, miten tila muuttuu syötekenttään kirjoitettaessa:

fullstack content

Nyt komponentin App tila newNote heijastaa koko ajan syötekentän arvoa, joten voimme viimeistellä uuden muistiinpanon lisäämisestä huolehtivan metodin addNote:

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

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

Ensin luodaan uutta muistiinpanoa vastaava olio noteObject, jonka sisältökentän arvo saadaan komponentin tilasta newNote. Yksikäsitteinen tunnus eli id generoidaan kaikkien muistiinpanojen lukumäärän perusteella. Koska muistiinpanoja ei poisteta, menetelmä toimii sovelluksessamme. Komennon Math.random() avulla muistiinpanosta tulee 50 %:n todennäköisyydellä tärkeä.

Uusi muistiinpano lisätään vanhojen joukkoon oikeaoppisesti käyttämällä osasta 1 tuttua taulukon metodia concat:

setNotes(notes.concat(noteObject))

Metodi ei muuta alkuperäistä tilaa notes vaan luo uuden taulukon, joka sisältää myös lisättävän alkion. Tämä on tärkeää, sillä Reactin tilaa ei saa muuttaa suoraan!

Tapahtumankäsittelijä tyhjentää myös syötekenttää kontrolloivan tilan newNote sen funktiolla setNewNote.

setNewNote('')

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissä part2-2.

Näytettävien elementtien filtteröinti

Tehdään sovellukseen toiminto, joka mahdollistaa ainoastaan tärkeiden muistiinpanojen näyttämisen.

Lisätään komponentin App tilaan tieto siitä, näytetäänkö muistiinpanoista kaikki vai ainoastaan tärkeät:

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

Muutetaan komponenttia siten, että se tallettaa muuttujaan notesToShow näytettävien muistiinpanojen listan riippuen siitä, tuleeko näyttää kaikki vai vain tärkeät:

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

Muuttujan notesToShow määrittely on melko kompakti:

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

Käytössä on monissa muissakin kielissä oleva ehdollinen operaattori.

Lausekkeella

const tulos = ehto ? val1 : val2

muuttujan tulos arvoksi asetetaan val1:n arvo jos ehto on tosi. Jos ehto ei ole tosi, muuttujan tulos arvoksi tulee val2:n arvo.

Eli jos tilan arvo showAll on epätosi, muuttuja notesToShow saa arvokseen vain ne muistiinpanot, joiden important-kentän arvo on tosi. Filtteröinti tapahtuu taulukon metodilla filter:

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

Vertailuoperaatio on oikeastaan turha, koska note.important on arvoltaan joko true tai false eli riittää, kun kirjoitamme:

notes.filter(note => note.important)

Tässä käytettiin kuitenkin ensin vertailuoperaattoria mm. korostamaan erästä tärkeää seikkaa: JavaScriptissa arvo1 == arvo2 ei toimi kaikissa tilanteissa loogisesti ja onkin varmempi käyttää aina vertailuissa muotoa arvo1 === arvo2. Enemmän aiheesta on täällä.

Filtteröinnin toimivuutta voi jo nyt kokeilla vaihtelemalla sitä, miten tilan kentän showAll alkuarvo määritellään funktion useState parametrina.

Lisätään sitten toiminnallisuus, joka mahdollistaa showAll:in tilan muuttamisen sovelluksesta:

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)

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

Näkyviä muistiinpanoja (kaikki vai ainoastaan tärkeät) siis kontrolloidaan napin avulla. Napin tapahtumankäsittelijä on niin yksinkertainen, että se on kirjoitettu suoraan napin attribuutiksi. Tapahtumankäsittelijä muuttaa showAll:n arvon truesta falseksi ja päinvastoin:

() => setShowAll(!showAll)

Napin teksti riippuu tilan showAll arvosta:

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

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa branchissa part2-3.