svg-layout-designer-react/docs/Tutorial/Pages/PremierComposantReact.md
2022-11-10 15:38:37 +01:00

346 lines
No EOL
9.1 KiB
Markdown

# Notre premier composant React
Si vous êtes arrivé ici, c'est que vous avez compris ce qu'est un composant React et qu'il est composant de `props` et d'un `state`.
Mettons donc en pratique ce que vous avez vu.
Voici le cahier de charge fonctionnel du composant à implémenter :
Fonction:
- On veut ajouter un bouton `Home` dans l'éditeur pour revenir au menu principal
Contraintes principales :
- Le bouton `home` ne doit pas rafraîchir la page
Contraites secondaires :
- Le bouton est positionné en bas sur la barre
- Le bouton change de couleur à chaque fois que le curseur entre dedans
- Le bouton possède un icône home
## Initialiser un composant
Commençont par créer le composant sans le lier forcément au reste du projet.
Par convention comme il est dit dans la section [Pour commencer](PourCommencer.md), créons le fichier `Home.tsx` dans un dossier `Home/` dans `src/Components`. Il est recommandé de toujours créer un dossier pour le composant pour plus tard si on veut ajouter des styles propres au composant ou si on veut faire des tests.
Maintenant créons le squelette du composant. Pour rappel, on utilise la syntaxe *fonctionnelle* (i.e. une fonction = un composant).
```tsx
// Home.tsx
export function Home(): JSX.Element {
return <></>;
}
```
Note: vous pouvez utiliser le snippet `tsrfc` si vous utilisez l'extension de VSCode.
La convention de nommage indique qu'il faut prioriser les fonctions nommées (donc pas de flèches fonctions).
Mettons le bouton html et mettons du texte dedans :
```tsx
export function Home(): JSX.Element {
return (
<button>
Home
</button>
);
}
```
## Appeler une fonction
Pour que le bouton appelle une fonction nous allons utiliser la propriété `onClick`. Pour lui équiper d'une variable/fonction, on la met entre accolades `prop={var}`.
```tsx
function GoHome(): void {
location.reload();
}
export function Home(): JSX.Element {
return (
<button
onClick={GoHome}
>
Home
</button>
);
}
```
Cela devrait être suffisant pour revenir au menu principal. Appelons-le dans l'editeur.
## Ajouter le composant dans l'editeur
Tout ce qui est UI doit se mettre dans `UI.tsx`.
On sait que l'on doit l'ajouter dans la **barre** à gauche, soit `Bar.tsx`
Commençons par importer le composant dans `Bar.tsx`:
```tsx
// Bar.tsx
import { Home } from '../Home/Home';
...
```
Puis ajoutons-le dans la vue :
```tsx
// Bar.tsx
export function Bar(props: IBarProps): JSX.Element {
return (
<div className='bar'>
...
<Home />
</div>
);
}
```
Testez avec `pnpm dev`. Et ouvrez la page sur `http://localhost:5173`.
Et voilà ! Vous avez fini !
...
Sauf que vous n'avez respecté aucune des contraintes.
Faisons mieux. Utilisons les `props` et le `state` de l'application.
## Props et state
Le composant doit restaurer l'état du menu principal.
Dans le menu principal, lorsque l'on clique sur `Start from scratch`, le composant doit forcément changer un état pour cacher le menu.
Ce que l'on veut faire est donc de renverser cette opération, autrement dit changer l'état qui permet de cacher le menu pour le remontrer.
Allons donc dans le composant `MainMenu.tsx` et analysons le contenu du bouton `Start from scratch`.
```tsx
// MainMenu.tsx
...
default:
return (
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<button type="button" className='mainmenu-btn' onClick={() => {
props.newEditor();
}}>Start from scratch</button>
<button type="button" className='mainmenu-btn' onClick={() => setWindowState(WindowState.Load)}>Load a configuration file</button>
</div>
);
```
On trouve la fonction `onClick` suivante :
```tsx
// MainMenu.tsx
<button type="button" className='mainmenu-btn' onClick={() => {
props.newEditor();
}}>
```
On trouve la
`newEditor` étant une fonction donnée par `props`, doit donc correspondre à la fonctionalité que l'on veut renverser.
Regardons donc le composant parent de `MainMenu`: `App`.
```tsx
// App.tsx
...
return (
<div
ref={appRef}
className='App mainmenu-bg'
>
<MainMenu
newEditor={() => {
setAppState(AppState.Loading);
NewEditor(
editorState,
(newEditor) => setEditorState(newEditor),
() => setAppState(AppState.Loaded)
);
}}
loadEditor={(files: FileList | null) => LoadEditor(
files,
setEditorState,
setAppState
)}
/>
</div>
);
...
```
Cette fonction `newEditor` appelle donc une autre fonction `NewEditor`.
```tsx
export function NewEditor(
editorState: IEditorState,
setEditorState: (newState: IEditorState) => void,
enableLoaded: () => void
): void {
if (DISABLE_API) {
enableLoaded();
}
if (editorState.configuration.APIConfiguration !== undefined) {
enableLoaded();
return;
}
// Fetch the configuration from the API
FetchConfiguration()
.then((configuration: IConfiguration) => {
// Set the editor from the given properties of the API
const editorState: IEditorState = GetDefaultEditorState(configuration);
setEditorState(editorState);
enableLoaded();
}, (error) => {
console.debug('[NewEditor] Could not fetch resource from API. Using default.', error);
enableLoaded();
});
}
```
Cette fonction à l'air assez compliquée, mais on sait qu'elle utilise `setEditorState` et `enableLoaded()`. Et c'est tout ce qui nous intéresse. On observe dans `App.tsx` que `setEditorState` sert à changer l'état de `editorState` et que `enableLoaded` est appelle `setAppState` qui lui sert à changer l'état de `appState`.
En analysant un peu plus près `App.tsx`, on remarque l'évidante condition permettant d'afficher ou non le menu principal :
```tsx
// App.tsx
switch (appState) {
case AppState.Loaded:
return (
<div
ref={appRef}
className='App'
>
<Editor
root={props.root}
configuration={editorState.configuration}
history={editorState.history}
historyCurrentStep={editorState.historyCurrentStep}
/>
</div>
);
case AppState.Loading:
return (
<div
ref={appRef}
className='App mainmenu-bg'
>
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<Loader />
</div>
</div>
);
default:
return (
<div
ref={appRef}
className='App mainmenu-bg'
>
<MainMenu
newEditor={() => {
setAppState(AppState.Loading);
NewEditor(
editorState,
(newEditor) => setEditorState(newEditor),
() => setAppState(AppState.Loaded)
);
}}
loadEditor={(files: FileList | null) => LoadEditor(
files,
setEditorState,
setAppState
)}
/>
</div>
);
}
```
Ainsi, on sait qu'il faut appeler `setAppState` le setteur d'état de `appState`:
```tsx
// App.tsx
const [appState, setAppState] = useState<AppState>(FAST_BOOT);
```
Cependant comment appeler cette fonction depuis `Home` ? Et bien nous allons utiliser les `props` pour passer la fonction en dessous.
Comme pour `MainMenu`, dans `Home.tsx` nous allons mettre la fonction `GoHome` dans les props :
```tsx
import React from 'react';
interface IHomeProps {
goHome: () => void
}
export function Home({ goHome }: IHomeProps): JSX.Element {
return (
<button
onClick={goHome}
>
Home
</button>
);
}
```
Note: vous pouvez utiliser `Home(props: IHomeProps)` et appeler `props.goHome` comme tout le reste du projet au lieu de le destructurer en `{ goHome }`, mais le destructuring d'objet est autorisé (et dans certains cas, c'est mieux avec).
Home est utilisé dans les composants Bar, UI, et Editor avant d'être appelé dans App. Il faut faire la même chose, ajouter la fonction dans les props et l'appeler dans le composant :
```tsx
// Bar.tsx
interface IBarProps {
goHome: () => void
}
...
export function Bar(props: IBarProps): JSX.Element {
return (
<div className='bar'>
...
<Home
goHome={props.goHome}
/>
</div>
);
}
```
*Faites la même chose pour UI et Editor.*
Enfin dans `App.tsx`, créez la fonction permettant de changer `appState` vers `AppState.MainMenu`:
```tsx
...
case AppState.Loaded:
return (
<div
ref={appRef}
className='App'
>
<Editor
root={props.root}
configuration={editorState.configuration}
history={editorState.history}
historyCurrentStep={editorState.historyCurrentStep}
goHome={() => setAppState(AppState.MainMenu)}
/>
</div>
);
...
```
Félicitation vous avez correctement développé votre premier composant Home !
Essayons maintenant de faire le reste dans le prochain chapitre avec [`TailwindCSS`](Tailwind.md)