18 Δεκ, 2022

Αναπτυξη ενος React-Router Dialog

React router dialog

Τί είναι ένα Dialog;

Σύμφωνα με το Material Design, τα Dialogs (γνωστά επίσης ως Modals), μπορούν να απαιτούν μια πράξη, να παραθέτουν πληροφορία ή να βοηθούν το χρήστη να ολοκληρώσει ένα task.

Τα Dialogs μας επιτρέπουν, να επικεντρώνουμε την προσοχή μας σε ένα περιεχόμενο δίχως να χάνουμε την background πληροφορία που συσχετίζεται μαζί του.

Παράδειγμα του Instagram

Σκεφτείτε ότι βρίσκεστε στο Instagram σκρολάροντας σε μια λίστα από posts και κλικάρετε σε ένα post για να δείτε περισσότερες πληροφορίες για αυτό. Παραδοσιακά, αυτό θα μπορούσε να υλοποιηθεί απλώς με ένα navigation σε μια άλλη σελίδα, αλλά αυτό συνήθως δεν είναι πολύ user-friendly.

Αυτό που μπορεί να γίνει αντ'αυτού, είναι να δείξουμε ένα Dialog με τις λεπτομέρειες του post, χωρίς να φύγουμε από τη σελίδα διατηρώντας τη λίστα των posts στο background. Έτσι, μετά ο χρήστης μπορεί απλώς να κλείσει το post και να συνεχίσει με τη λίστα, κλικάροντας σε ένα άλλο post κλπ, κλπ.

Δείτε τι κάνει το Instagram με τα posts του και πόσο user-friendly φαίνεται:

instagram-dialog-example

Μια Αφελής Προσπάθεια

Θα μπορούσαμε να θεωρήσουμε ότι μπορούμε να υλοποιήσουμε την παραπάνω λειτουργικότητα με το να γράψουμε κάτι σαν και αυτό:

const App = () => {
  const [selectedPost, setSelectedPost] = useState(null);

  return <div>
    <ListOfPosts>
      {posts.map(post => <Post post={post} onClick={() => setSelectedPost(post)}/>)}
    </ListOfPosts>
    <PostDetailsDialog selectedPost={selectedPost}>
  <div/>
}

Φαίνεται αρκετά απλό έτσι;

Ή... μήπως όχι;

Ο κώδικας παραπάνω σίγουρα θα μπορούσε γενικά να λειτουργήσει, όμως μας ξεφεύγει ένα πολύ σημαντικό πράγμα.

Τι θα γίνει με το page url; Με μια υλοποίηση όπως την παραπάνω, το url της σελίδας δεν αλλάζει κάθε φορά που ένα Post εμφανίζεται.

Και από μια UI/UX προοπτική, είναι σίγουρα για το καλύτερο εάν το page-url ακολουθεί το user-journey της εφαρμογής μας.

Επιπλέον, αλλάζοντας το page url όταν το PostDetailsDialog εμφανίζεται και κρατώντας ένα unique url για κάθε post, επιτρέπουμε στον χρήστη να κάνει εύκολα share ένα post απλώς μοιράζοντας το σύνδεσμο που βρίσκεται εκείνη τη στιγμή ο browser!

Τώρα, κοιτάξτε πώς το Instagram συμπεριφέρεται, όταν κάνουμε share το URL ενός post: https://www.instagram.com/p/CfeYvQOD1z-/

To post εμφανίζεται σε μια νέα σελίδα χωρίς να βρίσκεται μέσα σε Dialog και χωρίς να δείχνει τη λίστα των posts στο background. Απλώς επικεντρώνεται σε αυτό το post.

Απαιτήσεις

Αυτές είναι οι απαιτήσεις που πρέπει να τηρηθούν ώστε να επιτύχουμε την προαναφερθείσα λειτουργικότητα:

  1. Εμφάνισε ένα Dialog για κάθε post που κλικάρεται
  2. Το page url πρέπει να αλλάζει και πρέπει να είναι unique για κάθε post
  3. Όταν ο χρήστης επισκέπτεται ένα Post από ένα shared URL, το post πρέπει να εμφανίζεται κανονικά μέσα σε μια νέα σελίδα (χωρίς να βρίσκεται μέσα σε Dialog)

Μπορούμε να χρησιμοποιήσουμε το React Router με ένα Dialog component ώστε να ικανοποιήσουμε τις απαιτήσεις.

Κώδικας & Υλοποίηση

Μπορείτε να βρείτε τον πλήρη κώδικα εδώ: https://codesandbox.io/s/react-router-modal-gallery-qs44n1

Τι θα επιτύχουμε (παρατηρήστε πώς αλλάζει το URL)

peek1


Και όταν επισκεπτόμαστε την εικόνα από ένα shared URL το post ανοίγει σε νέα σελίδα αυτούσιο:

peek2

Ώρα για κώδικα!

Το κλειδί εδώ είναι ότι θα χρησιμοποιήσουμε το location prop του Routes component που παρέχεται από το react-router-dom.

Βασικά, χρησιμοποιώντας το location prop μπορούμε να "παρακάμψουμε" το πραγματικό location που βρίσκεται η εφαρμογή μας εκείνη τη στιγμή.

