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

Closed
matronator opened this issue Jul 29, 2022 · 28 comments
Closed

Add a React example in TypeScript #2002

matronator opened this issue Jul 29, 2022 · 28 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...

@damien-schneider
Copy link
Contributor

damien-schneider commented Jul 8, 2024

I've made significant progress in creating a dynamic grid system with Gridstack and React, allowing for state-based updates and management of grid items. However, I'm encountering an issue with the grid.on("change") event and the proper cleanup of grid items on unmount. I'm reaching out for assistance, possibly from @FreakDev, to resolve this final hurdle.

Here's a summary of what I have achieved and the current challenge:

Achievements:

  1. State Initialization and Updates: I've initialized grid options as state and replicated state changes, allowing dynamic updates to item positions directly via state.
  2. Grid Item Movement: By utilizing grid.on("change"), the state updates accordingly when an item is moved within the grid.

Current Challenge:

The grid.on("change") event is set up during the grid's initial mount. This causes the event listener to only reference the initial state values, leading to issues when trying to use states dynamically.

Additionally, there's a problem with the cleanup process when unmounting a grid item. The following code does not seem to properly remove the widget and the event listener:

grid.removeWidget(itemRef.current, false);
grid.off("change");

Due to this, remounting the same grid item fails. This prevents the wrapper from fully benefiting from state management.

Context and Component Setup:

I've created a Gridstack context and provider to manage the grid's initialization and state. The GridstackItemComponent is responsible for individual grid items, managing their initialization, updates, and cleanup.

Demo:

A demo showcases the wrapper's capabilities, demonstrating dynamic state management and grid item control.

Request for Assistance:

If anyone has insights or solutions for making the grid.on("change") event more dynamic or ensuring proper cleanup of grid items on unmount, your help would be greatly appreciated. The goal is to have a robust wrapper that fully utilizes state management for Gridstack items. @adumesny I also have a question, would it be possible to make the onChange dynamic, because it is where all my problems are when I use Gridstack, as we have to setup only on mount, we cannot use states inside the onChange as it will only take the value when the grid.on("change") initialize ? Or should we unmount with grid.off and remount everytime we want to change the grid.on("change") logic, I don't know how it is managed usually in other lib but it is the first time I encounter this problem, thanks for your help !

Thank you for your support and contributions to this effort!

PS : The cn() can be removed but it demonstrate well how we can dynamically change anything we want based on the states

Here is a video showcase that show how close we are but just the unmount problem and on events make it stuck :/

Gridstack.react.wrapper.showcase.mp4
// gridstack-context.tsx
"use client";
import { GridStack, type GridStackOptions } from "gridstack";
import "gridstack/dist/gridstack-extra.css";
import "gridstack/dist/gridstack.css";
import type React from "react";
import { createContext, useContext, useEffect, useRef } from "react";
// Create a context for the GridStack instance
const GridstackContext = createContext<GridStack | null>(null);

export const useGridstack = () => {
  return useContext(GridstackContext);
};

export const GridstackProvider = ({
  options,
  children,
  grid,
  setGrid,
}: {
  options: GridStackOptions;
  children: React.ReactNode;
  grid: GridStack | null;
  setGrid: React.Dispatch<React.SetStateAction<GridStack | null>>;
}) => {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!grid && containerRef.current) {
      const gridInstance = GridStack.init(options, containerRef.current);
      setGrid(gridInstance);
      console.log("Gridstack initialized");
      console.log("USE EFFECT : GridRef.current :", grid);
    }
  }, [options, grid, setGrid]);
  console.log("CONTEXT : GridRef.current :", grid);

  return (
    <GridstackContext.Provider value={grid}>
      <div ref={containerRef}>{children}</div>
    </GridstackContext.Provider>
  );
};
"use client";
// gridstack-item.tsx

import { cn } from "@/utils/cn";
import type { GridStackElement, GridStackNode } from "gridstack";
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { useGridstack } from "./gridstack-context";

interface GridstackItemComponentProps {
  options: GridStackNode;
  setOptions?: React.Dispatch<React.SetStateAction<GridStackNode>>;
  children: ReactNode;
  className?: string;
}

