Tämä on 3. periodilla pidetyn, jo päättyneen kurssin sisältö.

Jos haluat suorittaa kurssin nyt, mene osoitteeseen https://fullstackopen-2019.github.io/

Osassa 2 on jo katsottu kahta tapaa tyylien lisäämiseen eli vanhan koulukunnan yksittäistä CSS-tiedostoa, inline-tyylejä. Katsotaan tässä osassa vielä muutamaa tapaa.

Valmiit käyttöliittymätyylikirjastot

Eräs lähestymistapa sovelluksen tyylien määrittelyyn on valmiin "UI frameworkin", eli suomeksi ehkä käyttöliittymätyylikirjaston käyttö.

Ensimmäinen laajaa kuuluisuutta saanut UI framework oli Twitterin kehittämä Bootstrap, joka lienee edelleen UI frameworkeista eniten käytetty. Viime aikoina UI frameworkeja on noussut kuin sieniä sateella. Valikoima on niin iso, ettei tässä kannata edes yrittää tehdä tyhjentävää listaa.

Monet UI-frameworkit sisältävät web-sovellusten käyttöön valmiiksi määriteltyjä teemoja sekä "komponentteja", kuten painikkeita, menuja, taulukkoja. Termi komponentti on edellä kirjotettu hipsuissa sillä kyse ei ole samasta asiasta kuin React-komponentti. Useimmiten UI-frameworkeja käytetään sisällyttämällä sovellukseen frameworkin määrittelemät CSS-tyylitiedostot sekä Javascript-koodi.

Monesta UI-frameworkista on tehty React-ystävällisiä versiota, joissa UI-frameworkin avulla määritellyistä "komponenteista" on tehty React-komponentteja. Esim. Bootstrapista on olemassa parikin React-versiota reactstrap ja react-bootstrap.

Katsotaan seuraavaksi kahta UI-framworkia bootstrapia ja semantic ui:ta. Lisätään molempien avulla samantapaiset tyylit luvun React-router sovellukseen.

react bootstrap

Aloitetaan bootstrapista, käytetään kirjastoa react-bootstrap.

Asennetaan kirjasto suorittamalla komento

npm install --save react-bootstrap

Lisätään sitten sovelluksen tiedostoon public/index.html tagin head sisään bootstrapin css-määrittelyt lataava rivi:

<head>
<link
  rel="stylesheet"
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
  integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS"
  crossorigin="anonymous"
/>
  // ...
</head>

Kun sovellus ladataan uudelleen, näyttää se jo aavistuksen tyylikkäämmältä:

fullstack content

Bootstrapissa koko sivun sisältö renderöidään yleensä container:ina, eli käytännössä koko sovelluksen ympäröivä div-elementti merkitään luokalla container:

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

  return (
    <div class="container">      // ...
    </div>
  )
}

Sovelluksen ulkoasu muuttuu siten, että sisältö ei ole enää yhtä kiinni selaimen reunoissa:

fullstack content

Muutetaan seuraavaksi komponenttia Notes siten, että se renderöi muistiinpanojen listan taulukkona. React bootstrap tarjoaa valmiin komponentin Table, joten CSS-luokan käyttöön ei ole tarvetta.

const Notes = (props) => (
  <div>
    <h2>Notes</h2>
    <Table striped>
      <tbody>
        {props.notes.map(note =>
          <tr key={note.id}>
            <td>
              <Link to={`/notes/${note.id}`}>
                {note.content}
              </Link>
            </td>
            <td>
              {note.user}
            </td>
          </tr>
        )}
      </tbody>
    </Table>
  </div>
)

Ulkoasu on varsin tyylikäs:

fullstack content

Huomaa, että koodissa käytettävät React bootstrapin komponentit täytyy importata, eli koodiin on lisättävä:

import { Table } from 'react-bootstrap'

Lomake

Parannellaan seuraavaksi näkymän Login kirjautumislomaketta Bootstrapin lomakkeiden avulla.

React bootstrap tarjoaa valmiit komponentit myös lomakkeiden muodostamiseen (dokumentaatio tosin ei ole paras mahdollinen):

let Login = (props) => {
  // ...
  return (
    <div>
      <h2>login</h2>
      <Form onSubmit={onSubmit}>
        <Form.Group>
          <Form.Label>username:</Form.Label>
          <Form.Control
            type="text"
            name="username"
          />
          <Form.Label>password:</Form.Label>
          <Form.Control
            type="password"
          />
          <Button variant="primary" type="submit">
            login
          </Button>
        </Form.Group>
      </Form>
    </div>
)}

