gridstack.js icon indicating copy to clipboard operation
gridstack.js copied to clipboard

Add a React example in TypeScript

Open matronator opened this issue 1 year ago • 14 comments

Subject of the issue

I'm trying to implement gridstack into my React TypeScript web app and I'm going from the React example. It would be really helpful to have an example written in TypeScript as well, as the types are not always easily deducible and I'm struggling to make everything the correct type to finally successfully compile the app.

Your environment

  • gridstack v5.1.1 and I'm using the HTML5
  • Safari 15.3 / macOS 11.6.2

Steps to reproduce

  1. Copy the React example into a TypeScript project.2.
  2. Try to compile

Expected behavior

Have an example using React with TypeScript to showcase the correct types and stuff.

Actual behavior

Currently only React example with pure JS (without TypeScript and types).

matronator avatar Jul 29 '22 20:07 matronator

I would love to have a high quality wrapper for React (and Vue) as I've now created one for Angular (what I use at work) - clearly keeping gridstack neutral (plain TS) as frameworks come and go....

I don't know React, but for more advanced things (multiple grids drag&drop, nested grids, dragging from toolbar to add/remove items) is it best to let gridstack do all the DOM manipulation as trying to sync between framework and GS becomes complex quickly. This is what I've done in the Angular wrapper - GS calls back to have correct Ng component created instead of <div class="gridstack-item"> for example, but all dom dragging/reparenting/removing is done by gs and callbacks the given framework for custom stuff.

The current React & Vue use the for loop which quickly falls appart IMO (I have the same for Angular but discourage for only the simplest things (display a grid from some data, with little modification by user)

adumesny avatar Apr 08 '23 16:04 adumesny

I have the same problem, I want to know if I can make it react, I am working in a NEXTJS environment with Typescript and it is costing me a bit to implement the use of this library, with pure js it works correctly.

erickfabiandev avatar Oct 11 '23 03:10 erickfabiandev

I'm building a wrapper to use gridstack properly and not with a hook, which could be used like this :

I'm close to achiving it but I have to be optimized and fixed for some weird rerenders

I think this code can help ;)

// demo.tsx
"use client";
import React, { useState } from "react";
import { GridstackAPI, GridstackItem, GridstackWrapper } from "./gridstack-wrapper";

export default function Demo() {
  const [counter, setCounter] = useState(1);
  const [showItem, setShowItem] = useState(false);
  const [gridstackAPI, setGridstackAPI] = useState<GridstackAPI | null>(null);
  return (
    <>
      <button type="button" onClick={() => setShowItem(!showItem)}>
        {showItem ? "Hide" : "Show"} item
      </button>
      <button type="button" onClick={() => gridstackAPI?.column(10)}>
        Decrease columns
      </button>
      <button
        type="button"
        onClick={() => {
          gridstackAPI?.addWidget({
            x: 1,
            y: 1,
            w: 2,
            h: 2,
          });
        }}
      >
        Add widget
      </button>
      <GridstackWrapper
        options={{
          column: 12,
          animate: true,
          float: true,
          margin: 0,
          acceptWidgets: true,
          resizable: {
            handles: "all",
          },
        }}
        setGridstackAPI={setGridstackAPI}
      >
        {showItem && (
          <GridstackItem initWidth={2} initHeight={2} initX={0} initY={0}>
            <div>
              <h1>Item 1</h1>
            </div>
          </GridstackItem>
        )}
        <GridstackItem initWidth={counter} initHeight={4} initX={0} initY={0}>
          <button
            type="button"
            onClick={() => {
              setCounter(counter + 1);
            }}
          >
            Item 3 width : {counter}
          </button>
        </GridstackItem>
      </GridstackWrapper>
    </>
  );
}

But I'm having issues when I want to update the grid and for example update the number of column

Here is the code, if someone could help me we would finally build a modern react example !

