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 React, { useState } from 'react'import Note from './components/Note'

const App = (props) => {  const [notes, setNotes] = useState(props.notes)
  const rows = () => notes.map(note =>
    <Note
      key={note.id}
      note={note}
    />
  )

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {rows()}
      </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) 

  // ...
}

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 rows = () => // ...

  const addNote = (event) => {    event.preventDefault()    console.log('button clicked', event.target)  }
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {rows()}
      </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?

Tapoja on useampia, tutustumme ensin ns. 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...'  )   // ...

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {rows()}
      </ul>
      <form onSubmit={addNote}>
        <input value={newNote} />        <button type="submit">save</button>
      </form>      
    </div>
  )
}

Tilaan newNote määritelty "placeholder"-teksti uusi muistiinpano... ilmestyy syötekomponenttiin, tekstiä ei kuitenkaan 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>
        {rows()}
      </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ää metodikutusua 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

Muistithan jo asentaa React devtoolsin? Devtoolsista 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,
    date: new Date().toISOString(),
    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% 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än hetkinen 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:

const App = (props) => {
  // ..

  const notesToShow = showAll    ? notes    : notes.filter(note => note.important === true)
  const rows = () => notesToShow.map(note =>    <Note
      key={note.id}
      note={note}
    />
  )

  // ...
}  

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.

Operaattori toimii seuraavasti. Jos meillä on esim:

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 vaan ne muistiinpanot, joiden important-kentän arvo on tosi. Filtteröinti tapahtuu taulukon metodilla filter:

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

vertailu-operaatio on oikeastaan turha, koska note.important on arvoltaan joko true tai false, eli riittää kirjoittaa

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 täällä.

Filtteröinnin toimivuutta voi jo nyt kokeilla vaihtelemalla sitä, miten tilan kentän showAll alkuarvo määritelään konstruktorissa.

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

Oleelliset muutokset ovat seuraavassa:

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>
        {rows()}
      </ul>
      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleNoteChange}
        />
        <button type="submit">save</button>
      </form>      
    </div>
  )
}

Näkyviä muistiinpanoja (kaikki vai ainoastaan tärkeät) siis kontrolloidaan napin avulla. Napin tapahtumankäsittelijä on niin yksinkertainen että se on kirjotettu 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än hetkinen koodi on kokonaisuudessaan githubissa, branchissa part2-3.