Importoitavien komponenttien määrä kasvaa:

import { Table, Form, Button } from 'react-bootstrap'

Lomake näyttää parantelun jälkeen seuraavalta:

fullstack content

Notifikaatio

Toteutetaan sovellukseen kirjautumisen jälkeinen notifikaatio:

fullstack content

Asetetaan notifikaatio kirjautumisen yhteydessä komponentin App tilan muuttujaan message:

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

  const [user, setUser] = useState(null)
  const [message, setMessage] = useState(null)
  const login = (user) => {
    setUser(user)
    setMessage(`welcome ${user}`)    setTimeout(() => {      setMessage(null)    }, 10000)  }
  // ...
}

ja renderöidään viesti Bootstrapin Alert-komponentin avulla. React bootstrap tarjoaa tähän jälleen valmiin React-komponentin:

<div className="container">
  <Router>
    <div>
      {(message &&        <Alert variant="success">          {message}        </Alert>      )}    //...
)}

Navigaatiorakenne

Muutetaan vielä lopuksi sovelluksen navigaatiomenu käyttämään Bootstrapin Navbaria. Tähänkin React bootstrap tarjoaa valmiit komponentit, dokumentaatio on hieman kryptistä, mutta trial and error johtaa lopulta toimivaan ratkaisuun:

<Navbar collapseOnSelect expand="lg" bg="dark" variant="dark">
  <Navbar.Toggle aria-controls="responsive-navbar-nav" />
  <Navbar.Collapse id="responsive-navbar-nav">
    <Nav className="mr-auto">
      <Nav.Link href="#" as="span">
        <Link style={padding} to="/">home</Link>
      </Nav.Link>
      <Nav.Link href="#" as="span">
        <Link style={padding} to="/notes">notes</Link>
      </Nav.Link>
      <Nav.Link href="#" as="span">
        <Link style={padding} to="/users">users</Link>
      </Nav.Link>
      <Nav.Link href="#" as="span">
        {user
          ? <em>{user} logged in</em>
          : <Link to="/login">login</Link>
        }
    </Nav.Link>
    </Nav>
  </Navbar.Collapse>
</Navbar>

Ulkoasu on varsin tyylikäs

fullstack content

Jos selaimen kokoa kaventaa, huomaamme että menu "kollapsoituu" ja sen saa näkyville vain klikkaamalla:

fullstack content

Bootstrap ja valtaosa tarjolla olevista UI-frameworkeista tuottavat responsiivisia näkymiä, eli sellaisia jotka renderöityvät vähintään kohtuullisesti monen kokoisilla näytöillä.

Chromen developer-konsolin avulla on mahdollista simuloida sovelluksen käyttöä erilaisilla mobiilipäätteillä

fullstack content

Esimerkin sovelluksen koodi kokonaisuudessaan täällä

Semantic UI

Olen käyttänyt bootstrapia vuosia, mutta reilu vuosi sitten siirryin Semantic UI:n käyttäjäksi. Kurssin tehtävien palautusovellus on tehty Semanticilla ja kokemukset ovat olleet rohkaisevia, erityisesti semanticin React-tuki on ensiluokkainen ja dokumentaatiokin huomattavasti parempi kuin bootstrapissa.

Lisätään nyt React-router-sovellukselle edellisen luvun tapaan tyylit semanticilla.

Aloitetaan asentamalla semantic-ui-react-kirjasto:

npm install --save semantic-ui-react

Lisätään sitten sovelluksen tiedostoon public/index.html head-tagin sisään semanticin css-määrittelyt lataava rivi (joka löytyy tästä):

<head>
  <link
    rel="stylesheet"
    href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"
  />
  // ...
</head>

Sijoitetaan koko sovelluksen renderöimä sisältö Semanticin komponentin Container sisälle.

Semanticin dokumentaatio sisältää jokaisesta komponentista useita esimerkkikoodinpätkiä, joiden avulla komponenttien käytön periaatteet on helppo omaksua:

fullstack content

Muutetaan komponentin App uloin div-elementti komponentiksi Container:

import { Container } from 'semantic-ui-react'

// ...

const App = () => {
  // ...
  return (
    <Container>
      // ...
    </Container>
  )
}

