1

I want to adapt this image magnifier code for React Typescript as I do not want to use a library for this. The working Vanilla Javascript Codepen is here. I do not want to copy&paste the CSS into a .css file but to use it with my const styles. Or alternatively achieve the same result with a styled component.

Apart from the thing that I currently get no reaction what should I use instead of getElementById as manual DOM manipulation is not the best to do I think?

I use the container to center the element. Then we have a magnifyWrapper, which will act as our hover div, so once we hover this div, the magnifying glass will show a bigger version of the image.

Then we add the image and a ghost div in which we will load the large image.

React Typescript Code

import React from 'react';

const styles = {

    container: {
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "100vh",
      },

      magnifyWrapper: {
        position: "relative",
        maxHeight: "50vh",
        image: {
          maxHeight: "inherit",
        },
        #largeImg: {
            background: "url("https://images.unsplash.com/photo-1542856204-00101eb6def4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=975&q=80")",
              noRepeat "#fff",
            width: "100px",
            height: "100px",
            boxShadow: "0 5px 10px -2px rgba(0, 0, 0, 0.3)",
            pointerEvents: "none",
            position: "absolute",
            border: "4px solid #efefef",
            zIndex: "99",
            borderRadius: "100%",
            display: "block",
            opacity: "0",
            transition: "opacity 0.2s",
          },
          &:hover,
          &:active: {
            #largeImg: {
              opacity: "1"
            }
          }
        }
};

interface Props {
    magnified: HTMLElement;
    original: HTMLElement;
    imgWidth: number;
    imgHeight: number;

}

function Magnifier(props: Props) {

    document.getElementById("zoom").addEventListener(
        "mousemove",
        function (e) {
          //define all viables, then get entrypoint of mouse by calc the page position minus the 
          //offset on the element
          let original = document.getElementById("main-img"),
            magnified = document.getElementById("large-img"),
            style = magnified.style,
            x = e.pageX - this.offsetLeft,
            y = e.pageY - this.offsetTop,
            imgWidth = original.width,
            imgHeight = original.height,
            xperc = (x / imgWidth) * 100,
            yperc = (y / imgHeight) * 100;
      
          // Add some margin for right edge
          if (x > 0.01 * imgWidth) {
            xperc += 0.15 * xperc;
          }
      
          // Add some margin for bottom edge
          if (y >= 0.01 * imgHeight) {
            yperc += 0.15 * yperc;
          }
      
          // Set the background of the magnified image horizontal
          style.backgroundPositionX = xperc - 9 + "%";
          // Set the background of the magnified image vertical
          style.backgroundPositionY = yperc - 9 + "%";
      
          // Move the magnifying glass with the mouse movement.
          style.left = x - 50 + "px";
          style.top = y - 50 + "px";
        },
        false
      );
      

    return (
        <div sx={styles.container} >
            <div id="zoom" sx={styles.magnifyWrapper}>
                <img 
src="https://images.unsplash.com/photo-1542856204-00101eb6def4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=975&q=80" id="main-img" 
/>
            <div sx={styles.largeImg}></div>
            </div>
        </div>
    );
}

export { Magnifier };

  • 3
    Your `document.getElementById("main-img")` is not going to find anything because you don't have an element with `id="main-img"`. But I would take a very different approach here and store the zoom state (x/y/%) as react state rather than manipulating the DOM directly. – Linda Paiste Oct 02 '22 at 01:06

1 Answers1

1

Approach

You want to approach this differently in React because React is declarative rather than imperative. Instead of modifying the style property, you'll render your elements with a style based on the current state and props. You need to determine:

  • What is the current state of the application at any given point in time?
  • How can I represent that application state as a combination of component props and component state?
  • How do I render the DOM elements correctly based on that state?

Solution

Here's what I came up with:

import "./styles.css";
import { Box, Image } from "theme-ui";
import React, { useState } from "react";

interface MagnifierProps {
  imgSrc: string;
  imgWidth?: number;
  imgHeight?: number;
  magnifierRadius: number;
}