Το backgroundLocation (μπορείτε να ονομάσετε την μεταβλητή όπως επιθυμείτε) έρχεται από το location state της εφαρμογής μας.

Έπειτα τσεκάρουμε εάν το backgroundLocation έχει οριστεί και εάν ναι, σημαίνει ότι πρέπει να δείξουμε ένα Dialog και να διατηρήσουμε το background location, οπότε εφαρμόζουμε το location prop στο Routes component.

Ειδάλλως, εάν το backgroundLocation δεν έχει οριστεί, τότε το location prop απλώς παίρνει την default τιμή του location και η εφαρμογή μας ακολουθεί το "normal" flow που σημαίνει ότι το Post μας θα γίνει render όπως είναι σε μια σελίδα και όχι σε ένα Dialog.

// App.js
import { Box, Container, Divider, Typography } from "@mui/material";
import { Outlet, Route, Routes, useLocation } from "react-router-dom";
import About from "./About";
import ImageList from "./ImageList";
import ImageView from "./ImageView";
import ImageModal from "./ImageModal";
import Home from "./Home";

const Layout = () => (
  <Container maxWidth="md">
    <Box mb={2}>
      <Typography variant="h4">Application Demo</Typography>
      <Divider />
    </Box>
    <Outlet />
  </Container>
);

const App = () => {
  const location = useLocation();
  const { state } = location;

  return (
    <div>
      <Routes location={state?.backgroundLocation || location}>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/images" element={<ImageList />} />
          <Route path="/images/:id" element={<ImageView />} />
        </Route>
      </Routes>

      {/* Show the modal when a `backgroundLocation` is set */}
      {state?.backgroundLocation && (
        <Routes>
          <Route path="/images/:id" element={<ImageModal />} />
        </Routes>
      )}
    </div>
  );
};

export default App;

Δείτε πώς μπορούμε να ορίσουμε το state μαζί με το url σε ένα Link component, όταν ένας χρήστης κλικάρει σε μια εικόνα.

Είναι σαν να λέμε στην εφαρμογή μας: "Ο χρήστης κλίκαρε σε μια εικόνα, οπότε πρέπει να δείξουμε την εικόνα μέσα σε ένα Dialog διατηρώντας το background location".

// ImageList.js
import { Box, Grid, Stack, Typography } from "@mui/material";
import React, { useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { IMAGES } from "./images";

const ImageBox = ({ image, location }) => {
  const [hovered, setHovered] = useState(false);
  const { id, src } = image;

  return (
    <Box width="100%" position="relative">
      <Link to={`/images/${id}`} state={{ backgroundLocation: location }}>
        <Box
          onMouseEnter={() => setHovered(true)}
          component="img"
          src={src}
          alt="post"
          width="100%"
          height="250px"
        />
        {hovered && (
          <Box
            onMouseLeave={() => setHovered(false)}
            position="absolute"
            top={0}
            left={0}
            bottom={0}
            right={0}
            width="100%"
            height="250px"
            bgcolor="rgba(0, 0, 0, 0.3)"
            // sx={{ opacity: 0.6 }}
          />
        )}
      </Link>
    </Box>
  );
};

const ImageList = () => {
  let location = useLocation();

  return (
    <Grid container spacing={2} alignItems="center" justifyContent="center">
      {IMAGES.map(({ title, src, id }, index) => (
        <Grid key={id} item sm={4}>
          <ImageBox image={{ title, src, id }} location={location} />
        </Grid>
      ))}
    </Grid>
  );
};

export default ImageList;
// ImageModal.js
import { useNavigate, useParams } from "react-router-dom";
import ImageView from "./ImageView";
import { getImage } from "./images";
import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";

const ImageModal = () => {
  let navigate = useNavigate();
  let { id } = useParams();
  let image = getImage(id);

  if (!image) return null;

  const handleClose = () => {
    navigate(-1);
  };

  return (
    <Dialog open onClose={handleClose}>
      <DialogContent>
        <ImageView />
      </DialogContent>
      <DialogActions>
        <Button onClick={handleClose}>Close</Button>
      </DialogActions>
    </Dialog>
  );
};

export default ImageModal;
// ImageView.js
import { ArrowBackIos } from "@mui/icons-material";
import { Button, Link, Stack, Typography, Box } from "@mui/material";
import React from "react";
import { useLocation, useParams, Link as RouterLink } from "react-router-dom";
import { getImage } from "./images";

const ImageView = () => {
  const { state } = useLocation();
  const { id } = useParams();
  const image = getImage(id);
  const { src, title } = image;

  const isInsideModal = state?.backgroundLocation;

  return (
    <div>
      {!isInsideModal && (
        <Link component={RouterLink} to="/images" underline="none">
          <Button startIcon={<ArrowBackIos />} variant="text">
            Back to Images
          </Button>
        </Link>
      )}
      <Box display="flex" justifyContent="center">
        <Stack direction="column" spacing={1}>
          <Typography variant="h5" fontWeight={600}>
            {title}
          </Typography>

          <Box component="img" sx={{ width: "400px" }} src={src} alt="dog" />
        </Stack>
      </Box>
    </div>
  );
};

export default ImageView;

Τέλος

Ελπίζω να βρήκατε το άρθρο χρήσιμο.

Τα λέμε σύντομα! 🙂

Loading Comments...