// gridstack-wrapper.tsx
"use client";
import { cn } from "@/utils/cn";
import { GridStack, GridStackNode, GridStackOptions } from "gridstack";
import "gridstack/dist/gridstack.min.css";
import "gridstack/dist/gridstack-extra.css";
import React, {
  useContext,
  useEffect,
  useRef,
  createContext,
  ReactNode,
  useLayoutEffect,
} from "react";
import { toast } from "sonner";

// Context to pass down the grid instance
type GridStackRefType = React.MutableRefObject<GridStack | undefined>;

const GridContext = createContext<GridStackRefType | undefined>(undefined);

export const useGridstackContext = () => {
  const context = useContext(GridContext);
  if (context === undefined) {
    throw new Error("useGridstackContext must be used within a GridstackWrapper");
  }
  return context;
};

interface GridstackWrapperProps {
  children: ReactNode;
  options?: GridStackOptions;
  onGridChange?: (items: GridStackNode[]) => void;
  setGridstackAPI: (API: GridstackAPI) => void;
}

export type GridstackAPI = {
  column: (count: number) => void;
  addWidget: (node: GridStackNode) => void;
};

export const GridstackWrapper: React.FC<GridstackWrapperProps> = ({
  children,
  options,
  setGridstackAPI,
}) => {
  const gridInstanceRef = useRef<GridStack>();
  const gridHTMLElementRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    if (!gridInstanceRef.current && gridHTMLElementRef.current && options) {
      initializeGridstack();
    } else {
      refreshGridstack();
    }
    // initializeGridstack();
  }, [options, setGridstackAPI]);

  function initializeGridstack() {
    if (!gridInstanceRef.current && gridHTMLElementRef.current && options) {
      gridInstanceRef.current = GridStack.init(options, gridHTMLElementRef.current);
      toast("GridStack Initialized");
    }
  }
  function refreshGridstack() {
    gridInstanceRef.current?.batchUpdate();
    gridInstanceRef.current?.commit();
  }

  // TRYING TO BUILD AN API
  useEffect(() => {
    if (!gridInstanceRef.current) {
      return;
    }
    const functionSetColumns = (count: number) => {
      gridInstanceRef.current?.column(count);
      toast(`Column count set to ${count}`);
    };
    const functionAddWidget = (node: GridStackNode) => {
      gridInstanceRef.current?.addWidget(node);
    };
    setGridstackAPI({
      column: functionSetColumns,
      addWidget: functionAddWidget,
    });
  }, [setGridstackAPI, gridInstanceRef]);

  return (
    <GridContext.Provider value={gridInstanceRef}>
      <div ref={gridHTMLElementRef} className="grid-stack">
        {children}
      </div>
    </GridContext.Provider>
  );
};

interface GridstackItemProps {
  children: ReactNode;
  initX: number;
  initY: number;
  initWidth: number;
  initHeight: number;
  className?: string;
}

// GridstackItem component
export const GridstackItem: React.FC<GridstackItemProps> = ({
  children,
  initX,
  initY,
  initWidth,
  initHeight,
  className,
}) => {
  const itemRef = useRef<HTMLDivElement>(null);
  const gridInstanceRef = useGridstackContext();

  useLayoutEffect(() => {
    const gridInstance = gridInstanceRef.current;
    const element = itemRef.current;

    if (!gridInstance || !element) {
      console.log("Grid instance or itemRef is not ready:", gridInstance, element);
      return;
    }

    console.log("Running batchUpdate and makeWidget");
    toast("Running batchUpdate and makeWidget");
    gridInstance.makeWidget(element, {
      x: initX,
      y: initY,
      w: initWidth,
      h: initHeight,
    }); // Ensure item properties are used if provided
    return () => {
      console.log("Removing widget:", element);
      gridInstance.removeWidget(element, false); // Pass `false` to not remove from DOM, as React will handle it
    };
    // initWidth, initHeight, initX, initY are not in the dependencies array because they are init values, they should not trigger a re-render
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div ref={itemRef} className={cn("grid-stack-item bg-red-100 rounded-lg", className)}>
      <div className="grid-stack-item-content">{children}</div>
    </div>
  );
};

damien-schneider avatar Apr 20 '24 15:04 damien-schneider