Sivun sisältö ei ole enää reunoissa kiinni:

fullstack content

Edellisen luvun tapaan, renderöidään muistiinpanot taulukkona, komponentin Table avulla. Koodi näyttää seuraavalta

import { Table } from 'semantic-ui-react'

const Notes = (props) => (
  <div>
    <h2>Notes</h2>
    <Table striped celled>
      <Table.Body>
        {props.notes.map(note =>
          <Table.Row key={note.id}>
            <Table.Cell>
              <Link to={`/notes/${note.id}`}>
                {note.content}
              </Link>
            </Table.Cell>
            <Table.Cell>
              {note.user}
            </Table.Cell>
          </Table.Row>
        )}
      </Table.Body>
    </Table>
  </div>

Muistiinpanojen lista näyttää seuraavalta:

fullstack content

Lomake

Otetaan kirjautumissivulla käyttöön Semanticin Form-komponentti:

import { Form, Button } from 'semantic-ui-react'

let Login = (props) => {
  const onSubmit = (event) => {
    // ...
  }

  return (
    <Form onSubmit={onSubmit}>
      <Form.Field>
        <label>username</label>
        <input name='username' />
      </Form.Field>
      <Form.Field>
        <label>password</label>
        <input type='password' />
      </Form.Field>
      <Button type='submit'>login</Button>
    </Form>
  )
}

Ulkoasu näyttää seuraavalta:

fullstack content

Notifikaatio

Edellisen luvun tapaan, toteutetaan sovellukseen kirjautumisen jälkeinen notifikaatio:

fullstack content

Kuten edellisessä luvussa, asetetaan notifikaatio kirjautumisen yhteydessä komponentin App tilan muuttujaan message:

const App = () => {
  // ...
  const [message, setMessage] = useState(null)

  const login = (user) => {
    setUser(user)
    setMessage(`welcome ${user}`)
    setTimeout(() => {
      setMessage(null)
    }, 10000)
  }

  // ...
}

ja renderöidään viesti käyttäen komponenttia Message:

<Container>
  {(message &&
    <Message success>
      {message}
    </Message>
  )}
  // ...
</Conteiner>

Navigaatiorakenne

Navigaatiorakenne toteutetaan komponentin Menu avulla:

<Router>
  <div>
    <Menu inverted>
      <Menu.Item link>
        <Link to="/">home</Link>
      </Menu.Item>
      <Menu.Item link>
        <Link to="/notes">notes</Link>
      </Menu.Item>
      <Menu.Item link>
        <Link to="/users">users</Link>
      </Menu.Item>
      <Menu.Item link>
        {user
          ? <em>{user} logged in</em>
          : <Link to="/login">login</Link>
        }
      </Menu.Item>
    </Menu>
    // ...
  </div>
</Router>

Lopputulos näyttää seuraavalta:

fullstack content

Esimerkin sovelluksen koodi kokonaisuudessaan täällä.

Loppuhuomioita

Ero react-bootstrapin ja semantic-ui-reactin välillä ei ole suuri. On makuasia kummalla tuotettu ulkoasu on tyylikkäämpi. Oma vuosia kestäneen bootstrapin käytön jälkeinen siirtymiseni semanticiin johtuu semanticin saumattomammasta React-tuesta, laajemmasta valmiiden komponenttien valikoimasta ja paremmasta sekä selkeämmästä dokumentaatiosta. Semantic UI projektin kehitystyön jatkuvuuden suhteen on kuitenkin viime aikoina ollut ilmoilla muutamia kysymysmerkkejä, ja tilannetta kannattaakin seurata.

Esimerkissä käytettiin UI-frameworkeja niiden React-integraatiot tarjoavien kirjastojen kautta.

Sen sijaan että käytimme kirjastoa React bootstrap, olisimme voineet aivan yhtä hyvin käyttää Bootstrapia suoraan, liittämällä HTML-elementteihin CSS-luokkia. Eli sen sijaan että määrittelimme esim. taulukon komponentin Table avulla

<Table striped>
  // ...
</Table>

olisimme voineet käyttää normaalia HTML:n taulukkoa table ja CSS-luokkaa

<table className="table striped">
  // ...
</table>

Taulukon määrittelyssä React bootstrapin tuoma etu ei ole suuri.

Tiiviimmän ja ehkä paremmin luettavissa olevan kirjoitusasun lisäksi toinen etu React-kirjastoina olevissa UI-frameworkeissa on se, että kirjastojen mahdollisesti käyttämä Javascript-koodi on sisällytetty React-komponentteihin. Esim. osa Bootstrapin komponenteista edellyttää toimiakseen muutamaakin ikävää Javascript-riippuvuutta joita emme mielellään halua React-sovelluksiin sisällyttää.

React-kirjastoina tarjottavien UI-frameworkkien ikävä puoli verrattuna frameworkin "suoraan käyttöön" on React-kirjastojen API:n mahdollinen epästabiilius ja osittain huono dokumentaatio. Tosin react-semanticin suhteen tilanne on paljon parempi kuin monien muiden UI-frameworkien sillä kyseessä on virallinen React-integraatio.

Kokonaan toinen kysymys on se kannattaako UI-frameworkkeja ylipäätän käyttää. Kukin muodostakoon oman mielipiteensä, mutta CSS:ää taitamattomalle ja puutteellisilla design-taidoilla varustetulle ne ovat varsin käyttökelpoisia työkaluja.

Muita UI-frameworkeja

Luetellaan tässä kaikesta huolimatta muitakin UI-frameworkeja. Jos oma suosikkisi ei ole mukana, tee pull request

Styled components

Tapoja liittää tyylejä React-sovellukseen on jo näkemiämme lisäksi muitakin.

Mielenkiintoisen näkökulman tyylien määrittelyyn tarjoaa ES6:n tagged template literal -syntaksia hyödyntävä styled components -kirjasto.

Tehdään styled-componentsin avulla esimerkkisovellukseemme muutama tyylillinen muutos. Tehdään ensin kaksi tyylimäärittelyyn käytettävää komponenttia:

import styled from 'styled-components'

const Button = styled.button`
  background: Bisque;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid Chocolate;
  border-radius: 3px;
`

const Input = styled.input`
  margin: 0.25em;
`

Koodi luo HTML:n elementeistä button ja input tyyleillä rikastetut versiot ja sijoitetaan ne muuttujiin Button ja Input:

Tyylien määrittelyn syntaksi on varsin mielenkiintoinen, css-määrittelyt asetetaan backtick-hipsujen sisään.

Määritellyt komponentit toimivat kuten normaali button ja input ja sovelluksessa käytetään niitä normaaliin tapaan:

const Login = (props) => {
  // ...
  return (
    <div>
      <h2>login</h2>
      <form onSubmit={onSubmit}>
        <div>
          username:
          <Input />        </div>
        <div>
          password:
          <Input type='password' />        </div>
        <Button type="submit" primary=''>login</Button>      </form>
    </div>
  )
}

Määritellään vielä seuraavat tyylien lisäämiseen tarkoitetut komponentit, jotka ovat kaikki rikastettuja versioita div-elementistä:

const Page = styled.div`
  padding: 1em;
  background: papayawhip;
`

const Navigation = styled.div`
  background: BurlyWood;
  padding: 1em;
`

const Footer = styled.div`
  background: Chocolate;
  padding: 1em;
  margin-top: 1em;
`

Otetaan uudet komponentit käyttöön sovelluksessa:

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

  return (
    <Page>      <Router>
        <div>
          <Navigation>            <Link style={padding} to="/">home</Link>
            <Link style={padding} to="/notes">notes</Link>
            <Link style={padding} to="/users">users</Link>
            {user
              ? <em>{user} logged in</em>
              : <Link to="/login">login</Link>
            }
          </Navigation>

          <Route exact path="/" render={() => <Home />} />
          <Route exact path="/notes" render={() =>
            <Notes notes={notes} />}
          />
          <Route exact path="/notes/:id" render={({ match }) =>
            <Note note={noteById(match.params.id)} />}
          />
          <Route path="/users" render={() =>
            user ? <Users /> : <Redirect to="/login" />
          } />
          <Route path="/login" render={() =>
            <Login onLogin={login} />}
          />
        </div>
      </Router>
      <Footer>        <em>Note app, Department of Computer Science 2019</em>
      </Footer>
    </Page>
  )
}

Lopputulos on seuraavassa:

fullstack content

Styled components on nostanut tasaisesti suosiotaan viime aikoina ja tällä hetkellä näyttääkin, että se on melko monien mielestä paras tapa React-sovellusten tyylien määrittelyyn.