Portali in React | Aulab

GUIDE PER ASPIRANTI PROGRAMMATORI

Portali in React

La nostra applicazione di esempio sa creare e leggere gli elementi di cose da fare. Delle quattro operazioni CRUD (Create, Read, Update, Delete), sappiamo fare le prime due. È ora di aggiungere la terza! Per quanto riguarda l’interfaccia grafica, dato che abbiamo l’intera applicazione in un’unica pagina, implementeremo il form di modifica in una modale.…

Lezione 34 / 41
Enza Neri
Immagine di copertina

Vuoi avviare una nuova carriera o fare un upgrade?

Trova il corso Digital & Tech più adatto a te nel nostro catalogo!

La nostra applicazione di esempio sa creare e leggere gli elementi di cose da fare. Delle quattro operazioni CRUD (Create, Read, Update, Delete), sappiamo fare le prime due. È ora di aggiungere la terza!

Per quanto riguarda l’interfaccia grafica, dato che abbiamo l’intera applicazione in un’unica pagina, implementeremo il form di modifica in una modale. Questo ci permetterà di scoprire un’altra funzione di React: createPortal.

Prima di continuare, però, ci serve un modo univoco per distinguere un elemento della lista dall’altro, che non sia la sua posizione nella lista (l’indice, che abbiamo usato per l’attributo key in precedenza). Non avendo a disposizione un server, scriveremo una piccola funzione che genera un id progressivo numerico. In una situazione realistica, non dovremmo preoccuparci di questa cosa, ci penserebbe il server.

// File: src/App.jsx

// ...

const initialData = [
  {
    id: 1,
    description: "Something to be done",
    isDone: false,
  },
  {
    id: 2,
    description: "Something already done",
    isDone: true,
  },
];

export default function App() {
  // ...

  const onTodoItemFormSubmit = (todoItem) => {
    setTodoList((todoList) => {
      const allIds = todoList.map((item) => {
        return item.id;
      });

      const maxId = Math.max(0, ...allIds);
      const newId = maxId + 1;
      const itemWithId = { ...todoItem, id: newId };

      return [itemWithId, ...todoList];
    });
  };

  // return ...
}

Abbiamo aggiunto gli id 1 e 2 ai nostri dati iniziali, initialData. Quando creiamo un nuovo elemento, estraiamo prima tutti gli id dalla lista, troviamo il massimo, aggiungiamo uno e otteniamo l’id da assegnare al nuovo elemento.

Un’altra cosa che dobbiamo fare è rendere il nostro form TodoItemForm in grado di partire da dati già esistenti, mentre adesso parte sempre con tutti (due) i campi vuoti. Per farlo, aggiungiamo una prop opzionale che accetta, se esiste, lo stato corrente dell’elemento da rappresentare. Dopodiché, interveniamo sui valori iniziali passati a useForm, cioè le proprietà initialValue dei vari campi dell’oggetto fields, più la nuova proprietà id, che, in caso di modifica, deve essere inviata insieme al resto quando scatta l’evento “Submit”:

// File: src/components/TodoItemForm.jsx

// ...

export default function TodoItemForm({ initialData, onSubmit }) {
  // ...

  const {
    inputProps,
    onSubmit: onFormSubmit,
    reset,
  } = useForm({
    fields: {
      id: {
        initialValue: initialData?.id,
      },
      description: {
        initialValue: initialData?.description ?? "",
        validator: nonEmptyStringValidator("Description cannot be empty!"),
      },
      isDone: {
        initialValue: initialData?.isDone ?? false,
      },
    },
    submit: (data) => {
      createTodoItem(data).then((response) => {
        onSubmit(response);
        reset();
      });
    },
  });

  // ...
}

Ora TodoItemForm è pronto per modificare, oltre che creare elementi. I prossimi componenti che dobbiamo preparare sono TodoItem e TodoList.

Non vogliamo inserire il form di modifica in TodoItem, perché non vogliamo avere un elemento <form /> in pagina per ogni elemento della lista. Basterà un unico form per tutta la lista, che popoleremo con i dati dell’elemento che stiamo modificando volta per volta.

Il form andrà dentro TodoList, ma ci servirà un bottone “Edit” (modifica) per ogni elemento, così da poter impostare una “modalità di modifica” su un singolo elemento facendo click sul bottone “Edit” corrispondente.

Il modo in cui viene gestito il click dei bottoni “Edit” è l’ormai classico sollevamento di stato: al click del bottone “Edit”, TodoItem notificherà TodoList tramite un evento.

// File: src/components/TodoItem.jsx

import "./TodoItem.css";

export default function TodoItem({ todoItem, onEditButtonClick }) {
  return (
    <div className="TodoItem">
      <input type="checkbox" checked={todoItem.isDone} disabled />
      <span>{todoItem.description}</span>
      <button onClick={onEditButtonClick}>Edit</button>
    </div>
  );
}

