c

Lisää tyyleistä

Osassa 2 on jo katsottu kahta tapaa tyylien lisäämiseen, eli vanhan koulukunnan yksittäistä CSS-tiedostoa ja 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 ja 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.

Monista UI-frameworkeista on tehty React-ystävällinen versio, jossa 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äyttämällä kirjastoa React-Bootstrap.

Asennetaan kirjasto:

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://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
    integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
    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 className="container">      // ...
    </div>
  )
}

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

fullstack content

Taulukko

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 = ({ notes }) => (
  <div>
    <h2>Notes</h2>
    <Table striped>      <tbody>
        {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>

Navigaatiomenu

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

Material UI

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

Asennetaan kirjasto:

npm install @mui/material @emotion/react @emotion/styled

Tehdään nyt MaterialUI:n avulla koodiin suunnilleen samat muutokset kuin mitä teimme Bootstrapilla.

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

import { Container } from '@mui/material'

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

Taulukko

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, joten muistiinpanojen sivun import-lista on aika pitkä:

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

Lomake

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

const Login = (props) => {
  const navigate = useNavigate()

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

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

fullstack content

Bootstrapiin verrattuna pieni ero on nyt se, että MaterialUI ei tarjoa erillistä komponenttia itse lomakkeelle, vaan 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 on ulkoasultaan tyylikäs:

fullstack content

Navigaatiomenu

Navigaatiomenu 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

Dokumentaatiota lueskelemalla 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 on 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 ja on kasvattanut sen jälkeen eroa tasaisesti:

fullstack content

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

React-Bootstrap-kirjaston sijaan 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-frameworkien 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-frameworkeja 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äkemiemme 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 sijoittaa ne muuttujiin Button ja Input.

Tyylien määrittelyn syntaksi on varsin mielenkiintoinen, sillä 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.