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ä:
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:
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:
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.Group>
<Form.Group>
<Form.Label>password:</Form.Label>
<Form.Control
type="password"
/>
</Form.Group>
<Button variant="primary" type="submit">
login
</Button>
</Form>
</div>
)
}
Importoitavien komponenttien määrä kasvaa:
import { Table, Form, Button } from 'react-bootstrap'
Lomake näyttää parantelun jälkeen seuraavalta:
Notifikaatio
Toteutetaan sovellukseen kirjautumisen jälkeinen notifikaatio:
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:
Jos selaimen kokoa kaventaa, huomaamme että menu "kollapsoituu" ja sen saa näkyville vain klikkaamalla:
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ä:
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:
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:
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:
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:
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:
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:
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:
- https://bulma.io/
- https://ant.design/
- https://get.foundation/
- https://chakra-ui.com/
- https://tailwindcss.com/
- https://semantic-ui.com/
- https://mantine.dev/
- https://react.fluentui.dev/
- https://www.primefaces.org/primereact/
- https://v2.grommet.io
- https://blueprintjs.com
- https://evergreen.segment.com
- https://www.radix-ui.com/
- https://react-spectrum.adobe.com/react-aria/index.html
- https://master.co/
- https://baseweb.design/
- https://ui.aceternity.com/
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:
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.