Nota: potrà capitarti di vedere chiamate a funzioni come onEditButtonClick – che rappresentano la gestione di eventi – passare dati che il componente controller ha sicuramente già a disposizione. Per esempio, potresti pensare che a onEditButtonClick vada passato l’ID univoco dell’elemento, chiamandola in modo simile a: onEditButtonClick(todoItem.id). Questa cosa non serve, perché todoItem è a sua volta una prop, il che significa che il componente controller, che usa TodoItem, deve avere a disposizione l’intero oggetto todoItem per poter usare il componente TodoItem. Di conseguenza, anche l’ID dell’elemento, todoItem.id, sarà disponibile al componente controller. Insomma, quando progetti la comunicazione tra componenti controller e componenti view, tieni sempre a mente dove sono i dati che verranno passati.

La funzione createPortal in React

createPortal ci serve quando vogliamo far uscire un componente dalla struttura ad albero che React crea, portandolo fuori dal flusso e inserendolo da un’altra parte nel DOM (la struttura di elementi HTML).

Il concetto è molto simile a quello di position: absolute; o position: fixed; in linguaggio CSS. E guarda caso, la position del nostro componente modale sarà proprio fixed!

// File: src/components/Modal.jsx

import "./Modal.css";
import { createPortal } from "react-dom";

export default function Modal({ isOpen, onCancel, children }) {
  const modalClassName = ["Modal", ...(isOpen ? ["visible"] : [])].join(" ");

  return createPortal(
    <div className={modalClassName}>
      <div className="content">
        <button className="close-button" onClick={onCancel}>
          &times;
        </button>
        {children}
      </div>
    </div>,
    document.body
  );
}

Una piccola nota su una sintassi che potrebbe non esserti familiare:

const modalClassName = ["Modal", ...(isOpen ? ["visible"] : [])].join(" ");

Ci sono tanti modi di comporre le liste di classi in React, questo è senza dubbio uno tra i più validi. Quello che vogliamo in questo caso è che “Modal” sia una classe sempre associata al nostro componente, mentre “visible” sarà presente solo quando la proprietà isOpen – che è un valore booleano – sarà true.

Usiamo lo spread operator (…) per scomporre un Array di stringhe, che sarà vuoto se isOpen sarà false, mentre conterrà un solo elemento, la stringa “visible“, quando isOpen sarà true. Chiamiamo poi il metodo join per comporre la stringa che contiene le classi separate da spazi.

Con isOpen che vale false, la parte tra parentesi (isOpen ? [“visible”] : []) risolve in un Array vuoto []. Di conseguenza, modalClassName diventa [“Modal”, …[]]. La destrutturazione di un Array vuoto …[] non dà nessun risultato, quindi modalClassName diventa [“Modal”]. A questo punto, join non aggiunge niente alla stringa, quindi modalClassName diventa “Modal”.

Con isOpen che vale true, la parte tra parentesi (isOpen ? [“visible”] : []) risolve in [“visible”]. Di conseguenza, modalClassName diventa [“Modal”, …[“visible”]]. La destrutturazione porta a [“Modal”, “visible”] e join aggiunge uno spazio, quindi modalClassName diventa “Modal visible”.

Questo metodo permette di inserire multiple classi che devono essere sempre presenti, multiple classi opzionali, più classi per ogni condizione e, se mischiata con le IIFE/IIAE, possiamo inserire if-else, switch-case, funzioni di matching e, in generale, un po’ quello che ci pare, per calcolare liste di classi, utilizzando sempre la stessa sintassi. Infine, non ci sono casi in cui questa tecnica aggiunge spazi non necessari, a differenza, per esempio, del classico:

<div className={`Modal ${isOpen ? "visible" : ""}`}>{/* ... */}</div>

Questa forma, quando isOpen è false, scrive className=”Modal “, con un fastidiosissimo spazio inutile alla fine della stringa. La soluzione potrebbe essere:

<div className={`Modal${isOpen ? " visible" : ""}`}>{/* ... */}</div>

Il risultato di questa forma è più consistente, ma il codice è meno elegante e, se dovessimo avere tante classi opzionali, questa forma si trasformerebbe molto presto in un incubo di operatori ternari:

<div
  className={`Modal${isOpen ? " visible" : ""}${
    variant === "success"
      ? " success"
      : variant === "error"
      ? " error"
      : variant === "warning"
      ? " warning"
      : " default"
  }`}
>
  {/* ... */}
</div>

Ora, createPortal:

// File: src/components/Modal.jsx

return createPortal(
  <div className={modalClassName}>
    <div className="content">
      <button className="close-button" onClick={onCancel}>
        &times;
      </button>
      {children}
    </div>
  </div>,
  document.body
);

createPortal accetta due argomenti: 

  • la parte di JSX che vogliamo inserire in pagina, quella che farà parte del rendering di React. 
  • Il secondo è l’elemento HTML in da cui vogliamo partire. 

Per il secondo argomento, usiamo le funzioni native del browser, come document.getElementById o document.querySelector. Nel nostro caso specifico vogliamo l’elemento <body />, che è sempre presente in pagina e accessibile tramite document.body.

Il resto del componente è abbastanza intuitivo: le props sono isOpen, che serve a mostrare o nascondere la modale; onCancel, collegato al click di un bottone per chiudere il modale; children, che contiene tutto il contenuto che verrà inserito nella modale.

