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 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ä:

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">
  {(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

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ä

Material UI

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

Asennetaan kirjasto suorittamalla komento

npm install @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:

fullstack content

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:

fullstack content

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 komponentti 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 @material-ui/lab

Komponentti importataan seuraavasti

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

Alert on ulkoasultaan tyylikäs:

fullstack content

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

fullstack content

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-router-dom-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

fullstack content

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änyt 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:

fullstack content

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:

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.