const GridstackItemComponent = ({
  options,
  children,
  setOptions,
  className,
}: GridstackItemComponentProps) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const optionsRef = useRef<GridStackNode>(options);
  const [gridIsInitialized, setGridIsInitialized] = useState<boolean>(false);
  const grid = useGridstack();
  const itemRef = useRef<GridStackElement | null>(null);

  useEffect(() => {
    optionsRef.current = options;
  }, [options]);

  const updateStateIfItemPositionAndSizeHaveChanged = useCallback(
    (node: GridStackNode) => {
      setOptions?.((prev) => ({ ...prev, ...node }));
    },
    [setOptions],
  );

  const setupOnChangeEvent = useCallback(() => {
    if (!grid) {
      console.info("Gridstack is not initialized yet", grid);
    } else {
      console.info("Gridstack setup on change event", grid);
      grid.on("change", (event, nodes) => {
        console.log("Gridstack item has changed", event, nodes);
        for (const node of nodes) {
          if (node.el === itemRef.current) {
            updateStateIfItemPositionAndSizeHaveChanged(node);
          }
        }
      });
    }
  }, [grid, updateStateIfItemPositionAndSizeHaveChanged]);

  useEffect(() => {
    if (grid && optionsRef.current && containerRef.current && gridIsInitialized) {
      grid.batchUpdate(true);
      grid.update(containerRef.current, options);
      grid.batchUpdate(false);
      console.log("Gridstack item updated");
    }
  }, [grid, options, gridIsInitialized]);

  useEffect(() => {
    if (!grid || !containerRef.current || gridIsInitialized === true) {
      return;
    }
    grid.batchUpdate(true);
    itemRef.current = grid.addWidget(containerRef.current, optionsRef.current);
    grid.batchUpdate(false);
    console.log("Gridstack item initialized");
    setGridIsInitialized(true);
    setupOnChangeEvent();

    return () => {
      if (grid && itemRef.current && gridIsInitialized && containerRef) {
        grid.removeWidget(itemRef.current, false);
        grid.off("change");
        console.error("Gridstack item removed");
      }
    };
  }, [grid, gridIsInitialized, setupOnChangeEvent]);

  return (
    <div ref={containerRef}>
      <div className={cn("w-full h-full", className)}>{children}</div>
    </div>
  );
};

export default GridstackItemComponent;

And here is a demo using this wrapper which demonstrate the capabilities of how I manage the wrapper :

"use client";
// demo.tsx

import type { GridStack, GridStackNode, GridStackOptions } from "gridstack";
import type React from "react";
import { useEffect, useState } from "react";
import { GridstackProvider } from "./gridstack-context";
import GridstackItemComponent from "./gridstack-item";