/* File: src/components/Modal.css */

.Modal {
  align-items: center;
  background-color: rgba(0, 0, 0, 0.64);
  bottom: 0;
  display: flex;
  justify-content: center;
  left: 0;
  opacity: 0;
  position: fixed;
  right: 0;
  top: 0;
  visibility: hidden;
}

.Modal.visible {
  opacity: 1;
  visibility: visible;
}

.Modal .content {
  position: relative;
  width: 90vw;
  max-width: 45em;
  background-color: #fff;
  box-sizing: border-box;
  padding: 48px calc(48px + 1.5em) 1.5em 1.5em;
  border-radius: 8px;
}

.Modal .close-button {
  width: 48px;
  height: 48px;
  font-size: 32px;
  padding: 0;
  line-height: 32px;
  border: 0px none;
  background-color: transparent;
  position: absolute;
  top: 0;
  right: 0;
  cursor: pointer;
}

Ora possiamo inserire il nostro form in TodoList e chiudere il cerchio:

Nota: TodoItemForm non verrà popolato correttamente con i dati relativi all’elemento di cui clicchiamo il bottone “Edit”. Questa cosa è normale, la sistemeremo nella prossima sezione.

// File: src/components/TodoList.jsx

import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
import Modal from "./Modal";
import TodoItemForm from "./TodoItemForm";

export default function TodoList({ todoList, onItemUpdate }) {
  const [editingItem, setEditingItem] = useState(null);

  const onModalCancel = () => {
    setEditingItem(null);
  };

  const onEditFormSubmit = (data) => {
    onItemUpdate(data);
    setEditingItem(null);
  };

  return (
    <div className="TodoList">
      {todoList.map((todoItem) => {
        return (
          <TodoItem
            key={todoItem.id}
            todoItem={todoItem}
            onEditButtonClick={() => {
              setEditingItem(todoItem);
            }}
          />
        );
      })}
      <Modal isOpen={editingItem !== null} onCancel={onModalCancel}>
        <TodoItemForm initialData={editingItem} onSubmit={onEditFormSubmit} />
      </Modal>
    </div>
  );
}

Usiamo uno stato per memorizzare l’elemento che stiamo modificando, se ce n’è uno, altrimenti lo stato avrà valore null, che è anche il suo valore iniziale. Quando l’utente fa click su un bottone “Edit”, TodoItem chiama la nostra funzione associata a onEditButtonClick, che a sua volta imposta lo stato editingItem, chiamando la sua funzione setter e scatenando un ciclo di rendering.

Lo stato editingItem decide se la modale, rappresentata dal componente Modal, viene visualizzata. Dentro Modal abbiamo TodoItemForm, che invia i dati tramite onSubmit alla funzione onEditFormSubmit, la quale, a sua volta, chiude la modale (impostando editingItem a null, sempre tramite funzione setter e ciclo di rendering) e notifica il componente controller, in questo caso App.

Vediamo, infine, App:

// File: src/App.jsx

// ...

const initialData = [
  // ...
];

export default function App() {
  const [todoListNetworkRequest, retry, setTodoList] = useQueryState({
    path: "/todo",
    initialData,
  });

  const onTodoItemFormSubmit = (todoItem) => {
    // ...
  };

  const onTodoItemUpdate = (updatedItem) => {
    setTodoList((todoList) => {
      return todoList.map((todoItem) => {
        if (todoItem.id === updatedItem.id) {
          return updatedItem;
        } else {
          return todoItem;
        }
      });
    });
  };

  return (
    <div className="App">
      <Query
        // ...
        render={(todoList) => (
          <>
            <TodoList todoList={todoList} onItemUpdate={onTodoItemUpdate} />
            {/* ... */}
          </>
        )}
      />
    </div>
  );
}

Per aggiornare solo l’elemento che è stato modificato, nella funzione onTodoItemUpdate, chiamiamo map sull’Array che rappresenta la lista, come abbiamo già visto nella sezione in cui parlavamo di Array e oggetti negli stati. Utilizziamo l’ID univoco id per distinguere l’elemento modificato dagli altri della lista, visto che tutte le altre proprietà (description e isDone) possono cambiare. Se l’elemento che abbiamo in mano durante l’operazione di map è proprio quello che è stato modificato, allora lo rimpiazziamo con la versione modificata, altrimenti teniamo la versione attuale.

Aprendo l’inspector del browser, possiamo vedere come l’elemento che corrisponde al nostro componente Modal non sia figlio di TodoList, ma figlio dell’elemento <body />, come ci aspettavamo:

Modal

Questo significa che, anche se dovessimo associare position: relative; (o qualsiasi altro valore che non sia static) all’elemento .TodoList o a qualunque genitore dell’elemento, il nostro elemento .Modal rimarrebbe visibile e continuerebbe a coprire il resto della pagina, come è giusto che sia.

Sei indeciso sul percorso? 💭

Parliamone! Scrivici su Whatsapp e risponderemo a tutte le tue domande per capire quale dei nostri corsi è il più adatto alle tue esigenze.

Oppure chiamaci al 800 128 626