nikitaeverywhere / react-xmasonry

Simple, minimalistic and featured native masonry layout for React JS.
https://zitros.github.io/react-xmasonry
MIT License
91 stars 12 forks source link

[Feature Request]/[Help Needed] Custom breakpoints for responsive number of columns #23

Closed tejasahluwalia closed 3 years ago

tejasahluwalia commented 3 years ago

Hi,

I want to set the number of columns AND block width responsively so it always fills 100% of the container width

Currently, I've tried to implement this by manipulating targetBlockWidth but it doesn't work reliably.

  // Using variable columnWidth`
      <XMasonry targetBlockWidth={columnWidth} responsive={false}>`
       {items}
     </XMasonry>
    const [columnWidth, setColumnWidth] = useState(300);
    // Setting responsive column widths where sizes.width is the container width
    const getWidth = () => {
      if (sizes.width < 480) {
        setColumnWidth(Math.floor(sizes.width / 2));
      } else if (sizes.width < 640) {
        setColumnWidth(Math.floor(sizes.width / 3));
      } else if (sizes.width < 1280) {
        setColumnWidth(Math.floor(sizes.width / 4));
      } else if (sizes.width < 1536) {
        setColumnWidth(Math.floor(sizes.width / 5));
      } else if (sizes.width >= 1536) {
        setColumnWidth(Math.floor(sizes.width / 6));
      }
    };
    // Updating the variable on window resize
    useEffect(() => {
      function handleResize() {
        getWidth();
      }
      window.addEventListener("resize", handleResize);
      return (_) => {
        window.removeEventListener("resize", handleResize);
      };
    });

I was wondering if such a feature can be implemented natively? or if I'm missing a straightforward way to do this. I'm still a little new to react, so any help would be appreciated. Thanks a lot for this layout it works great in everything else.

What would be awesome is if the number of columns for certain breakpoints could be set in the XMasonry component itself and have the column width be automatically calculated. Something like this <XMasonry columns={{ 480: 2, 768: 3, 1280: 4, 1536: 6 }}>

nikitaeverywhere commented 3 years ago

Hello! Thanks for asking.

This library is was designed to handle one of two cases:

It cannot handle both together, as XMasonry always calculates the width for you (this is by design: so that it fits all screens nicely).

This functionality is something to consider for the next major release of XMasonry (a complete rewrite).

But looking at the problem itself, why targetBlockWidth wasn't a solution for you? It guarantees that the column will be more or less the same width on all devices, while the number of columns can vary.

tejasahluwalia commented 3 years ago

Thanks Nikita for the quick reply.

I quickly drew this up to show you why it isn't working for my content (gallery of images)

image

I wasn't very clear in my question. I want the columnWidth = calc(100% / numberColumns) at all times where numberColumns changes on different breakpoints.

Thanks again. Anyway by using the variable I am able to get close to what I want, except it doesn't always detect resize for some reason.

nikitaeverywhere commented 3 years ago

From your amazing picture, I understand the following: you want cards to scale when the window size scales. In CSS terms, you probably don't want px which XMasonry uses, you want vw or vmin.

Then, your strategy looks good, but I don't understand either why it doesn't always resize. Maybe, attaching a minimalistic example of the problem will help (what is sizes and where is it set?).

tejasahluwalia commented 3 years ago

Even giving one vw value would not achieve what I want. I usually use % widths for my columns. But also have different % for different breakpoints. For mobile it would be 50% (i.e. 2 cols) then 33% for medium screens, 25% for large and so on.

Thanks so much for sticking with me through this. The issue with resizing is I think a combination of problems.

This is the website: https://my2020hero.vercel.app/gallery I don't know if it's my browser or what, but if I refresh it goes back to the default targetBlockWidth. If I resize the window manually, it works fine. If I make the window fullscreen it breaks again.

sizes is from react-resize-aware which is basically a hook that gives me the container height and width.

return (
    <InfiniteScroll
      loadMore={loadMore}
      hasMore={hasMoreItems}
      initialLoad={false}
    >
      {resizeListener} //react-resize-aware listener
      <XMasonry
        targetBlockWidth={columnWidth}
        ref={(x) => (xMasonryComponent = x)}
      >
        {items}
      </XMasonry>
    </InfiniteScroll>
  );

I think my trouble is getting the <xMasonry> component to render after changing targetBlockWidth I thought using the xMasonry.update() function would fix it, but it doesn't work somehow.

useEffect(() => {

    function handleResize() {
      getWidth();
    }

    getWidth();   // Changing targetBlockWidth on load
    xMasonryComponent.update();   // Updating component again after load

    window.addEventListener("resize", handleResize);

    // cleanup
    return (_) => {
      window.removeEventListener("resize", handleResize);
    };
  });
nikitaeverywhere commented 3 years ago

Looking at your website now, I don't understand why just setting targetBlockWidth to a fixed pixel value doesn't work. By design, you don't need it: the card will be more or less the same size on all devices. If you set it to 200 px, you will get 2 columns on mobile devices and more columns depending on the size of the window, automatically. That was the point of this library after all, to make you not even think about sizes. With 200px, the block is min 150px and max 300px. By resizing the browser now, I think that's what you wanted. You can never guess the browser window's size, but you can make elements a consistent size.

For detecting width I don't see a problem on your website: it changes whenever I change the size of the window/fullscreen/rotate/etc

tejasahluwalia commented 3 years ago

Hmm if it looks fine to you, maybe it's my screen density?

I use a 2k monitor at 100% scaling on windows and this is how it looks on load.

image Checking the card size, its at 175px

--

And once I resize it and the new targetBlockWidth kicks in, it looks like this:

image Checking the card size, its at 350px

So that's where my problem lies.

tejasahluwalia commented 3 years ago

I increased the number of images I load initially in my infinite scroll component. This made it so that even on a high dpi screen (like my first image) there are enough images to cause the loadMore function to fire. This, in turn, makes xMasonry re-render and sets the correct targetBlockWidth I want, fixing my issue of small cards.

I was calling xMasonry.update(), but it probably wasn't working because it didn't detect a change in the children? Maybe it should also check if any of the props have changed or we could have an xMasonry.render() to re-render the component manually. But this will kill performance if we use it incorrectly.

nikitaeverywhere commented 3 years ago

Got it. It's strange, usually pixel behaves as a constant-size real-world unit even on different screen DPI. To be precise, not a constant size, but very well adopted to the device. Without any breakpoints, it should be exactly nearly the same number of inches on all devices and monitors, which should appear very same on different screens. Changing the targetBlockWidth dynamically would only benefit if you want to show, for instance, 2 columns on small screens, and exactly 2 columns (but wider ones) on bigger screens. But that's not what this library was designed for.

Regarding update, from the doc:

Trigger this function to update nested XBlocks sizes and positions. It is safe to trigger this function multiple times, updates are optimized. Technically, this function will check if any of containers changed its size and re-render XMasonry only if size was changed.

tejasahluwalia commented 3 years ago

Hey Nikita,

Sorry I had gotten busy with other work. I have fixed all my issues with xMasonry and am using it happily in my project. Thanks again for your help. I will close the issue with this comment.

For those with the same requirements as me in the future, here is how I fixed the columnWidth not being set on load:

First initialize the columnWidth as null or 0 const [columnWidth, setColumnWidth] = useState(null);

Set the width and handle resizing in useEffect hook

  useEffect(() => {
    function handleResize() {
      getWidth(sizes.width);
    }
    getWidth(sizes.width);
    window.addEventListener("resize", handleResize);
    return (_) => {
      window.removeEventListener("resize", handleResize);
    };
  });

Do a conditional rendering of xMasonry block only once columnWidth has been set.

      {columnWidth && (
        <XMasonry
          targetBlockWidth={columnWidth}
          ref={(x) => (xMasonryComponent = x)}
        >
          {items}
        </XMasonry>
      )}
nikitaeverywhere commented 3 years ago

Thanks for sharing these tips!

On Wed, 10 Mar 2021 at 08:22, Tejas Ahluwalia notifications@github.com wrote:

Hey Nikita,

Sorry I had gotten busy with other work. I have fixed all my issues with xMasonry and am using it happily in my project. Thanks again for your help. I will close the issue with this comment.

For those with the same requirements as me in the future, here is how I fixed the columnWidth not being set on load:

First initialize the columnWidth as null or 0 const [columnWidth, setColumnWidth] = useState(null);

Set the width and handle resizing in useEffect hook

useEffect(() => { function handleResize() { getWidth(sizes.width); } getWidth(sizes.width); window.addEventListener("resize", handleResize); return (_) => { window.removeEventListener("resize", handleResize); }; });

Do a conditional rendering of xMasonry block only once columnWidth has been set.

  {columnWidth && (
    <XMasonry
      targetBlockWidth={columnWidth}
      ref={(x) => (xMasonryComponent = x)}
    >
      {items}
    </XMasonry>
  )}

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/ZitRos/react-xmasonry/issues/23#issuecomment-794901090, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABGCCSETWZQYCWARCVUVF5LTC36Z3ANCNFSM4XFQZILA .

tejasahluwalia commented 3 years ago

Got it. It's strange, usually pixel behaves as a constant-size real-world unit even on different screen DPI. To be precise, not a constant size, but very well adopted to the device. Without any breakpoints, it should be exactly nearly the same number of inches on all devices and monitors, which should appear very same on different screens.

I understand, but PPI/DPI does not adapt well when the device is high PPI AND large size. It just looks too small with the width that xMasonry provides, so I absolutely need control of the width.

In a way, it is similar to how you say here:

Changing the targetBlockWidth dynamically would only benefit if you want to show, for instance, 2 columns on small screens, and exactly 2 columns (but wider ones) on bigger screens.