export const GridstackDemo = () => {
  const [optionsItem1, setOptionsItem1] = useState<GridStackNode>({
    x: 0,
    y: 0,
    w: 2,
    h: 2,
  });
  const [optionsItem2, setOptionsItem2] = useState<GridStackNode>({
    x: 2,
    y: 0,
    w: 2,
    h: 2,
  });
  const [grid, setGrid] = useState<GridStack | null>(null);
  const [displayItem1, setDisplayItem1] = useState<boolean>(true);
  const [displayItem2, setDisplayItem2] = useState<boolean>(false);
  const gridOptions: GridStackOptions = {
    column: 12,
    acceptWidgets: false,
    removable: false,
    itemClass: "grid-stack-item",
    staticGrid: false,
    cellHeight: "100px",
    margin: "2",
    minRow: 5,
  };

  useEffect(() => {
    console.group("GridstackDemo");
    console.log("OptionsItem1 :", optionsItem1);
    console.log("OptionsItem2 :", optionsItem2);

    console.groupEnd();
  }, [optionsItem1, optionsItem2]);
  return (
    <>
      <div className="flex gap-2 *:bg-neutral-200 *:rounded-lg *:p-2 m-4">
        <button
          type="button"
          onClick={() => {
            grid?.addWidget(`<div style="background-color:#2E5">Item 3</div>`, {
              x: 4,
              y: 0,
              w: 2,
              h: 2,
            });
          }}
        >
          Add widget
        </button>
        <button
          type="button"
          onClick={() => {
            console.log(grid?.getGridItems());
            console.log("GRID", grid);
          }}
        >
          Console log grid Items
        </button>
        <button
          type="button"
          onClick={() => {
            setDisplayItem1((prev) => !prev);
          }}
        >
          {displayItem1 ? "Hide" : "Show"} Item 1
        </button>
        <button
          type="button"
          onClick={() => {
            setDisplayItem2((prev) => !prev);
          }}
        >
          {displayItem2 ? "Hide" : "Show"} Item 2
        </button>
      </div>
      <GridstackProvider options={gridOptions} grid={grid} setGrid={setGrid}>
        {displayItem1 && (
          <GridstackItemComponent
            options={optionsItem1}
            setOptions={setOptionsItem1}
            className={(optionsItem1.x ?? 0) < 5 ? "bg-blue-300" : "bg-red-300"}
          >
            <div>Item 1</div>
            <button
              type="button"
              onClick={() => {
                setOptionsItem1((prev) => ({ ...prev, x: (prev.x ?? 0) + 1 }));
              }}
            >
              Move right
            </button>
          </GridstackItemComponent>
        )}

        {displayItem2 && (
          <GridstackItemComponent
            options={optionsItem2}
            setOptions={setOptionsItem2}
            className="bg-neutral-300"
          >
            <div>Item 2</div>
            <button
              type="button"
              onClick={() => {
                setOptionsItem2((prev) => ({ ...prev, x: (prev.x ?? 0) + 1 }));
              }}
            >
              Move right
            </button>
          </GridstackItemComponent>
        )}
      </GridstackProvider>
    </>
  );
};

@adumesny
Copy link
Member

adumesny commented Jul 8, 2024

not sure what you mean by making the grid.on("change") event more dynamic

Also, you are still trying to make framework manage the state - guess you didn't read my comment above.
Look, I have many commercial apps at work usig the Angular wrapper similar to the one I published. I don't use the DOM to create grid items for a reason...

@damien-schneider
Copy link
Contributor

damien-schneider commented Jul 9, 2024

By making the grid.on("change") event more dynamic, I mean that if I reference a state variable within the grid.on("change") event, it will only capture the initial state value from when the grid was mounted, even if the state has changed subsequently.

I have read your comment thoroughly, and I appreciate the input. My goal is to help the community use Gridstack with React effectively. The existing React examples are quite outdated compared to current best practices and are often challenging to understand.

I’m also curious about how you "inject" Angular components inside your wrapper without using the DOM directly. In my approach, I use the DOM with refs, which allows me to manage Gridstack events without directly manipulating the React DOM, that's why I don't understand why you are always negative with this approach..

