e) 🍬 Librairie pour UI
Introduction aux librairies de composants React
Il existe de nombreuses librairies de composants React qui permettent de rendre plus facile et plus rapide la création de UI : Material UI, Ant Design, Chakra UI, ...
Pour ce tutoriel optionnel, nous allons utiliser Material UI, une librairie de composants React très populaire qui permet de créer des applications web modernes et réactives.
Toute la documentation de cette librairie est disponible : https://mui.com/material-ui/
Mise en place de Material UI
Installation de Material UI
Pour ce tutoriel, veuillez créer une copie de la solution de l'exercice 2.8
, si nécessaire voici le code de ex2.8, et l'appeler ui-library
. Changez le nom du projet dans package.json
.
Commencez par installer les librairies de bases nécessaires :
shnpm install @mui/material @emotion/react @emotion/styled @fontsource/roboto @mui/icons-material
Utilisation de Material UI
Par défaut, Material UI utilise le font Roboto
qu'il faut installer. Pour utiliser un font, il faut ensuite faire un import
, par exemple dans votre script d'entrée de votre application /src/main.tsx
:
tsximport "@fontsource/roboto/700.css";
Reset global du CSS
Il est de bonne pratique de normaliser tous les composants HTML en faisant appel à CssBaseline
.
Veuillez mettre à jour /src/main.tsx
ainsi :
tsximport React from "react";import ReactDOM from "react-dom/client";import "@fontsource/roboto/700.css";import CssBaseline from "@mui/material/CssBaseline";import App from "./components/App";import "./index.css";ReactDOM.createRoot(document.getElementById("root")!).render(<React.StrictMode><CssBaseline /> {/* Global CSS reset from Material-UI */}<App /></React.StrictMode>);
Principe général de fonctionnement de MUI
MUI
met à disposition beaucoup de composants qui permettent de créer des UI en utilisant les règles de Material Design
comme référence.
Tous les composants peuvent être découverts ici : https://mui.com/material-ui/all-components/
Les composants peuvent être taillés sur mesure selon différentes stratégies. En voici les principales :
- Customiser un seul élément d'un composant
MUI
via la propsx
(les valeurs sont un superset de CSS) ou via la propclassName
(pour utiliser des classes CSS personnelles); - Créer un composant réutilisable à partir d'un composant
MUI
et de l'utilitairestyled
; - Faire une surcharge d'un composant
MUI
via untheme
; - Faire une surcharge globale du CSS de certains éléments HTML en utilisant le composant
GlobalStyles
.
Dans ce cours, nous allons explorer la première option uniquement à l'aide de sx
. N'hésitez pas à en découvrir plus par vous-même via : https://mui.com/material-ui/customization/how-to-customize/
Il existe des composants de layout qui permettent d'agencer d'autres composants horizontalement ou verticalement, principalement :
Box
: Un composant qui sert de conteneur flexible pour appliquer des marges, des paddings, des alignements et d'autres styles CSS aux enfants.Container
: Un composant qui centre et limite la largeur du contenu à une taille prédéfinie pour maintenir des marges cohérentes et une mise en page réactive.Grid
: Un composant pour créer des mises en page en grille réactives, permettant de définir des rangées et des colonnes avec des espacements et des alignements configurables.Stack
: Un composant qui simplifie l'agencement des enfants en les empilant verticalement ou horizontalement avec des espacements uniformes.
Le système de breakpoints de Material UI permet de créer des mises en page réactives, c'est-à-dire en ajustant le rendu des composants en fonction de la taille de l'écran. Material UI propose plusieurs breakpoints par défaut qui correspondent à des largeurs d'écran courantes.
Les breakpoints par défaut de Material UI sont définis comme suit :
xs
(extra-small): 0px et plussm
(small): 600px et plusmd
(medium): 900px et pluslg
(large): 1200px et plusxl
(extra-large): 1536px et plus
Par exemple, le composant Grid utilise ces breakpoints pour définir le nombre de colonnes à afficher à différentes largeurs d'écran :
tsx<Grid container spacing={2}><Grid xs={12} md={6}><Item>xs=6 md=8</Item></Grid><Grid xs={12} md={6}><Item>xs=6 md=4</Item></Grid><Grid xs={12} md={6}><Item>xs=6 md=4</Item></Grid><Grid xs={12} md={6}><Item>xs=6 md=8</Item></Grid></Grid>
Ce Grid
permet à un composant d'occuper 6 colonnes sur 12 du viewport quand la largeur du viewport est de 600 et plus pixel. Pour les viewport plus petits, le composant remplit les 12 colonnes disponibles. Cela permet de créer une mise en page réactive qui s'adaptent à la taille de l'écran : soit 2 colonnes sur un écran large, soit 1 colonne sur un écran plus petit.
Utilisation de base des composants MUI
Pour ce tutoriel, les composants MUI
qui semblent applicables à l'UI de notre application gérant une pizzeria ont été sélectionnés sur base de la documentation de MUI
.
Nous vous proposons de mettre à jour les scripts de votre projet sans aucune gestion du style : nous allons donc enlever toutes les références au CSS, et nous n'allons pas encore utiliser le système de MUI
pour styler les éléments de notre applications.
Nous avons déjà mis à jour /src/main.tsx
. Continuons donc par la mise à jour de Main
(dans /src/components/App/index.tsx
) :
tsx1import Footer from "../Footer";2import Header from "../Header";3import Main from "../Main";4import { useState } from "react";5import { Box } from "@mui/material";67function App() {8const [actionToBePerformed, setActionToBePerformed] = useState(false);910const handleHeaderClick = () => {11setActionToBePerformed(true);12};1314const clearActionToBePerformed = () => {15setActionToBePerformed(false);16};1718return (19<Box>20<Header21title="We love Pizza"22version={0 + 1}23handleHeaderClick={handleHeaderClick}24/>25<Main26actionToBePerformed={actionToBePerformed}27clearActionToBePerformed={clearActionToBePerformed}28/>2930<Footer />31</Box>32);33}3435export default App;
Nous avons juste utilisé Box
pour prendre la place d'une div
et nous n'utilisons plus App.css
.
Veuillez ensuite mettre à jour le Header
(dans /src/components/Header/index.tsx
) :
tsx1import { Box, Container, Typography } from "@mui/material";2import { useState } from "react";34interface HeaderProps {5title: string;6version: number;7handleHeaderClick: () => void;8}910const Header = ({ title, handleHeaderClick }: HeaderProps) => {11const [menuPrinted, setMenuPrinted] = useState(false);1213const handleClick = () => {14console.log(`value of menuPrinted before click: ${menuPrinted}`);15setMenuPrinted(!menuPrinted);16handleHeaderClick();17};1819return (20<Box21component="header"22onClick={handleClick}23>24<Container maxWidth="sm">25<Typography variant="h1">26{menuPrinted ? `${title}... and rarely do we hate it!` : title}27</Typography>28</Container>29</Box>30);31};3233export default Header;
Là nous utilisons :
Box
: pour créer notre future élément<header>
.Container
: pour créer un container qui centre ses éléments et dont la largeur vaut maximumsm
(600px et plus).Typography
: pour gérer le texte (qui deviendra un élément<h1>
).
Nous n'utilisons plus Header.css
.
Veuillez ensuite mettre à jour le Main
(dans /src/components/Main/index.tsx
). Notons que nous souhaitons faire un refactor de l'application afin que le menu des boissons soit affiché sur base d'une collection de données :
tsx1import { useState } from "react";2import sound from "../../assets/sounds/Infecticide-11-Pizza-Spinoza.mp3";3import DrinkMenu from "./DrinkMenu";4import PizzaMenu from "./PizzaMenu";5import { NewPizza, Pizza, Drink} from "../../types";6import AddPizza from "./AddPizza";7import AudioPlayer from "./AudioPlayer";8import { Container, Typography } from "@mui/material";910const defaultPizzas: Pizza[] = [11{12id: 1,13title: "4 fromages",14content: "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates",15},16{17id: 2,18title: "Vegan",19content: "Tomates, Courgettes, Oignons, Aubergines, Poivrons",20},21{22id: 3,23title: "Vegetarian",24content: "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives",25},26{27id: 4,28title: "Alpage",29content: "Gruyère, Mozarella, Lardons, Tomates",30},31{32id: 5,33title: "Diable",34content: "Tomates, Mozarella, Chorizo piquant, Jalapenos",35},36];3738const drinks: Drink[] = [39{40title: "Coca-Cola",41image:42"https://media.istockphoto.com/id/1289738725/fr/photo/bouteille-en-plastique-de-coke-avec-la-conception-et-le-chapeau-rouges-d%C3%A9tiquette.jpg?s=1024x1024&w=is&k=20&c=HBWfROrGDTIgD6fuvTlUq6SrwWqIC35-gceDSJ8TTP8=",43volume: "Volume: 33cl",44price: "2,50 €",45},46{47title: "Pepsi",48image:49"https://media.istockphoto.com/id/185268840/fr/photo/bouteille-de-cola-sur-un-fond-blanc.jpg?s=1024x1024&w=is&k=20&c=xdsxwb4bLjzuQbkT_XvVLyBZyW36GD97T1PCW0MZ4vg=",50volume: "Volume: 33cl",51price: "2,50 €",52},53{54title: "Eau Minérale",55image:56"https://media.istockphoto.com/id/1397515626/fr/photo/verre-deau-gazeuse-%C3%A0-boire-isol%C3%A9.jpg?s=1024x1024&w=is&k=20&c=iEjq6OL86Li4eDG5YGO59d1O3Ga1iMVc_Kj5oeIfAqk=",57volume: "Volume: 50cl",58price: "1,50 €",59},60];6162interface MainProps {63actionToBePerformed: boolean;64clearActionToBePerformed: () => void;65}6667const Main = ({ actionToBePerformed, clearActionToBePerformed }: MainProps) => {68const [pizzas, setPizzas] = useState(defaultPizzas);6970const addPizza = (newPizza: NewPizza) => {71const pizzaAdded = { ...newPizza, id: nextPizzaId(pizzas) };72setPizzas([...pizzas, pizzaAdded]);73};7475return (76<Container component="main" sx={{ mt: 8, mb: 2, flex: "1" }} maxWidth="sm">77<Typography variant="h2" component="h1" gutterBottom>78My HomePage79</Typography>80<Typography variant="h5" component="h2" gutterBottom>81Because we love JS, you can also click on the header to stop / start the82music ; )83</Typography>84<AudioPlayer85sound={sound}86actionToBePerformed={actionToBePerformed}87clearActionToBePerformed={clearActionToBePerformed}88/>8990<PizzaMenu pizzas={pizzas} />9192<br/>9394<AddPizza addPizza={addPizza} />9596<br/>9798<DrinkMenu title="Notre Menu de Boissons" drinks={drinks} />99</Container>100);101};102103const nextPizzaId = (pizzas: Pizza[]) => {104return pizzas.reduce((maxId, pizza) => Math.max(maxId, pizza.id), 0) + 1;105};106107export default Main;
Il n'y a pas de composants MUI
non rencontrés précédemment. Il n'y a plus de CSS.
A ce stade-ci, il est normal qu'il y ait toujours des erreurs dans votre application. Nous allons les corriger en changeant plusieurs composants.
Voici le nouveau type Drink
qui doit être ajouté à /src/types.ts
:
ts1interface Pizza {2id: number;3title: string;4content: string;5}67type NewPizza = Omit<Pizza, "id">;89interface Drink {10title: string;11image: string;12volume: string;13price: string;14}1516export type { Pizza, NewPizza, Drink };
Le composant AudioPlayer
ne doit pas être mis à jour. Le composant CssBaseline
offre déjà un joli style de base.
Veuillez ensuite mettre à jour PizzaMenu
(dans /src/components/Main/PizzaMenu.tsx
) :
tsx1import {2Table,3TableBody,4TableCell,5TableContainer,6TableHead,7TableRow,8Paper,9} from "@mui/material";1011import { Pizza } from "../../types";1213interface PizzaMenuProps {14pizzas: Pizza[];15}16const PizzaMenu = ({ pizzas }: PizzaMenuProps) => {17return (18<TableContainer component={Paper}>19<Table>20<TableHead>21<TableRow>22<TableCell>Pizza</TableCell>23<TableCell>æ©escription</TableCell>24</TableRow>25</TableHead>26<TableBody>27{pizzas.map((pizza) => (28<TableRow key={pizza.id}>29<TableCell>{pizza.title}</TableCell>30<TableCell>{pizza.content}</TableCell>31</TableRow>32))}33</TableBody>34</Table>35</TableContainer>36);37};3839export default PizzaMenu;
Pour ce composant, nous avons utilisé tous les composants MUI
permettant de créer une table HTML : https://mui.com/material-ui/react-table/#basic-table.
Pour le menu des boissons, nous avons créé un tout nouveau design pour être basé sur des données plutôt que des composants enfants (via children
).
Veuillez mettre à jour DrinkMenu
(dans /src/components/Main/DrinkMenu.tsx
) :
tsx1import {2Container,3Card,4CardMedia,5CardContent,6Typography,7Grid,8} from "@mui/material";9import { Drink } from "../../types";1011interface DrinkMenuProps {12title: string;13drinks: Drink[];14}1516const DrinkMenu: React.FC<DrinkMenuProps> = ({ title, drinks }) => {17return (18<Container>19<Typography variant="h4" gutterBottom>20{title}21</Typography>22<Grid container spacing={3}>23{drinks.map((drink, index) => (24<Grid item xs={12} sm={6} key={index}>25<Card>26<CardMedia27component="img"28image={drink.image}29alt={drink.title}30style={{ objectFit: "contain", height: "200px" }} // Ensure image is fully visible31/>32<CardContent>33<Typography gutterBottom variant="h5" component="div">34{drink.title}35</Typography>36<Typography variant="body2" color="textSecondary" component="p">37{drink.volume}38</Typography>39<Typography variant="body2" color="textSecondary" component="p">40Prix: {drink.price}41</Typography>42</CardContent>43</Card>44</Grid>45))}46</Grid>47</Container>48);49};5051export default DrinkMenu;
Nous avons utilisé le composant Card
du MUI
pour l'UI de chaque boisson : https://mui.com/material-ui/react-card/.
Veuillez ensuite mettre à jour AddPizza
(dans /src/components/Main/AddPizza.tsx
) :
tsx1import { useState, SyntheticEvent } from "react";23import { NewPizza } from "../../types";4import { Box, Button, TextField } from "@mui/material";56interface AddPizzaProps {7addPizza: (pizza: NewPizza) => void;8}910const AddPizza = ({ addPizza }: AddPizzaProps) => {11const [pizza, setPizza] = useState("");12const [description, setDescription] = useState("");1314const handleSubmit = (e: SyntheticEvent) => {15e.preventDefault();16addPizza({ title: pizza, content: description });17};1819const handlePizzaChange = (e: SyntheticEvent) => {20const pizzaInput = e.target as HTMLInputElement;21console.log("change in pizzaInput:", pizzaInput.value);22setPizza(pizzaInput.value);23};2425const handleDescriptionChange = (e: SyntheticEvent) => {26const descriptionInput = e.target as HTMLInputElement;27console.log("change in descriptionInput:", descriptionInput.value);28setDescription(descriptionInput.value);29};3031return (32<Box>33<form onSubmit={handleSubmit}>34<Box sx={{ marginBottom: 2 }}>35<TextField36fullWidth37id="pizza"38name="pizza"39label="Pizza"40variant="outlined"41value={pizza}42onChange={handlePizzaChange}43required44color="primary"45/>46</Box>47<Box sx={{ marginBottom: 2 }}>48<TextField49fullWidth50id="description"51name="description"52label="Description"53variant="outlined"54value={description}55onChange={handleDescriptionChange}56required57color="primary"58/>59</Box>60<Button type="submit" variant="contained" color="primary">61Ajouter62</Button>63</form>64</Box>65);66};6768export default AddPizza;
Nous avons utilisé TextField
afin de gérer les 2 inputs de notre formulaire.
Il ne reste plus qu'à mettre à jour Footer
(dans /src/components/Footer/index.tsx
) :
tsx1import { Box, Container, Typography } from "@mui/material";2import logo from "../../assets/images/js-logo.png";3import { Copyright } from "@mui/icons-material";45const Footer = () => {6return (7<Box component="footer" color="">8<Container maxWidth="sm">9<Box>10<Typography variant="body2">But we also love JS</Typography>11<Typography>12<Copyright />13myAmazingPizzeria14</Typography>15</Box>16<Box>17<img src={logo} alt="" width={50} />18</Box>19</Container>20</Box>21);22};2324export default Footer;
Nous avons utilisé l'icône Copyright
pour le Footer
.
Veuillez exécutez l'application !
Le résultat est fort intéressant : l'interface est très épurée, avec un look assez professionnel. Mais ça manque de style !
Exemple d'utilisation de la prop sx
Notre application possède déjà un thème par défaut, même si nous ne l'utilisons actuellement pas vraiment.
Nous allons donc maintenant styler de manière individuelle chacun des éléments MUI
à l'aide de la prop sx
. En fait, vous allez faire du CSS très ciblé, sans devoir créer de classes.
Voici la mise à jour du composant App
afin d'ajouter la photo de background et de s'assurer que l'application prendra au minimum une hauteur de 100% du viewport (pour avoir un footer qui sera toujours en bas de page) :
tsx1import pizza from "../../assets/images/pizza.jpg";2// Other imports...3function App() {4const [actionToBePerformed, setActionToBePerformed] = useState(false);56const handleHeaderClick = () => {7setActionToBePerformed(true);8};910const clearActionToBePerformed = () => {11setActionToBePerformed(false);12};1314return (15<Box sx={{16display: 'flex',17flexDirection: 'column',18height: '100%',19backgroundImage: `url(${pizza})`,20backgroundSize: 'cover',21}}>22232425<Header26title="We love Pizza"27version={0 + 1}28handleHeaderClick={handleHeaderClick}29/>30<Main31actionToBePerformed={actionToBePerformed}32clearActionToBePerformed={clearActionToBePerformed}33/>3435<Footer />36</Box>37);38}
Voici la mise à jour du composant Header
afin d'obtenir la couleur du thème :
tsx1import { Box, Container, Typography, useTheme } from "@mui/material";2import { useState } from "react";34interface HeaderProps {5title: string;6version: number;7handleHeaderClick: () => void;8}910const Header = ({ title, handleHeaderClick }: HeaderProps) => {11const theme = useTheme();12const [menuPrinted, setMenuPrinted] = useState(false);1314const handleClick = () => {15console.log(`value of menuPrinted before click: ${menuPrinted}`);16setMenuPrinted(!menuPrinted);17handleHeaderClick();18};1920return (21<Box22component="header"23sx={{24px: 2,25backgroundColor:26theme.palette.mode === "light"27? theme.palette.primary.light28: theme.palette.primary.dark,29color: (theme) => theme.palette.primary.contrastText,30}}31onClick={handleClick}32>33<Container maxWidth="sm">34<Typography variant="h1">35{menuPrinted ? `${title}... and rarely do we hate it!` : title}36</Typography>37</Container>38</Box>39);40};4142export default Header;
Il y a différents moyens d'obtenir le thème, mais nous trouvons que le hook useTheme
est le plus simple. Ici, le thème par défaut de MUI
sera utilisé pour la couleur primary
(une sorte de bleu).
Nous allons maintenant styler le composant Main
afin qu'il prenne tout l'espace disponible (flex:"1"
) pour assurer que le Footer
soit toujours tout en bas de la page :
tsx1import { useState } from "react";2import sound from "../../assets/sounds/Infecticide-11-Pizza-Spinoza.mp3";3import DrinkMenu from "./DrinkMenu";4// import "./Main.css";5import PizzaMenu from "./PizzaMenu";6import { NewPizza, Pizza, Drink} from "../../types";7import AddPizza from "./AddPizza";8import AudioPlayer from "./AudioPlayer";9import { Container, Typography } from "@mui/material";1011const defaultPizzas: Pizza[] = [12{13id: 1,14title: "4 fromages",15content: "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates",16},17{18id: 2,19title: "Vegan",20content: "Tomates, Courgettes, Oignons, Aubergines, Poivrons",21},22{23id: 3,24title: "Vegetarian",25content: "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives",26},27{28id: 4,29title: "Alpage",30content: "Gruyère, Mozarella, Lardons, Tomates",31},32{33id: 5,34title: "Diable",35content: "Tomates, Mozarella, Chorizo piquant, Jalapenos",36},37];3839const drinks: Drink[] = [40{41title: "Coca-Cola",42image:43"https://media.istockphoto.com/id/1289738725/fr/photo/bouteille-en-plastique-de-coke-avec-la-conception-et-le-chapeau-rouges-d%C3%A9tiquette.jpg?s=1024x1024&w=is&k=20&c=HBWfROrGDTIgD6fuvTlUq6SrwWqIC35-gceDSJ8TTP8=",44volume: "Volume: 33cl",45price: "2,50 €",46},47{48title: "Pepsi",49image:50"https://media.istockphoto.com/id/185268840/fr/photo/bouteille-de-cola-sur-un-fond-blanc.jpg?s=1024x1024&w=is&k=20&c=xdsxwb4bLjzuQbkT_XvVLyBZyW36GD97T1PCW0MZ4vg=",51volume: "Volume: 33cl",52price: "2,50 €",53},54{55title: "Eau Minérale",56image:57"https://media.istockphoto.com/id/1397515626/fr/photo/verre-deau-gazeuse-%C3%A0-boire-isol%C3%A9.jpg?s=1024x1024&w=is&k=20&c=iEjq6OL86Li4eDG5YGO59d1O3Ga1iMVc_Kj5oeIfAqk=",58volume: "Volume: 50cl",59price: "1,50 €",60},61];6263interface MainProps {64actionToBePerformed: boolean;65clearActionToBePerformed: () => void;66}6768const Main = ({ actionToBePerformed, clearActionToBePerformed }: MainProps) => {69const [pizzas, setPizzas] = useState(defaultPizzas);7071const addPizza = (newPizza: NewPizza) => {72const pizzaAdded = { ...newPizza, id: nextPizzaId(pizzas) };73setPizzas([...pizzas, pizzaAdded]);74};7576return (77<Container component="main" sx={{ mt: 8, mb: 2, flex: "1" }} maxWidth="sm">78<Typography variant="h2" component="h1" gutterBottom>79My HomePage80</Typography>81<Typography variant="h5" component="h2" gutterBottom>82Because we love JS, you can also click on the header to stop / start the83music ; )84</Typography>85<AudioPlayer86sound={sound}87actionToBePerformed={actionToBePerformed}88clearActionToBePerformed={clearActionToBePerformed}89/>9091<PizzaMenu pizzas={pizzas} />9293<AddPizza addPizza={addPizza} />9495<DrinkMenu title="Notre Menu de Boissons" drinks={drinks} />96</Container>97);98};99100const nextPizzaId = (pizzas: Pizza[]) => {101return pizzas.reduce((maxId, pizza) => Math.max(maxId, pizza.id), 0) + 1;102};103104export default Main;
Nous allons maintenant mettre à jour le PizzaMenu
afin d'ajouter des couleurs de la palette du thème par défaut à la table HTML :
tsx1import {2Table,3TableBody,4TableCell,5TableContainer,6TableHead,7TableRow,8Paper,9useTheme,10} from "@mui/material";1112import { Pizza } from "../../types";1314interface PizzaMenuProps {15pizzas: Pizza[];16}17const PizzaMenu = ({ pizzas }: PizzaMenuProps) => {18const theme = useTheme();19return (20<TableContainer component={Paper}>21<Table22sx={{23minWidth: 500,24"& .MuiTableCell-head": {25backgroundColor: theme.palette.primary.dark,26color: theme.palette.primary.contrastText,27fontWeight: "bold",28},29"& .MuiTableCell-body": {30backgroundColor: theme.palette.primary.light,31color: "white",32},33"& .MuiTableCell-root": {34border: `1px solid ${theme.palette.secondary.main} `,35},36}}37>38<TableHead>39<TableRow>40<TableCell>Pizza</TableCell>41<TableCell>Description</TableCell>42</TableRow>43</TableHead>44<TableBody>45{pizzas.map((pizza) => (46<TableRow key={pizza.id}>47<TableCell>{pizza.title}</TableCell>48<TableCell>{pizza.content}</TableCell>49</TableRow>50))}51</TableBody>52</Table>53</TableContainer>54);55};5657export default PizzaMenu;
Nous utilisons ici la notion de &
qui vient du monde CSS / SASS permettant de cibler des classes ou des pseudos-classes imbriquées à l'intérieur d'un sélecteur parent spécifié. Cela facilite la création de règles CSS spécifiques à des contextes particuliers sans avoir à répéter le sélecteur parent complet.
Si vous souhaitez en savoir plus sur cette pratique : https://mui.com/material-ui/customization/how-to-customize/#overriding-nested-component-styles
Voici comment nous stylons le composant DrinkMenu
:
tsx1import {2Container,3Card,4CardMedia,5CardContent,6Typography,7Grid,8useTheme,9} from "@mui/material";10import { Drink } from "../../types";1112interface DrinkMenuProps {13title: string;14drinks: Drink[];15}1617const DrinkMenu: React.FC<DrinkMenuProps> = ({ title, drinks }) => {18const theme = useTheme();1920return (21<Container>22<Typography23variant="h4"24gutterBottom25sx={{26color: theme.palette.primary.contrastText,27textAlign: "center",28marginTop: 2,29}}30>31{title}32</Typography>33<Grid container spacing={3}>34{drinks.map((drink, index) => (35<Grid item xs={12} sm={6} key={index}>36<Card>37<CardMedia38component="img"39image={drink.image}40alt={drink.title}41style={{ objectFit: "contain", height: "200px" }} // Ensure image is fully visible42/>43<CardContent>44<Typography gutterBottom variant="h5" component="div">45{drink.title}46</Typography>47<Typography variant="body2" color="textSecondary" component="p">48{drink.volume}49</Typography>50<Typography variant="body2" color="textSecondary" component="p">51Prix: {drink.price}52</Typography>53</CardContent>54</Card>55</Grid>56))}57</Grid>58</Container>59);60};6162export default DrinkMenu;
Voici comment mettre à jour le style du formulaire au sein de AddPizza
:
tsx1import { useState, SyntheticEvent } from "react";23import { NewPizza } from "../../types";4import { Box, Button, TextField, useTheme } from "@mui/material";56interface AddPizzaProps {7addPizza: (pizza: NewPizza) => void;8}910const AddPizza = ({ addPizza }: AddPizzaProps) => {11const theme = useTheme();12const [pizza, setPizza] = useState("");13const [description, setDescription] = useState("");1415const handleSubmit = (e: SyntheticEvent) => {16e.preventDefault();17addPizza({ title: pizza, content: description });18};1920const handlePizzaChange = (e: SyntheticEvent) => {21const pizzaInput = e.target as HTMLInputElement;22console.log("change in pizzaInput:", pizzaInput.value);23setPizza(pizzaInput.value);24};2526const handleDescriptionChange = (e: SyntheticEvent) => {27const descriptionInput = e.target as HTMLInputElement;28console.log("change in descriptionInput:", descriptionInput.value);29setDescription(descriptionInput.value);30};3132return (33<Box34sx={{35marginTop: 2,36padding: 3,37backgroundColor: "secondary.light",38borderRadius: 4,39boxShadow: 2,40}}41>42<form onSubmit={handleSubmit}>43<Box sx={{ marginBottom: 2 }}>44<TextField45fullWidth46id="pizza"47name="pizza"48label="Pizza"49variant="outlined"50value={pizza}51onChange={handlePizzaChange}52required53color="primary"54sx={{55input: { color: theme.palette.secondary.contrastText },56}}57/>58</Box>59<Box sx={{ marginBottom: 2 }}>60<TextField61fullWidth62id="description"63name="description"64label="Description"65variant="outlined"66value={description}67onChange={handleDescriptionChange}68required69color="primary"70sx={{71input: { color: theme.palette.secondary.contrastText },72}}73/>74</Box>75<Button type="submit" variant="contained" color="primary">76Ajouter77</Button>78</form>79</Box>80);81};8283export default AddPizza;
Nous pouvons maintenant mettre à jour le Footer
:
tsx1import { Box, Container, Typography, useTheme } from "@mui/material";2import logo from "../../assets/images/js-logo.png";3import { Copyright } from "@mui/icons-material";45const Footer = () => {6const theme = useTheme();78return (9<Box10component="footer"11sx={{12py: 3,13backgroundColor:14theme.palette.mode === "light"15? theme.palette.secondary.light16: theme.palette.secondary.dark,17}}18>19<Container maxWidth="sm">20<Box21sx={{22display: "inline-block",23paddingRight: 2,24color: theme.palette.secondary.contrastText,25}}26>27<Typography variant="body1">But we also love JS</Typography>28<Typography>29<Copyright />30myAmazingPizzeria31</Typography>32</Box>33<Box sx={{ display: "inline-block" }}>34<img src={logo} alt="" width={50} />35</Box>36</Container>37</Box>38);39};4041export default Footer;
Nous avons maintenant une application qui commence à être bien stylée !
Il y a un souci qui est visible sur toutes les applications offertes par les templates de projet de MUI
. Il y a toujours un espace, une sorte de marge en bas de page, après notre Footer
.
Pour résoudre ce souci, qui ne semble malheureusement pas documenté sur le Web, il vous est proposé d'ajouter une seule feuille de style à votre application, au niveau de /src/main.tsx
, veuillez importer /src/index.css
contenant ce code :
cssdiv#root {width: 100%;display: inline-block; /* avoid margins to collapse to avoid vertical scrollbar */}
Wow, nous avons quelque chose de fonctionnel !
Création de son propre thème
Pour ce tutoriel, nous vous proposons de créer la palette de couleurs la plus simple pour donner les couleurs primaires et secondaires que nous souhaitons pour un site d'une pizzeria.
Il existe des outils très intéressants pour créer ses thèmes et palettes de couleurs. Vous trouvez ceux-ci ici :
- Material palette generator
- mui-theme-creator
- Autres outils pour générer ou découvrir des palettes : https://mui.com/material-ui/customization/color/
Veuillez créer un fichier pour y ajouter la définition d'un nouveau thème dans /src/themes.ts
:
tsimport { createTheme } from "@mui/material/styles";const theme = createTheme({palette: {primary: {main: "#f0483b",},secondary: {main: "#3bf048",},},});export default theme;
Et pour utiliser ce nouveau thème, nous devons créer un provider qui va "injecter" ce thème dans l'arbre de tous les composants React. Pour ce faire, veuillez mettre à jour /src/main.tsx
:
tsx1import React from "react";2import ReactDOM from "react-dom/client";3import "@fontsource/roboto/700.css";4import CssBaseline from "@mui/material/CssBaseline";5import { ThemeProvider } from "@mui/material/styles";6import theme from "./themes";78import App from "./components/App";9import "./index.css";1011ReactDOM.createRoot(document.getElementById("root")!).render(12<React.StrictMode>13<ThemeProvider theme={theme}>14<CssBaseline /> {/* Global CSS reset from Material-UI */}15<App />16</ThemeProvider>17</React.StrictMode>18);
Si nécessaire, vous pouvez trouver le code associé à ce tutoriel ici : ui-library.
🍬 Exercice 2.9 : Utilisation de composants MUI
Veuillez créer un nouveau projet sur base d'un copier/coller de l'exercice nommé /exercises/2.7
(gestion de films) dans votre git repo.
Pour une première étape, veuillez remplacer tous les composants TSX que vous pouvez par des composants MUI
.
Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex2.9+
Pour la dernière étape, veuillez créer votre palette de couleur, et styler votre application à l'aide du MUI System
.
Une fois votre application peaufinée, veuillez faire un commit avec le message suivant : new:ex2.9++