function Magnifier({
  imgSrc,
  imgHeight,
  imgWidth,
  magnifierRadius
}: MagnifierProps) {
  // Store the position of the magnifier and position of the large image relative to the magnifier.
  const [magnifierState, setMagnifierState] = useState({
    top: 0,
    left: 0,
    offsetX: 0,
    offsetY: 0
  });

  // Store whether the magnifier is currently visible.
  const [isVisible, setIsVisible] = useState(false);

  return (
    <Box
      sx={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "100vh"
      }}
    >
      <Box sx={{ position: "relative" }}>
        <Image
          src={imgSrc}
          // Set the intrinsic width of the element (optional).
          width={imgWidth}
          height={imgHeight}
          // Image can be a maximum of 50% of the viewport in either direction.
          sx={{
            maxHeight: "50vh",
            maxWidth: "50vh",
            height: "auto",
            width: "auto"
          }}
          // Set the magnifier state on every move of the mouse over the image.
          onMouseMove={(e) => {
            setIsVisible(true);
            const smallImage = e.currentTarget;
            // mouse position on the small image.
            const x = e.nativeEvent.offsetX;
            const y = e.nativeEvent.offsetY;
            setMagnifierState({
              top: y - magnifierRadius,
              left: x - magnifierRadius,
              // scale up to get position relative to the large image.
              offsetX:
                (x / smallImage.width) * smallImage.naturalWidth -
                magnifierRadius,
              offsetY:
                (y / smallImage.height) * smallImage.naturalHeight -
                magnifierRadius
            });
          }}
          // Hide the magnifier when leaving the image.
          onMouseLeave={() => setIsVisible(false)}
        />
        <Box
          sx={{
            // Constants:
            boxShadow: "0 5px 10px -2px rgba(0, 0, 0, 0.3)",
            pointerEvents: "none",
            position: "absolute",
            border: "4px solid #efefef",
            zIndex: 99,
            display: "block",
            transition: "opacity 0.2s",
            // Set background to the image from props:
            background: `url("${imgSrc}") no-repeat #fff`,
            // Set sizing based on the magnifierRadius from props:
            width: 2 * magnifierRadius,
            height: 2 * magnifierRadius,
            borderRadius: magnifierRadius,
            // Set position based on on the magnifier state:
            top: magnifierState.top + "px",
            left: magnifierState.left + "px",
            backgroundPositionX: -1 * magnifierState.offsetX,
            backgroundPositionY: -1 * magnifierState.offsetY,
            // Toggle opacity based on the isVisible state:
            opacity: isVisible ? 1 : 0
          }}
        />
      </Box>
    </Box>
  );
}

export default function App() {
  return (
    <Magnifier
      imgSrc="https://images.unsplash.com/photo-1542856204-00101eb6def4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=975&q=80"
      imgWidth={975}
      imgHeight={1300}
      magnifierRadius={50}
    />
  );
}

CodeSandbox Link

Explanation

Props

I created a Magnifier component which you can render inside of your App. Some of the information which was hard-coded as constants in the the CodePen pure JS example seems like it would be better as component props instead. Those props are:

interface MagnifierProps {
  imgSrc: string;
  imgWidth?: number;
  imgHeight?: number;
  magnifierRadius: number;
}

Now you can easily change what image you want to display using the imgSrc.

The imgWidth and imgHeight are optional and will set the width and height properties on the <img> element.

The magnifierRadius prop allows you to control the size of the zoom circle.

onMouseMove and magnifierState

We need to reposition the magnifier every time the mouse is moved over the image. I am doing this with an onMouseMove prop on the <img> element. You can get the position of the mouse relative to the image using e.nativeEvent.offsetX. You can use e.currentTarget to access the HTMLImageElement so that you know its width and height and can determine the ratio of the x to the width. (The example code uses a percentage string for the backgroundPositionX while I am using a pixel value computed by looking at the .naturalWidth property of the image element, but this really doesn't matter as both are fine).

I then store the computed top, left, offsetX and offsetY for the zoom circle and storing these to a magnifierState with a useState hook.

isVisible State

The example code used a nested SCSS selector to hide the zoom circle when the mouse is outside of the image. I personally found it easier to use React state for this. But both are fine.

Rendering

We can determine the style properties of the zoom circle based on the current state. There is no DOM manipulation. React will set the correct style properties on each render of the component. For example, we can hide and show with a ternary operator: opacity: isVisible ? 1 : 0. And we can set pixel values to numbers from the state: top: magnifierState.top + "px"

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
  • 1
    Thank you for the detailed explanation and great solution. I studied the code and played around with the values and I think I do understand it and have no questions –  Oct 04 '22 at 13:42