c

Lisää tyyleistä

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 joista suosituin on react-bootstrap.

Katsotaan seuraavaksi kahta UI-frameworkia bootstrapia ja MaterialUI: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.3.1/css/bootstrap.min.css"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
    crossorigin="anonymous"
  />
  // ...
</head>

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

5ea

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:

6ea

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:

7e

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:

8ea

Notifikaatio

Toteutetaan sovellukseen kirjautumisen jälkeinen notifikaatio:

9ea

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">
  {(message &&    <Alert variant="success">      {message}    </Alert>  )}  // ...
</div>

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

10ea

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

11ea

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ä

12ea

Esimerkin sovelluksen koodi kokonaisuudessaan täällä

Material UI

Tarkastellaan toisena esimerkkinä Googlen kehittämän "muotokielen" Material designin toteuttavaa React-kirjastoa MaterialUI.

Asennetaan kirjasto suorittamalla komento

npm install --save @material-ui/core

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

<head>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
  // ...
</head>

Tehdään nyt MaterialUI:n avulla koodiin suunnilleen samat muutokset, mitä teimme bootstrapilla.

Renderöidään koko sovelluksen sisältö komponentin Container sisälle:

import Container from '@material-ui/core/Container'

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

Aloitetaan komponentista Notes ja renderöidään muistiinpanojen lista taulukkona:

const Notes = ({notes}) => (
  <div>
    <h2>Notes</h2>

    <TableContainer component={Paper}>
      <Table>
        <TableBody>
          {notes.map(note => (
            <TableRow key={note.id}>
              <TableCell>
                <Link to={`/notes/${note.id}`}>{note.content}</Link>
              </TableCell>
              <TableCell>
                {note.name}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  </div>
)

Taulukko näyttää seuraavalta:

63eb

Hienoinen ikävä piirre Material UI:ssa on se, että jokainen komponentti on importattava erikseen, muistiinpanojen sivun import-lista on aika pitkä:

import {
  Container,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableRow,
  Paper,
} from '@material-ui/core'

Lomake

Parannellaan seuraavaksi näkymän Login kirjautumislomaketta käyttäen komponentteja TextField ja Button:

const Login = (props) => {
  const history = useHistory()

  const onSubmit = (event) => {
    event.preventDefault()
    props.onLogin('mluukkai')
    history.push('/')
  }

  return (
    <div>
      <h2>login</h2>
      <form onSubmit={onSubmit}>
        <div>
          <TextField label="username" />
        </div>
        <div>
          <TextField  label="password" type='password' />
        </div>
        <div>
          <Button variant="contained" color="primary" type="submit">
            login
          </Button>
        </div>
      </form>
    </div>
  )
}

Lopputulos on:

64ea

Bootstrapiin verrattuna pieni ero on nyt se, että MaterialUI ei tarjoa erillistä komponenttia itse lomakkeelle, lomake tehdään normaaliin tapaan HTML:n form-elementtinä.

Lomakkeen käyttämät komponentit on luonnollisesti importattava koodissa.

Notifikaatio

Kirjautumisen jälkeisen notifikaation näyttämiseen sopii komponenetti Alert, joka on lähes samanlainen kuin bootstrapin vastaava komponentti:

<div>
  {(message &&    <Alert severity="success">      {message}    </Alert>  )}</div>

Alert-komponentti ei ole vielä mukana MaterialUI:n core-pakkauksessa, ja komponentin sisältävä pakkaus lab tulee asentaa sovellukseen:

npm install --save @material-ui/lab

Komponentti importataan seuraavasti

import { Alert } from '@material-ui/lab'

Alert on ulkoasultaan tyylikäs:

65ea

Navigaatiorakenne

Navigaatiorakenne toteutetaan komponentin AppBar avulla

Jos sovelletaan suoraan dokumentaation esimerkkiä

<AppBar position="static">
  <Toolbar>
    <IconButton edge="start" color="inherit" aria-label="menu">
    </IconButton>
    <Button color="inherit">
      <Link to="/">home</Link>
    </Button>
    <Button color="inherit">
      <Link to="/notes">notes</Link>
    </Button>
    <Button color="inherit">
      <Link to="/users">users</Link>
    </Button>  
    <Button color="inherit">
      {user
        ? <em>{user} logged in</em>
        : <Link to="/login">login</Link>
      }
    </Button>                
  </Toolbar>
</AppBar>

saadaan kyllä toimiva ratkaisu, mutta sen ulkonäkö ei ole paras mahdollinen

66ea

Lueskelemalla dokumentaatiota, löytyy parempi tapa eli component props, jonka avulla voidaan muuttaa se miten MaterialUI-komponentin juurielementti renderöityy.

Määrittelemällä

<Button color="inherit" component={Link} to="/">
  home
</Button>

renderöidään komponentti Button, siten että sen juurikomponenttina onkin react-redux-kirjaston komponentti Link, jolle siirtyy polun kertova props to.

Navigaatiopalkin koodi kokonaisuudessaan on seuraava

<AppBar position="static">
  <Toolbar>
    <Button color="inherit" component={Link} to="/">
      home
    </Button>
    <Button color="inherit" component={Link} to="/notes">
      notes
    </Button>
    <Button color="inherit" component={Link} to="/users">
      users
    </Button>   
    {user
      ? <em>{user} logged in</em>
      : <Button color="inherit" component={Link} to="/login">
          login
        </Button>
    }                              
  </Toolbar>
</AppBar>

ja lopputulos on haluamamme kaltainen

67ea

Esimerkin sovelluksen koodi kokonaisuudessaan täällä

Loppuhuomioita

Ero react-bootstrapin ja MaterialUI:n välillä ei ole suuri. On makuasia kummalla tuotettu ulkoasu on tyylikkäämpi. En ole itse käyttänut MaterialUI:ta kovin paljoa, mutta ensikosketus on positiivinen. Dokumentaatio vaikuttaa aavistuksen react-bootstrapin dokumentaatiota selkeämmältä. Eri npm-kirjastojen lautausmääriä vertailevan sivuston https://www.npmtrends.com/ mukaan MaterialUI ohitti react-boostrapin suosiossa vuoden 2018 loppupuolella:

68ea

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

Sen sijaan että käytämme 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 Bootstrapin määrittelemää 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.

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.

Asennetaan styled-components ja tehdään sen avulla esimerkkisovellukseemme muutama tyylillinen muutos. Tehdään ensin kaksi tyylimäärittelyitä 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>      <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 style={padding} to="/login">login</Link>
        }
      </Navigation>
      <Switch>
        <Route path="/notes/:id">
          <Note note={note} />
        </Route>
        <Route path="/notes">
          <Notes notes={notes} />
        </Route>
        <Route path="/users">
          {user ? <Users /> : <Redirect to="/login" />}
        </Route>
        <Route path="/login">
          <Login onLogin={login} />
        </Route>
        <Route path="/">
          <Home />
        </Route>
      </Switch>
      
      <Footer>        <em>Note app, Department of Computer Science 2020</em>
      </Footer>    </Page>  )
}

Lopputulos on seuraavassa:

18ea

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.