Although you mentioned that using state to manage Gridstack might not be advisable, I am confident that it can be achieved. By replicating changes from the grid to the state and vice versa (using grid.update() for state changes, and using grid.on("change") to update the state when the grid change, we can maintain a synchronized state management system. This approach, although it might seem unconventional, can simplify the use of Gridstack significantly.
Using state to manage the Grid will unlock a lot of possibility as for now Gridstack is VERY complicated to use with React.

Also without state some logic cannot be handled properly from my point of view : If you want a button to show if the grid is in float mode, then you call the getFloat() but when the getFloat() change it values, it doesn't rerender so we cannot update the UI based on this getFloat().
Another use case as I show in my video is to change the background color based on the X position of the item, and I don't know how to achieve it without a state (which create a rerender on change) which is synchronized with the X gridItem position

@adumesny
Copy link
Member

adumesny commented Jul 9, 2024

I’m also curious about how you "inject" Angular components inside your wrapper without using the DOM directly

using Angular createComponent() API by type. https://github.com/gridstack/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L229
creates the gridItem component when GS tells us to create one (THIS WORKS because the grid itself is also an angular component (either DOM <gridstack [options]="gridOptionsFull"> or again dynamic inside another angular component), then the widget component is created using selector field here
https://github.com/gridstack/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L239
at that point your component is created and it can do whatever in it's template to use regular angular DOM logic (for exmple https://github.com/gridstack/gridstack.js/blob/master/angular/projects/demo/src/app/dummy.component.ts#L13)

by not creating items in the DOM directly from the state using ngFor loop, we don't need to sync the states between GS and the app (which can at anytime call save() to get a copy, or load() to set a state) and widgets can be re-parented betweeen grids without re-creating any components for example.

when we do a state change we replicate it with a grid.update()

grid.update() is good and what you want to use (not dom state driven) but might not handle everything, like moving a widget from one grid to another though (there is no api to reparent widgets, though we let user do it so code is there).

Also without state some logic cannot be handled properly from my point of view : If you want a button to show if the grid is in float mode, then you call the getFloat()

then you can directly update your state. grid.on("change") might not always be called for some state changes, only if items move around...

I really appreciate your work on React as I'm not familiar with it, just want to make sure we don't go down the path of managing 2 states and keeping them in sync when I already went through this for Angular. I made the decision to let GS own the widget state (size, location, what children are loaded), and the app can serialize the json when needed, else call load() with it. much simpler. And there are many apis to update or add new widgets the app can call directly too...

@adumesny
Copy link
Member

adumesny commented Sep 29, 2024

@damien-schneider I pulled in your CL so we can iterate on that. I have not looked at it yet, but make sure to re-read the msg I have above (updated some) as there is a major architecture deciscion I did for Angular that is working really well (our ng demo has nested grids, multiple grids and things work like regular TS version but with angular components) with a very thin Ng wrapper. Similar to what I've been using in production for 8 years now (before I took over GS). I would think we can do that same in React. Not using DOM to create widgets...

@damien-schneider
Copy link
Contributor

damien-schneider commented Sep 29, 2024

Hi @adumesny,

Thanks for working on this and merging the CL and sharing the architectural decisions for Angular. It’s great to hear the thin Angular wrapper is working well in production, especially with features like nested and multiple grids.

Regarding integrating Gridstack with React, I believe using React’s state management has some strong benefits:

1. Fits React’s Data Flow: Managing Gridstack state with React ensures it’s integrated with the app’s overall state, making features like conditional rendering and dynamic styling easier.
2. Automatic UI Updates: React’s state management allows the UI to re-render automatically when the grid changes, reducing the need for manual DOM updates and keeping the code more maintainable.
3. Leverages React’s Ecosystem: Using React’s state means we can take advantage of hooks, context, and libraries like Redux, which can help handle complex state interactions.
4. Easier Debugging and Testing: With a single source of truth in React’s state, tools like React DevTools make it simpler to track state changes and debug issues.

I appreciate the benefits of letting Gridstack manage its own state, especially from an Angular perspective. However, integrating it with React’s state system aligns better with React’s design and can lead to a more efficient implementation.

I’d love to hear other point of view of the community, as the approach you’re proposing for React seems quite different from the typical React methodology. Not having a wrapper where you can directly have children (and so not using the DOM to create widgets) in it seems far away of a React approach, and would then have no benefits of creating a wrapper from my point of view.

But as I said I would love to hear the community on this architecture decision because I totally agree with you it's a major decision !

@adumesny
Copy link
Member

adumesny commented Sep 29, 2024

@damien-schneider creating widgets dynamically (using the newer v14 createComponent() and before that resolveComponentFactory odd flow) is also not natural for Angular - most people would use NgFor loops and DOM conditional statments, like you write your content of your widget, or your entire page.

I went throught that pain 8 years ago trying to sync the two... gave up and let Gridstack handle the widgets state (position, size, list of children) and instead focus on widget content itself be native angular way (what people really care about). Shipped many apps that way (and still do, some rather complex dashboardings which widgets replacing themselfs with others, maximizing to take entire screen, etc...)

Ultimately if your wrapper doesn't support multitple grids without re-creating the widget when dragging in between, or nested grids, or your having bugs dealing with state mismatch, without a ton of code, then it's the wrong approach. If you look at the ng wrapper, a lot is about adding ng events and such to make my component act like an angular component, not a ton about state issue with gridstack.

Ever since I exposed the angular wrapper (a subset of the code I use) download of this lib have increased a lot and clearly people are using it as it for their production work. I do get occasional bugs that's how I know :)

@damien-schneider
Copy link
Contributor

Thank you for the detailed insights, @adumesny. I completely agree that if integrating subgrids requires writing a substantial amount of code, it likely isn’t the right approach. In the version merged, I followed your advice and abandoned managing positions within the state. Handling every event proved to be inefficient and added unnecessary complexity.

In the actual wrapper, we leverage the DOM directly by allowing Gridstack to manage widget positioning and sizing through the makeWidget method. This approach minimizes our overhead and lets Gridstack handle its responsibilities effectively but have the advantage of having a reference to the grid item easily through natural React approach.

Do you think using makeWidget instead of createWidget is fundamentally the wrong approach, or do you see potential advantages in this method? I’d greatly appreciate your perspective on whether this aligns with best practices or if there are any potential issues we should consider.

Thanks again for your guidance!

@adumesny
Copy link
Member

adumesny commented Sep 29, 2024

@damien-schneider the other thing to know is that GS DOM attribute support isn't 100% by a long shot. Like you can't do nested grids at all. that was intentional as I was moving away from DOM attr as the way to create everything since ultimately a JSON format of GridStackOptions is what you need to serialize to a backend anyway for real apps. Having to suppport DOM attr is a distraction frankly - I even consider removing it all to make that clear. json is what you want.

So my focus became create a JSON format that can describe eveyrything, let GS manage the entire create/delete/moving/reparenting of widgets, and let framework get called to create the grid/gridItem/content part that can be native component.

Does React have a concept of custom component tags (like <gridstack> and <gridstrack-item> I have for ng) and ability to create those on the fly given a type ? (that why in angular you have to register selector -> type so I can look it up at runtime (hidden inside Angular otherwise that info is also there when you define a component).

Note: V11 (upcoming) has a GridStack.renderCB for just the content when GS creates the 2 parent divs. In Angular is use the addRemoveCB as I want the parents components so gridstack widget (by type) can be hosted).

@adumesny
Copy link
Member

closing since we have an ongoing /react folder (updated to work with more fancy nested grids)

@Aysnine
Copy link

Aysnine commented Dec 10, 2024

closing since we have an ongoing /react folder (updated to work with more fancy nested grids)

I noticed that the react folder hasn't been updated recently, and there are few usage examples. Coincidentally, I recently needed to use gridstack in a react project, and this is the demo I created based on the code from the react folder: https://github.com/Aysnine/gridstack-react. It includes a more user-friendly encapsulation method and a more friendly API operation, hoping to provide a reference for those in need.

const COMPONENT_MAP: Record<string, React.FC<{ content: string }>> = {
  Text: ({ content }) => <div className="w-full h-full">{content}</div>,
  // ... other components here
};

const gridOptions: GridStackOptions = {
  // ... initial grid options here
  children: [
    {
      w: 2,
      h: 2,
      x: 0,
      y: 0,
      content: JSON.stringify({
        component: "Text",
        props: { content: "Item 1" },
      }),
    },
  ],
};

export default function App() {
  const [initialOptions] = useState(gridOptions);

  return (
    <GridStackProvider initialOptions={initialOptions}>
      <!-- Custom Toolbar maybe -->
      <Toolbar />

      <GridStackRenderProvider>
        <GridStackRender componentMap={COMPONENT_MAP} />
      </GridStackRenderProvider>

      <!-- other content... -->
    </GridStackProvider>
  );
}
image

@adumesny
Copy link
Member

@Aysnine could you please update the existing demo in a changelist for others ? and maybe ping the last user since I don't know React

@Aysnine
Copy link

Aysnine commented Dec 11, 2024

@Aysnine could you please update the existing demo in a changelist for others ? and maybe ping the last user since I don't know React

Very happy, PR later :)

@Aysnine
Copy link

Aysnine commented Dec 13, 2024

@adumesny PR Ready #2898

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

8 participants