Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a React example in TypeScript #2002

Open
matronator opened this issue Jul 29, 2022 · 14 comments
Open

Add a React example in TypeScript #2002

matronator opened this issue Jul 29, 2022 · 14 comments

Comments

@matronator
Copy link

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).

@adumesny
Copy link
Member

adumesny commented Apr 8, 2023

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)

@erickfabiandev
Copy link

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.

@damien-schneider
Copy link
Contributor

damien-schneider commented Apr 20, 2024

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>
  );
};

@sikhaman
Copy link

sikhaman commented May 8, 2024

yes would be nice to have a working basic example. I'm struggling currently with rendering react node not just text or html

@Thebks
Copy link

Thebks commented May 10, 2024

@damien-schneider does the issue still exist? I'm using gridstack in one of my projects and your example could help me a big time.

@damien-schneider
Copy link
Contributor

The issue still exists as I didn't try again, but I will in few weeks. I don't have that much time for now but I will have time very soon

@Thebks
Copy link

Thebks commented May 11, 2024

The issue still exists as I didn't try again, but I will in few weeks. I don't have that much time for now but I will have time very soon

can I get access to the repo coz I would like to look into the problem in detail?

@FreakDev
Copy link

FreakDev commented May 27, 2024

Hi i've made new version of a gridstack wrapper based on @damien-schneider version

it seems to work well so far (i'm new to grid stack) here it is :
https://gist.github.com/FreakDev/47b965916c4018fc77284149e1ea6939

usage would look like :

App.tsx

import { GridstackProvider } from './gridstack/gridstack-provider';
import Grid from './grid';

function App() {
  return (
    <>
      <GridstackProvider option={{
        column: 12,
        animate: true,
        float: true,
        margin: 0,
        acceptWidgets: true,
        resizable: {
          handles: "all",
        },
      }}>
        <Grid /> 
      </GridstackProvider>
    </>
  );
}

export default App;

grid.tsx

import { useContext, useState } from "react";
import { GridStackContext } from "./gridstack/grid-stack-context";
import GridstackWrapper from "./gridstack/gridstask-wrapper";
import { GridstackItem } from "./gridstack/gridstack-item";

const Grid = () => {
  const { gridRef } = useContext(GridStackContext)

  const [showItem, setShowItem] = useState(false);
  const [counter, setCounter] = useState(2);

  return (
    <>
      <button type="button" onClick={() => setShowItem(!showItem)}>
        {showItem ? "Hide" : "Show"} item
      </button>
      <button type="button" onClick={() => gridRef.current?.column(10)}>
        Decrease columns
      </button>
      <button
        type="button"
        onClick={() => {
          gridRef.current?.addWidget({
            x: 1,
            y: 1,
            w: 2,
            h: 2,
          });
        }}
      >
        Add widget
      </button>
      <GridstackWrapper>
        {showItem && (
          <GridstackItem w={2} h={2} x={0} y={0}>
            <div>
              <h1>Item 1</h1>
            </div>
          </GridstackItem>
        )}
        <GridstackItem w={counter} h={4} x={0} y={0}>
          <button
            type="button"
            onClick={() => {
              setCounter(counter + 1);
            }}
          >
            Item width : {counter}
          </button>
        </GridstackItem>
      </GridstackWrapper>
    </>
  )
}

export default Grid;

@damien-schneider
Copy link
Contributor

damien-schneider commented May 28, 2024

I've tried the gist and it works pretty well! Thanks a lot!

I think we can little by little create a complete React wrapper, as it is done for Angular (or should we create an external repo ?). I'm playing with it to see what could be the best way to manage features in a controlled way, such as, for example, a controlled way to update the size and position of the item:

useLayoutEffect(() => {
    const element = itemRef.current;
    if (!gridRef.current || !element) {
      console.log("Grid instance or itemRef is not ready:", gridRef.current, element);
      return;
    }
    gridRef.current.update(element, {
      x: controlledX,
      y: controlledY,
      w: controlledWidth,
      h: controlledHeight,
    });
  }, [controlledWidth, controlledHeight, controlledX, controlledY, gridRef]);

But it has some cons too. Maybe optionally passing an itemRef could be great to easily customize some events. What do you think?

(I'm also trying to improve types)

@FreakDev
Copy link

Yes i think it would be easier to collaborate (with PR, etc...) with a repo. Go ahead ! (Or maybe you already have one ?)

Btw i've updated my gist with a quite similar solution to update the position/size... But I encounter other issues : because i've also implemented onChange event on the grid and managing the state "outside of the grid", with other triggers that re-render the components tree kinda break everything (Working on it...)

What would be your solution with ref? (Ideally I would try to minimize ref usage. I think it's a kind of anti-pattern with react, but i have to admit that sometimes there is no other choices, that why it exists)

@damien-schneider
Copy link
Contributor

damien-schneider commented May 28, 2024

Ok let's build this little by little then

https://github.com/damien-schneider/gridstack-react-wrapper

@adumesny
Copy link
Member

adumesny commented May 28, 2024

let's not create another repo please. I think it's easier to have it all in GS and make it official like I did for Angular (there are already many angular repo flavors which were done incorrectly and got out of sync very quickly, and no longer maintained).

I don't know React so really appreciate having community help on this. that said I explicitly wrote the angular version as components (most common usage, the other being directive) AND not using the DOM attributes (for one thing gs doesn't handle all possible values) as gs editing including moving between grids, can be hard to sync with Angular idea of where things should be. I do have simple ngFor dom version but those are naiive implementation and will conflict with gs quickly...

I see managing the state "outside of the grid" and that should be avoided...
also want to avoid re-creating widgets just becauswe they get reparented... so I would STRONLGY recommend doing the same for React and let GS do it's thing, but having the content on widgets be framework specific with simple wrappers for grid and gridItem - like I did for Angular. Please read the readme there to see.

also mentioned in #2002 (comment)

@sikhaman
Copy link

Agree I think to keep it consistent we should work in this repo. lets just hope PRs will be handled without delays. sometimes in libraries PRs are just hanging years

@adumesny
Copy link
Member

@sikhaman that's not the case here. if things look good they go in asap. and since I don't know React, likely even faster...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants