From c3f68051f0d4b5a55a8fbfb6bec8df7f7052c5b2 Mon Sep 17 00:00:00 2001 From: CNine Date: Fri, 7 Feb 2025 10:01:42 +0800 Subject: [PATCH 1/8] Simplify react lib design --- react/README.md | 311 +++++++++++++----- react/src/App.tsx | 114 ++++++- react/src/component-map.tsx | 20 ++ react/src/components/complex-card.tsx | 170 ++++++++++ .../component-info-map-context.ts | 16 + .../component-info-map-provider.tsx | 58 ++++ .../components/component-info-map/index.ts | 3 + .../use-component-info-map.ts | 12 + react/src/components/debug-info.tsx | 62 ++++ react/src/components/toolbar.tsx | 76 +++++ react/src/default-grid-options.ts | 107 ++++++ react/src/{demo => }/demo.css | 0 react/src/demo/demo.tsx | 281 ---------------- react/src/lib/grid-stack-context.ts | 33 ++ .../lib/grid-stack-handle-re-initializer.tsx | 56 ++++ react/src/lib/grid-stack-item-context.tsx | 60 ++++ react/src/lib/grid-stack-item.tsx | 100 ++++++ react/src/lib/grid-stack-provider.tsx | 56 ++++ react/src/lib/grid-stack-render-context.ts | 24 ++ react/src/lib/grid-stack-render.tsx | 49 +++ react/src/lib/index.ts | 26 ++ react/src/utils.ts | 3 + react/vite.config.ts | 5 +- 23 files changed, 1278 insertions(+), 364 deletions(-) create mode 100644 react/src/component-map.tsx create mode 100644 react/src/components/complex-card.tsx create mode 100644 react/src/components/component-info-map/component-info-map-context.ts create mode 100644 react/src/components/component-info-map/component-info-map-provider.tsx create mode 100644 react/src/components/component-info-map/index.ts create mode 100644 react/src/components/component-info-map/use-component-info-map.ts create mode 100644 react/src/components/debug-info.tsx create mode 100644 react/src/components/toolbar.tsx create mode 100644 react/src/default-grid-options.ts rename react/src/{demo => }/demo.css (100%) delete mode 100644 react/src/demo/demo.tsx create mode 100644 react/src/lib/grid-stack-context.ts create mode 100644 react/src/lib/grid-stack-handle-re-initializer.tsx create mode 100644 react/src/lib/grid-stack-item-context.tsx create mode 100644 react/src/lib/grid-stack-item.tsx create mode 100644 react/src/lib/grid-stack-provider.tsx create mode 100644 react/src/lib/grid-stack-render-context.ts create mode 100644 react/src/lib/grid-stack-render.tsx create mode 100644 react/src/lib/index.ts create mode 100644 react/src/utils.ts diff --git a/react/README.md b/react/README.md index 38185e285..fa5239059 100644 --- a/react/README.md +++ b/react/README.md @@ -2,119 +2,280 @@ A React wrapper component for GridStack that provides better TypeScript support and React integration experience. +Open in [CodeSandbox](https://codesandbox.io/p/sandbox/github/gridstack/gridstack.js/tree/master/react?file=/src/App.tsx) + ## TODO -- [x] Component mapping -- [x] SubGrid support -- [ ] Save and restore layout -- [ ] Publish to npm +- [x] Add Widgets +- [x] Add Sub Grid +- [x] Nested Sub Grid +- [x] Remove Widget +- [x] Copy(Duplicate) Widget +- [x] Custom handle +- [ ] Drag between two grid stacks -## Basic Usage +Welcome to give any suggestions and ideas, you can submit an issue or contact me by email. :) -This is not an npm package, it's just a demo project. Please copy the relevant code to your project to use it. +## Usage -```tsx -import { - GridStackProvider, - GridStackRender, - GridStackRenderProvider, -} from "path/to/lib"; -import "gridstack/dist/gridstack.css"; -import "gridstack/dist/gridstack-extra.css"; -import "path/to/demo.css"; - -function Text({ content }: { content: string }) { - return
{content}
; -} +**Simple** -const COMPONENT_MAP = { - Text, - // ... other components -}; - -// Grid options -const gridOptions = { - acceptWidgets: true, - margin: 8, - cellHeight: 50, - children: [ - { - id: "item1", - h: 2, - w: 2, - content: JSON.stringify({ - name: "Text", - props: { content: "Item 1" }, - }), - }, - // ... other grid items - ], -}; +Render item with widget id selector. +```tsx function App() { + const [uncontrolledInitialOptions] = useState({ + // ... + children: [ + { id: "item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + ], + }); + return ( - - + + - - - - - + + +
hello
+
- + +
grid
+
+
); } ``` -## Advanced Features +**Advanced** -### Toolbar Operations +Render item with widget map component info. -Provide APIs to add new components and sub-grids: +_ComponentInfoMap is just an example, you can use any way you want to store and retrieve component information._ ```tsx -function Toolbar() { - const { addWidget, addSubGrid } = useGridStackContext(); +function App() { + const [uncontrolledInitialOptions] = useState({ + // ... + children: [ + { id: "item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + ], + }); + + const [initialComponentInfoMap] = useState>( + () => ({ + item1: { component: "Text", serializableProps: { content: "Text" } }, + item2: { + component: "ComplexCard", + serializableProps: { title: "Complex Card", color: "red" }, + }, + }) + ); return ( -
- - -
+ + + + + + + + + + ); +} + +export function DynamicGridStackItems() { + const { componentInfoMap } = useComponentInfoMap(); + + return ( + <> + {Array.from(componentInfoMap.entries()).map( + ([widgetId, componentInfo]) => { + const Component = COMPONENT_MAP[componentInfo.component]; + if (!Component) { + throw new Error(`Component ${componentInfo.component} not found`); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const props = componentInfo.serializableProps as any; + + if (componentInfo.component === "ComplexCard") { + return ( + + + + + + ); + } + + return ( + + + + ); + } + )} + ); } ``` -### Layout Saving +**Experimental** -Get the current layout: +Render item with custom handle. ```tsx -const { saveOptions } = useGridStackContext(); - -const currentLayout = saveOptions(); + + + + + ``` ## API Reference -### GridStackProvider +### Components -The main context provider, accepts the following properties: +#### GridStackProvider -- `initialOptions`: Initial configuration options for GridStack +Top-level component that provides GridStack context. -### GridStackRender +```typescript +type GridStackProviderProps = { + initialOptions: GridStackOptions; // GridStack initialization options + children: React.ReactNode; +}; +``` -The core component for rendering the grid, accepts the following properties: +#### GridStackRender + +Render GridStack root container component. + +```typescript +type GridStackRenderProps = { + children: React.ReactNode; +}; +``` -- `componentMap`: A mapping from component names to actual React components +#### GridStackItem + +Component representing a single grid item. + +```typescript +type GridStackItemProps = { + id: string; // Grid item unique identifier + children: React.ReactNode; +}; +``` + +#### GridStackHandleReInitializer + +Experimental component for reinitializing the drag handle of a grid item. + +```typescript +type GridStackHandleReInitializerProps = { + children: React.ReactNode; +}; +``` + +### Contexts + +#### GridStackContext + +Provide GridStack core functionality context. + +```typescript +interface GridStackContextType { + initialOptions: GridStackOptions; + addWidget: (widget: GridStackWidget) => void; + removeWidget: (el: GridStackElement) => void; + saveOptions: () => ReturnType | undefined; + + _gridStack: { + value: GridStack | null; + set: React.Dispatch>; + }; +} +``` + +#### GridStackItemContext + +Provide single grid item functionality context. + +```typescript +type GridStackItemContextType = { + id: string; + // Native methods + remove: () => void; + update: (opt: GridStackWidget) => void; + + // Extended methods + getBounds: () => { + current: { x?: number; y?: number; w?: number; h?: number }; + original: { x?: number; y?: number; w?: number; h?: number }; + } | null; + setSize: (size: { w: number; h: number }) => void; + setPosition: (position: { x: number; y: number }) => void; +}; +``` + +#### GridStackRenderContext + +Provide rendering related functionality context. + +```typescript +type GridStackRenderContextType = { + getWidgetContainer: (widgetId: string) => HTMLElement | null; +}; +``` ### Hooks -- `useGridStackContext()`: Access GridStack context and operations - - `addWidget`: Add a new component - - `addSubGrid`: Add a new sub-grid - - `saveOptions`: Save current layout - - `initialOptions`: Initial configuration options +#### useGridStackContext + +Get GridStack context. + +```typescript +function useGridStackContext(): GridStackContextType; +``` + +#### useGridStackItemContext + +Get grid item context. + +```typescript +function useGridStackItemContext(): GridStackItemContextType; +``` + +#### useGridStackRenderContext + +Get rendering context. + +```typescript +function useGridStackRenderContext(): GridStackRenderContextType; +``` + +### Type Exports + +```typescript +export type { + GridStackContextType, + GridStackProviderProps, + GridStackRenderContextType, + GridStackRenderProps, + GridStackItemProps, + GridStackItemContextType, + GridStackHandleReInitializerProps, +}; +``` diff --git a/react/src/App.tsx b/react/src/App.tsx index 72c9508a5..16166f868 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -1,14 +1,114 @@ -import { GridStackDemo } from "./demo/demo"; +import { useState } from "react"; + +import "./demo.css"; + +import { + GridStackHandleReInitializer, + GridStackItem, + GridStackProvider, + GridStackRender, +} from "./lib"; +import { GridStackOptions } from "gridstack"; +import { + CUSTOM_DRAGGABLE_HANDLE_CLASSNAME, + defaultGridOptions, +} from "./default-grid-options"; +import { COMPONENT_MAP, ComponentInfo } from "./component-map"; +import { ComplexCardEditableWrapper } from "./components/complex-card"; +import { + ComponentInfoMapProvider, + useComponentInfoMap, +} from "./components/component-info-map"; +import { DebugInfo } from "./components/debug-info"; +import { Toolbar } from "./components/toolbar"; + +export default function App() { + const [uncontrolledInitialOptions] = + useState(defaultGridOptions); + + const [initialComponentInfoMap] = useState>( + () => ({ + item3: { component: "Text", serializableProps: { content: "Text" } }, + item4: { + component: "Button", + serializableProps: { label: "Click me" }, + }, + item999: { + component: "ComplexCard", + serializableProps: { title: "Complex Card", color: "red" }, + }, + }) + ); + + return ( + + + + + + {/* Simple: Render item with id selector */} + +
hello
+
+ + +
grid
+
+ + {/* Advanced: Render item with widget map component info */} + + + {/* Experimental: Render item with custom handle */} + + + + + +
+ + +
+
+ ); +} + +export function DynamicGridStackItems() { + const { componentInfoMap } = useComponentInfoMap(); -function App() { return ( <> -

Gridstack React Wrapper Demo

+ {Array.from(componentInfoMap.entries()).map( + ([widgetId, componentInfo]) => { + const Component = COMPONENT_MAP[componentInfo.component]; + if (!Component) { + throw new Error(`Component ${componentInfo.component} not found`); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const props = componentInfo.serializableProps as any; -

(Uncontrolled)

- + if (componentInfo.component === "ComplexCard") { + return ( + + + + + + ); + } + + return ( + + + + ); + } + )} ); } - -export default App; diff --git a/react/src/component-map.tsx b/react/src/component-map.tsx new file mode 100644 index 000000000..587686e06 --- /dev/null +++ b/react/src/component-map.tsx @@ -0,0 +1,20 @@ +import { ComponentProps } from "react"; +import { ComplexCard } from "./components/complex-card"; + +export const COMPONENT_MAP = { + Text: (props: { content: string }) =>
{props.content}
, + Button: (props: { label: string }) => , + ComplexCard, + // ... more components here +}; + +export type ComponentMapName = keyof typeof COMPONENT_MAP; +export type ComponentMapProps = { + [K in ComponentMapName]: ComponentProps<(typeof COMPONENT_MAP)[K]>; +}; +export type ComponentInfo = { + [K in ComponentMapName]: { + component: K; + serializableProps: ComponentMapProps[K]; + }; +}[ComponentMapName]; diff --git a/react/src/components/complex-card.tsx b/react/src/components/complex-card.tsx new file mode 100644 index 000000000..31077c812 --- /dev/null +++ b/react/src/components/complex-card.tsx @@ -0,0 +1,170 @@ +import { PropsWithChildren, useState } from "react"; +import { + GridStackHandleReInitializer, + useGridStackContext, + useGridStackItemContext, +} from "../lib"; +import { newId } from "../utils"; +import { CUSTOM_DRAGGABLE_HANDLE_CLASSNAME } from "../default-grid-options"; +import { useComponentInfoMap } from "./component-info-map"; + +type ComplexCardProps = { + title: string; + color?: string; +}; + +export function ComplexCard(props: ComplexCardProps) { + return ( +
+

{props.title}

+
+ ); +} + +export function ComplexCardEditableWrapper( + props: PropsWithChildren<{ serializableProps: ComplexCardProps }> +) { + const { + id, + remove, + getBounds, + setSize: setSizeGridStack, + } = useGridStackItemContext(); + const { addWidget } = useGridStackContext(); + const { addComponentInfo, updateComponentInfo } = useComponentInfoMap(); + + const [dialogEditOpen, setDialogEditOpen] = useState(false); + + const title = props.serializableProps.title; + const color = props.serializableProps.color; + const setTitle = (title: string) => { + updateComponentInfo(id, { + component: "ComplexCard", + serializableProps: { title, color }, + }); + }; + const setColor = (color: string) => { + updateComponentInfo(id, { + component: "ComplexCard", + serializableProps: { title, color }, + }); + }; + const [size, _setSize] = useState<{ w: number; h: number }>({ + w: 0, + h: 0, + }); + const setSize = (size: { w: number; h: number }) => { + _setSize(size); + setSizeGridStack(size); + }; + + return ( + <> + {props.children} +
+ + + + + setDialogEditOpen(false)}> +
+ + + +
+ +
+
+
+ + + + + + +
+ + ); +} diff --git a/react/src/components/component-info-map/component-info-map-context.ts b/react/src/components/component-info-map/component-info-map-context.ts new file mode 100644 index 000000000..6781eb9f6 --- /dev/null +++ b/react/src/components/component-info-map/component-info-map-context.ts @@ -0,0 +1,16 @@ +import { createContext } from "react"; +import { ComponentInfo } from "../../component-map"; + +export const ComponentInfoMapContext = createContext<{ + componentInfoMap: Map; + setComponentInfoMap: (componentInfoMap: Map) => void; + removeComponentInfo: (widgetId: string) => void; + addComponentInfo: (widgetId: string, componentInfo: ComponentInfo) => void; + updateComponentInfo: (widgetId: string, componentInfo: ComponentInfo) => void; +}>({ + componentInfoMap: new Map(), + setComponentInfoMap: () => {}, + removeComponentInfo: () => {}, + addComponentInfo: () => {}, + updateComponentInfo: () => {}, +}); diff --git a/react/src/components/component-info-map/component-info-map-provider.tsx b/react/src/components/component-info-map/component-info-map-provider.tsx new file mode 100644 index 000000000..87dee6e61 --- /dev/null +++ b/react/src/components/component-info-map/component-info-map-provider.tsx @@ -0,0 +1,58 @@ +import { PropsWithChildren, useState, useCallback } from "react"; +import { ComponentInfo } from "../../component-map"; +import { ComponentInfoMapContext } from "./component-info-map-context"; + +export function ComponentInfoMapProvider({ + children, + initialComponentInfoMap, +}: PropsWithChildren<{ + initialComponentInfoMap: Record; +}>) { + const [componentInfoMap, setComponentInfoMap] = useState< + Map + >(new Map(Object.entries(initialComponentInfoMap))); + + const removeComponentInfo = useCallback((widgetId: string) => { + setComponentInfoMap((prev) => { + const newMap = new Map(prev); + newMap.delete(widgetId); + return newMap; + }); + }, []); + + const addComponentInfo = useCallback( + (widgetId: string, componentInfo: ComponentInfo) => { + setComponentInfoMap((prev) => { + const newMap = new Map(prev); + newMap.set(widgetId, componentInfo); + return newMap; + }); + }, + [] + ); + + const updateComponentInfo = useCallback( + (widgetId: string, componentInfo: ComponentInfo) => { + setComponentInfoMap((prev) => { + const newMap = new Map(prev); + newMap.set(widgetId, componentInfo); + return newMap; + }); + }, + [] + ); + + return ( + + {children} + + ); +} diff --git a/react/src/components/component-info-map/index.ts b/react/src/components/component-info-map/index.ts new file mode 100644 index 000000000..a4eabda6c --- /dev/null +++ b/react/src/components/component-info-map/index.ts @@ -0,0 +1,3 @@ +export { ComponentInfoMapContext } from "./component-info-map-context"; +export { useComponentInfoMap } from "./use-component-info-map"; +export { ComponentInfoMapProvider } from "./component-info-map-provider"; diff --git a/react/src/components/component-info-map/use-component-info-map.ts b/react/src/components/component-info-map/use-component-info-map.ts new file mode 100644 index 000000000..7abf92efc --- /dev/null +++ b/react/src/components/component-info-map/use-component-info-map.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import { ComponentInfoMapContext } from "./component-info-map-context"; + +export function useComponentInfoMap() { + const context = useContext(ComponentInfoMapContext); + if (!context) { + throw new Error( + "useComponentInfoMap must be used within a ComponentInfoMapProvider" + ); + } + return context; +} diff --git a/react/src/components/debug-info.tsx b/react/src/components/debug-info.tsx new file mode 100644 index 000000000..33b30c040 --- /dev/null +++ b/react/src/components/debug-info.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from "react"; +import { useGridStackContext } from "../lib"; +import { GridStackOptions, GridStackWidget } from "gridstack"; + +export function DebugInfo() { + const { initialOptions, saveOptions } = useGridStackContext(); + + const [realtimeOptions, setRealtimeOptions] = useState< + GridStackOptions | GridStackWidget[] | undefined + >(undefined); + + useEffect(() => { + const timer = setInterval(() => { + if (saveOptions) { + const data = saveOptions(); + setRealtimeOptions(data); + } + }, 2000); + + return () => clearInterval(timer); + }, [saveOptions]); + + return ( +
+

Debug Info

+
+
+

Initial Options

+
+            {JSON.stringify(initialOptions, null, 2)}
+          
+
+
+

Realtime Options (2s refresh)

+
+            {JSON.stringify(realtimeOptions, null, 2)}
+          
+
+
+
+ ); +} diff --git a/react/src/components/toolbar.tsx b/react/src/components/toolbar.tsx new file mode 100644 index 000000000..e734504d0 --- /dev/null +++ b/react/src/components/toolbar.tsx @@ -0,0 +1,76 @@ +import { BREAKPOINTS, CELL_HEIGHT } from "../default-grid-options"; +import { useGridStackContext } from "../lib"; +import { newId } from "../utils"; +import { useComponentInfoMap } from "./component-info-map"; + +export function Toolbar() { + const { addWidget } = useGridStackContext(); + const { addComponentInfo } = useComponentInfoMap(); + + return ( +
+ + + +
+ ); +} diff --git a/react/src/default-grid-options.ts b/react/src/default-grid-options.ts new file mode 100644 index 000000000..a67fe163a --- /dev/null +++ b/react/src/default-grid-options.ts @@ -0,0 +1,107 @@ +import { GridStackOptions } from "gridstack"; + +export const CELL_HEIGHT = 50; +export const BREAKPOINTS = [ + { c: 1, w: 700 }, + { c: 3, w: 850 }, + { c: 6, w: 950 }, + { c: 8, w: 1100 }, +]; + +export const CUSTOM_DRAGGABLE_HANDLE_CLASSNAME = "custom-draggable-handle"; + +export const defaultGridOptions: GridStackOptions = { + handleClass: CUSTOM_DRAGGABLE_HANDLE_CLASSNAME, + acceptWidgets: true, + columnOpts: { + breakpointForWindow: true, + breakpoints: BREAKPOINTS, + layout: "moveScale", + columnMax: 12, + }, + margin: 8, + cellHeight: CELL_HEIGHT, + subGridOpts: { + acceptWidgets: true, + columnOpts: { + breakpoints: BREAKPOINTS, + layout: "moveScale", + }, + margin: 8, + minRow: 2, + cellHeight: CELL_HEIGHT, + }, + children: [ + { + id: "item1", + h: 2, + w: 2, + x: 0, + y: 0, + }, + { + id: "item2", + h: 2, + w: 2, + x: 2, + y: 0, + }, + { + id: "sub-grid-1", + h: 5, + sizeToContent: true, + subGridOpts: { + acceptWidgets: true, + cellHeight: CELL_HEIGHT, + alwaysShowResizeHandle: false, + column: "auto", + minRow: 2, + layout: "list", + margin: 8, + children: [ + { + id: "sub-grid-1-title", + locked: true, + noMove: true, + noResize: true, + w: 12, + x: 0, + y: 0, + content: "Sub Grid 1", + }, + { + id: "item3", + h: 2, + w: 2, + x: 0, + y: 1, + }, + { + id: "item4", + h: 2, + w: 2, + x: 2, + y: 0, + }, + ], + }, + w: 12, + x: 0, + y: 2, + }, + { + id: "item5", + w: 2, + h: 2, + x: 0, + y: 2, + }, + { + id: "item999", + w: 4, + h: 4, + x: 0, + y: 2, + }, + ], +}; diff --git a/react/src/demo/demo.css b/react/src/demo.css similarity index 100% rename from react/src/demo/demo.css rename to react/src/demo.css diff --git a/react/src/demo/demo.tsx b/react/src/demo/demo.tsx deleted file mode 100644 index 2e1499b59..000000000 --- a/react/src/demo/demo.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { ComponentProps, useEffect, useState } from "react"; -import { GridStackOptions, GridStackWidget } from "gridstack"; -import { - ComponentDataType, - ComponentMap, - GridStackProvider, - GridStackRender, - GridStackRenderProvider, - useGridStackContext, -} from "../../lib"; - -import "gridstack/dist/gridstack-extra.css"; -import "gridstack/dist/gridstack.css"; -import "./demo.css"; - -const CELL_HEIGHT = 50; -const BREAKPOINTS = [ - { c: 1, w: 700 }, - { c: 3, w: 850 }, - { c: 6, w: 950 }, - { c: 8, w: 1100 }, -]; - -function Text({ content }: { content: string }) { - return
{content}
; -} - -const COMPONENT_MAP: ComponentMap = { - Text, - // ... other components here -}; - -// ! Content must be json string like this: -// { name: "Text", props: { content: "Item 1" } } -const gridOptions: GridStackOptions = { - acceptWidgets: true, - columnOpts: { - breakpointForWindow: true, - breakpoints: BREAKPOINTS, - layout: "moveScale", - columnMax: 12, - }, - margin: 8, - cellHeight: CELL_HEIGHT, - subGridOpts: { - acceptWidgets: true, - columnOpts: { - breakpoints: BREAKPOINTS, - layout: "moveScale", - }, - margin: 8, - minRow: 2, - cellHeight: CELL_HEIGHT, - }, - children: [ - { - id: "item1", - h: 2, - w: 2, - x: 0, - y: 0, - content: JSON.stringify({ - name: "Text", - props: { content: "Item 1" }, - } satisfies ComponentDataType>), // if need type check - }, - { - id: "item2", - h: 2, - w: 2, - x: 2, - y: 0, - content: JSON.stringify({ - name: "Text", - props: { content: "Item 2" }, - }), - }, - { - id: "sub-grid-1", - h: 5, - sizeToContent: true, - subGridOpts: { - acceptWidgets: true, - cellHeight: CELL_HEIGHT, - alwaysShowResizeHandle: false, - column: "auto", - minRow: 2, - layout: "list", - margin: 8, - children: [ - { - id: "sub-grid-1-title", - locked: true, - noMove: true, - noResize: true, - w: 12, - x: 0, - y: 0, - content: JSON.stringify({ - name: "Text", - props: { content: "Sub Grid 1 Title" }, - }), - }, - { - id: "item3", - h: 2, - w: 2, - x: 0, - y: 1, - content: JSON.stringify({ - name: "Text", - props: { content: "Item 3" }, - }), - }, - { - id: "item4", - h: 2, - w: 2, - x: 2, - y: 0, - content: JSON.stringify({ - name: "Text", - props: { content: "Item 4" }, - }), - }, - ], - }, - w: 12, - x: 0, - y: 2, - }, - ], -}; - -export function GridStackDemo() { - // ! Uncontrolled - const [initialOptions] = useState(gridOptions); - - return ( - - - - - - - - - - ); -} - -function Toolbar() { - const { addWidget, addSubGrid } = useGridStackContext(); - - return ( -
- - - -
- ); -} - -function DebugInfo() { - const { initialOptions, saveOptions } = useGridStackContext(); - - const [realtimeOptions, setRealtimeOptions] = useState< - GridStackOptions | GridStackWidget[] | undefined - >(undefined); - - useEffect(() => { - const timer = setInterval(() => { - if (saveOptions) { - const data = saveOptions(); - setRealtimeOptions(data); - } - }, 2000); - - return () => clearInterval(timer); - }, [saveOptions]); - - return ( -
-

Debug Info

-
-
-

Initial Options

-
-            {JSON.stringify(initialOptions, null, 2)}
-          
-
-
-

Realtime Options (2s refresh)

-
-            {JSON.stringify(realtimeOptions, null, 2)}
-          
-
-
-
- ); -} diff --git a/react/src/lib/grid-stack-context.ts b/react/src/lib/grid-stack-context.ts new file mode 100644 index 000000000..ef3a0011d --- /dev/null +++ b/react/src/lib/grid-stack-context.ts @@ -0,0 +1,33 @@ +import type { + GridStack, + GridStackElement, + GridStackOptions, + GridStackWidget, +} from "gridstack"; +import { createContext, useContext } from "react"; + +export interface GridStackContextType { + initialOptions: GridStackOptions; + addWidget: (widget: GridStackWidget) => void; + removeWidget: (el: GridStackElement) => void; + saveOptions: () => ReturnType | undefined; + + _gridStack: { + value: GridStack | null; + set: React.Dispatch>; + }; +} + +export const GridStackContext = createContext( + null +); + +export function useGridStackContext() { + const context = useContext(GridStackContext); + if (!context) { + throw new Error( + "useGridStackContext must be used within a GridStackProvider" + ); + } + return context; +} diff --git a/react/src/lib/grid-stack-handle-re-initializer.tsx b/react/src/lib/grid-stack-handle-re-initializer.tsx new file mode 100644 index 000000000..8d0f732ec --- /dev/null +++ b/react/src/lib/grid-stack-handle-re-initializer.tsx @@ -0,0 +1,56 @@ +import { PropsWithChildren, useLayoutEffect } from "react"; +import { useGridStackContext } from "./grid-stack-context"; +import { + GridItemHTMLElement, + GridStack, + GridStackNode, + Utils, +} from "gridstack"; +import { DDElementHost } from "gridstack/dist/dd-element"; +import { useGridStackItemContext } from "./grid-stack-item-context"; +import { useGridStackRenderContext } from "./grid-stack-render-context"; + +export type GridStackHandleReInitializerProps = PropsWithChildren; + +/** + * @experimental + * This is a temporary solution to reinitialize the handle for the grid stack item. + */ +export function GridStackHandleReInitializer( + props: GridStackHandleReInitializerProps +) { + const { + _gridStack: { value: gridStack }, + } = useGridStackContext(); + const { id: widgetId } = useGridStackItemContext(); + const { getWidgetContainer } = useGridStackRenderContext(); + + useLayoutEffect(() => { + if (gridStack) { + const widgetContainer = getWidgetContainer(widgetId); + if (widgetContainer) { + const element = Utils.getElement( + widgetContainer.parentElement! + ) as GridItemHTMLElement & DDElementHost; + const rawNode = element.gridstackNode; + const ddElement = element.ddElement; + if (rawNode && ddElement) { + // https://github.com/gridstack/gridstack.js/blob/a917afcada4bd2892963678c8b1bde7630bb9528/src/gridstack.ts#L2417 + const node = rawNode as GridStackNode & { _initDD: boolean }; + node._initDD = false; + + ddElement.cleanDraggable(); + ddElement.cleanDroppable(); + + // https://github.com/gridstack/gridstack.js/blob/a917afcada4bd2892963678c8b1bde7630bb9528/src/gridstack.ts#L2402 + const g = gridStack as GridStack & { + _prepareDragDropByNode: (node: GridStackNode) => void; + }; + g._prepareDragDropByNode(node); + } + } + } + }, [getWidgetContainer, gridStack, widgetId]); + + return <>{props.children}; +} diff --git a/react/src/lib/grid-stack-item-context.tsx b/react/src/lib/grid-stack-item-context.tsx new file mode 100644 index 000000000..728869225 --- /dev/null +++ b/react/src/lib/grid-stack-item-context.tsx @@ -0,0 +1,60 @@ +import { GridStackWidget } from "gridstack"; +import { createContext, useContext } from "react"; + +export type GridStackItemContextType = { + id: string; + + // Native methods + remove: () => void; + update: (opt: GridStackWidget) => void; + + // Extended methods + getBounds: () => { + current: { + x: number | undefined; + y: number | undefined; + w: number | undefined; + h: number | undefined; + }; + original: { + x: number | undefined; + y: number | undefined; + w: number | undefined; + h: number | undefined; + }; + } | null; + setSize: (size: { w: number; h: number }) => void; + setPosition: (position: { x: number; y: number }) => void; +}; + +export const GridStackItemContext = createContext({ + id: "", + remove: () => { + console.error("remove not implemented"); + }, + update: () => { + console.error("update not implemented"); + }, + getBounds: () => { + console.error("getBounds not implemented"); + return null; + }, + setSize: () => { + console.error("setSize not implemented"); + }, + setPosition: () => { + console.error("setPosition not implemented"); + }, +}); + +export function useGridStackItemContext() { + const context = useContext(GridStackItemContext); + + if (!context) { + throw new Error( + "useGridStackItemContext must be used within a GridStackItemContext" + ); + } + + return context; +} diff --git a/react/src/lib/grid-stack-item.tsx b/react/src/lib/grid-stack-item.tsx new file mode 100644 index 000000000..76a8a0f18 --- /dev/null +++ b/react/src/lib/grid-stack-item.tsx @@ -0,0 +1,100 @@ +import { PropsWithChildren, useCallback } from "react"; +import { useGridStackRenderContext } from "./grid-stack-render-context"; +import { createPortal } from "react-dom"; +import { GridStackItemContext } from "./grid-stack-item-context"; +import { useGridStackContext } from "./grid-stack-context"; +import { GridItemHTMLElement, GridStackWidget } from "gridstack"; + +export type GridStackItemProps = PropsWithChildren<{ + id: string; +}>; + +export function GridStackItem(props: GridStackItemProps) { + const renderContext = useGridStackRenderContext(); + const widgetContainer = renderContext.getWidgetContainer(props.id); + + const { removeWidget, _gridStack } = useGridStackContext(); + + const remove = useCallback(() => { + if (widgetContainer?.parentElement) { + removeWidget(widgetContainer.parentElement as GridItemHTMLElement); + } + }, [removeWidget, widgetContainer?.parentElement]); + + const update = useCallback( + (opt: GridStackWidget) => { + if (widgetContainer?.parentElement) { + _gridStack.value?.update(widgetContainer.parentElement, opt); + } + }, + [_gridStack.value, widgetContainer?.parentElement] + ); + + const getBounds = useCallback(() => { + const parentNode = widgetContainer?.parentElement; + if (parentNode) { + const widgetNode = parentNode as GridItemHTMLElement; + if (widgetNode.gridstackNode) { + const gridstackNode = widgetNode.gridstackNode; + return { + current: { + x: gridstackNode.x, + y: gridstackNode.y, + w: gridstackNode.w, + h: gridstackNode.h, + }, + original: { + x: gridstackNode.x, + y: gridstackNode.y, + w: gridstackNode.w, + h: gridstackNode.h, + }, + }; + } + } + return null; + }, [widgetContainer?.parentElement]); + + const setSize = useCallback( + (size: { w: number; h: number }) => { + if (widgetContainer?.parentElement) { + _gridStack.value?.update(widgetContainer.parentElement, { + w: size.w, + h: size.h, + }); + } + }, + [_gridStack.value, widgetContainer?.parentElement] + ); + const setPosition = useCallback( + (position: { x: number; y: number }) => { + if (widgetContainer?.parentElement) { + _gridStack.value?.update(widgetContainer.parentElement, { + x: position.x, + y: position.y, + }); + } + }, + [_gridStack.value, widgetContainer?.parentElement] + ); + + if (!widgetContainer) { + return null; + } + + return createPortal( + + {props.children} + , + widgetContainer + ); +} diff --git a/react/src/lib/grid-stack-provider.tsx b/react/src/lib/grid-stack-provider.tsx new file mode 100644 index 000000000..3fcd3777c --- /dev/null +++ b/react/src/lib/grid-stack-provider.tsx @@ -0,0 +1,56 @@ +import type { + GridStack, + GridStackElement, + GridStackOptions, + GridStackWidget, +} from "gridstack"; +import { type PropsWithChildren, useCallback, useState } from "react"; +import { GridStackContext } from "./grid-stack-context"; + +export type GridStackProviderProps = PropsWithChildren<{ + initialOptions: GridStackOptions; +}>; + +export function GridStackProvider({ + children, + initialOptions, +}: GridStackProviderProps) { + const [gridStack, setGridStack] = useState(null); + + const addWidget = useCallback( + (widget: GridStackWidget) => { + gridStack?.addWidget(widget); + }, + [gridStack] + ); + + const removeWidget = useCallback( + (el: GridStackElement) => { + gridStack?.removeWidget(el); + }, + [gridStack] + ); + + const saveOptions = useCallback(() => { + return gridStack?.save(true, true, (_, widget) => widget); + }, [gridStack]); + + return ( + + {children} + + ); +} diff --git a/react/src/lib/grid-stack-render-context.ts b/react/src/lib/grid-stack-render-context.ts new file mode 100644 index 000000000..26b0e0257 --- /dev/null +++ b/react/src/lib/grid-stack-render-context.ts @@ -0,0 +1,24 @@ +import { createContext, useContext } from "react"; + +export type GridStackRenderContextType = { + getWidgetContainer: (widgetId: string) => HTMLElement | null; +}; + +export const GridStackRenderContext = createContext( + { + getWidgetContainer: () => { + console.error("getWidgetContainer not implemented"); + return null; + }, + } +); + +export function useGridStackRenderContext() { + const context = useContext(GridStackRenderContext); + if (!context) { + throw new Error( + "useGridStackRenderContext must be used within a GridStackProvider" + ); + } + return context; +} diff --git a/react/src/lib/grid-stack-render.tsx b/react/src/lib/grid-stack-render.tsx new file mode 100644 index 000000000..4281a1507 --- /dev/null +++ b/react/src/lib/grid-stack-render.tsx @@ -0,0 +1,49 @@ +import { PropsWithChildren, useCallback, useLayoutEffect, useRef } from "react"; +import { useGridStackContext } from "./grid-stack-context"; +import { GridStack, GridStackOptions, GridStackWidget } from "gridstack"; +import { GridStackRenderContext } from "./grid-stack-render-context"; + +export type GridStackRenderProps = PropsWithChildren; + +export function GridStackRender({ children }: GridStackRenderProps) { + const { + _gridStack: { value: gridStack, set: setGridStack }, + initialOptions, + } = useGridStackContext(); + + const widgetContainersRef = useRef>(new Map()); + const containerRef = useRef(null); + const optionsRef = useRef(initialOptions); + + const initGrid = useCallback(() => { + if (containerRef.current) { + GridStack.renderCB = (element: HTMLElement, widget: GridStackWidget) => { + if (widget.id) { + widgetContainersRef.current.set(widget.id, element); + } + }; + return GridStack.init(optionsRef.current, containerRef.current); + } + return null; + }, []); + + useLayoutEffect(() => { + if (!gridStack) { + try { + setGridStack(initGrid()); + } catch (e) { + console.error("Error initializing gridstack", e); + } + } + }, [gridStack, initGrid, setGridStack]); + + const getWidgetContainer = useCallback((widgetId: string) => { + return widgetContainersRef.current.get(widgetId) || null; + }, []); + + return ( + +
{gridStack ? children : null}
+
+ ); +} diff --git a/react/src/lib/index.ts b/react/src/lib/index.ts new file mode 100644 index 000000000..c153d8ded --- /dev/null +++ b/react/src/lib/index.ts @@ -0,0 +1,26 @@ +export type { GridStackContextType } from "./grid-stack-context"; +export { GridStackContext, useGridStackContext } from "./grid-stack-context"; + +export type { GridStackProviderProps } from "./grid-stack-provider"; +export { GridStackProvider } from "./grid-stack-provider"; + +export type { GridStackRenderContextType } from "./grid-stack-render-context"; +export { + GridStackRenderContext, + useGridStackRenderContext, +} from "./grid-stack-render-context"; + +export type { GridStackRenderProps } from "./grid-stack-render"; +export { GridStackRender } from "./grid-stack-render"; + +export type { GridStackItemProps } from "./grid-stack-item"; +export { GridStackItem } from "./grid-stack-item"; + +export type { GridStackItemContextType } from "./grid-stack-item-context"; +export { + GridStackItemContext, + useGridStackItemContext, +} from "./grid-stack-item-context"; + +export type { GridStackHandleReInitializerProps } from "./grid-stack-handle-re-initializer"; +export { GridStackHandleReInitializer } from "./grid-stack-handle-re-initializer"; diff --git a/react/src/utils.ts b/react/src/utils.ts new file mode 100644 index 000000000..684f55996 --- /dev/null +++ b/react/src/utils.ts @@ -0,0 +1,3 @@ +export function newId() { + return `widget-${Math.random().toString(36).substring(2, 15)}`; +} diff --git a/react/vite.config.ts b/react/vite.config.ts index 861b04b35..711582204 100644 --- a/react/vite.config.ts +++ b/react/vite.config.ts @@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + server: { + host: true + }, +}); From 1ddf8a1b06fa9a60a92f404731a3ce040265914c Mon Sep 17 00:00:00 2001 From: CNine Date: Fri, 7 Feb 2025 14:04:57 +0800 Subject: [PATCH 2/8] Reorganize examples --- react/README.md | 4 +- react/lib/grid-stack-context.ts | 35 --- react/lib/grid-stack-provider.tsx | 113 --------- react/lib/grid-stack-render-context.ts | 15 -- react/lib/grid-stack-render-provider.tsx | 87 ------- react/lib/grid-stack-render.tsx | 69 ----- react/lib/grid-stack-widget-context.ts | 18 -- react/lib/index.ts | 19 -- react/src/App.tsx | 124 ++------- react/src/components/toolbar.tsx | 76 ------ react/src/default-grid-options.ts | 76 +----- react/src/examples/001-simple/index.tsx | 66 +++++ react/src/examples/002-nested/index.tsx | 135 ++++++++++ .../src/examples/003-custom-handle/index.tsx | 36 +++ .../009-advanced}/component-map.tsx | 6 +- .../009-advanced}/components/complex-card.tsx | 6 +- .../component-info-map-context.ts | 0 .../component-info-map-provider.tsx | 0 .../components/component-info-map/index.ts | 0 .../use-component-info-map.ts | 0 .../009-advanced/components/counter.tsx | 15 ++ .../examples/009-advanced/components/text.tsx | 7 + react/src/examples/009-advanced/index.tsx | 238 ++++++++++++++++++ react/src/lib/grid-stack-render.tsx | 18 +- react/src/main.tsx | 18 +- react/src/utils.ts | 2 +- 26 files changed, 550 insertions(+), 633 deletions(-) delete mode 100644 react/lib/grid-stack-context.ts delete mode 100644 react/lib/grid-stack-provider.tsx delete mode 100644 react/lib/grid-stack-render-context.ts delete mode 100644 react/lib/grid-stack-render-provider.tsx delete mode 100644 react/lib/grid-stack-render.tsx delete mode 100644 react/lib/grid-stack-widget-context.ts delete mode 100644 react/lib/index.ts delete mode 100644 react/src/components/toolbar.tsx create mode 100644 react/src/examples/001-simple/index.tsx create mode 100644 react/src/examples/002-nested/index.tsx create mode 100644 react/src/examples/003-custom-handle/index.tsx rename react/src/{ => examples/009-advanced}/component-map.tsx (77%) rename react/src/{ => examples/009-advanced}/components/complex-card.tsx (96%) rename react/src/{ => examples/009-advanced}/components/component-info-map/component-info-map-context.ts (100%) rename react/src/{ => examples/009-advanced}/components/component-info-map/component-info-map-provider.tsx (100%) rename react/src/{ => examples/009-advanced}/components/component-info-map/index.ts (100%) rename react/src/{ => examples/009-advanced}/components/component-info-map/use-component-info-map.ts (100%) create mode 100644 react/src/examples/009-advanced/components/counter.tsx create mode 100644 react/src/examples/009-advanced/components/text.tsx create mode 100644 react/src/examples/009-advanced/index.tsx diff --git a/react/README.md b/react/README.md index fa5239059..a213e6e00 100644 --- a/react/README.md +++ b/react/README.md @@ -12,7 +12,7 @@ Open in [CodeSandbox](https://codesandbox.io/p/sandbox/github/gridstack/gridstac - [x] Remove Widget - [x] Copy(Duplicate) Widget - [x] Custom handle -- [ ] Drag between two grid stacks +- [x] Drag between two grid stacks Welcome to give any suggestions and ideas, you can submit an issue or contact me by email. :) @@ -164,6 +164,8 @@ Render GridStack root container component. ```typescript type GridStackRenderProps = { + renderRawContent?: boolean; + children: React.ReactNode; }; ``` diff --git a/react/lib/grid-stack-context.ts b/react/lib/grid-stack-context.ts deleted file mode 100644 index 51e950700..000000000 --- a/react/lib/grid-stack-context.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { GridStack, GridStackOptions, GridStackWidget } from "gridstack"; -import { createContext, useContext } from "react"; - -export const GridStackContext = createContext<{ - initialOptions: GridStackOptions; - gridStack: GridStack | null; - addWidget: (fn: (id: string) => Omit) => void; - removeWidget: (id: string) => void; - addSubGrid: ( - fn: ( - id: string, - withWidget: (w: Omit) => GridStackWidget - ) => Omit - ) => void; - saveOptions: () => GridStackOptions | GridStackWidget[] | undefined; - - _gridStack: { - value: GridStack | null; - set: React.Dispatch>; - }; - _rawWidgetMetaMap: { - value: Map; - set: React.Dispatch>>; - }; -} | null>(null); - -export function useGridStackContext() { - const context = useContext(GridStackContext); - if (!context) { - throw new Error( - "useGridStackContext must be used within a GridStackProvider" - ); - } - return context; -} diff --git a/react/lib/grid-stack-provider.tsx b/react/lib/grid-stack-provider.tsx deleted file mode 100644 index 1afe4348c..000000000 --- a/react/lib/grid-stack-provider.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import type { GridStack, GridStackOptions, GridStackWidget } from "gridstack"; -import { type PropsWithChildren, useCallback, useState } from "react"; -import { GridStackContext } from "./grid-stack-context"; - -export function GridStackProvider({ - children, - initialOptions, -}: PropsWithChildren<{ initialOptions: GridStackOptions }>) { - const [gridStack, setGridStack] = useState(null); - const [rawWidgetMetaMap, setRawWidgetMetaMap] = useState(() => { - const map = new Map(); - const deepFindNodeWithContent = (obj: GridStackWidget) => { - if (obj.id && obj.content) { - map.set(obj.id, obj); - } - if (obj.subGridOpts?.children) { - obj.subGridOpts.children.forEach((child: GridStackWidget) => { - deepFindNodeWithContent(child); - }); - } - }; - initialOptions.children?.forEach((child: GridStackWidget) => { - deepFindNodeWithContent(child); - }); - return map; - }); - - const addWidget = useCallback( - (fn: (id: string) => Omit) => { - const newId = `widget-${Math.random().toString(36).substring(2, 15)}`; - const widget = fn(newId); - gridStack?.addWidget({ ...widget, id: newId }); - setRawWidgetMetaMap((prev) => { - const newMap = new Map(prev); - newMap.set(newId, widget); - return newMap; - }); - }, - [gridStack] - ); - - const addSubGrid = useCallback( - ( - fn: ( - id: string, - withWidget: (w: Omit) => GridStackWidget - ) => Omit - ) => { - const newId = `sub-grid-${Math.random().toString(36).substring(2, 15)}`; - const subWidgetIdMap = new Map(); - - const widget = fn(newId, (w) => { - const subWidgetId = `widget-${Math.random() - .toString(36) - .substring(2, 15)}`; - subWidgetIdMap.set(subWidgetId, w); - return { ...w, id: subWidgetId }; - }); - - gridStack?.addWidget({ ...widget, id: newId }); - - setRawWidgetMetaMap((prev) => { - const newMap = new Map(prev); - subWidgetIdMap.forEach((meta, id) => { - newMap.set(id, meta); - }); - return newMap; - }); - }, - [gridStack] - ); - - const removeWidget = useCallback( - (id: string) => { - gridStack?.removeWidget(id); - setRawWidgetMetaMap((prev) => { - const newMap = new Map(prev); - newMap.delete(id); - return newMap; - }); - }, - [gridStack] - ); - - const saveOptions = useCallback(() => { - return gridStack?.save(true, true, (_, widget) => widget); - }, [gridStack]); - - return ( - - {children} - - ); -} diff --git a/react/lib/grid-stack-render-context.ts b/react/lib/grid-stack-render-context.ts deleted file mode 100644 index 1135f8a44..000000000 --- a/react/lib/grid-stack-render-context.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext, useContext } from "react"; - -export const GridStackRenderContext = createContext<{ - getWidgetContainer: (widgetId: string) => HTMLElement | null; -} | null>(null); - -export function useGridStackRenderContext() { - const context = useContext(GridStackRenderContext); - if (!context) { - throw new Error( - "useGridStackRenderContext must be used within a GridStackProvider" - ); - } - return context; -} diff --git a/react/lib/grid-stack-render-provider.tsx b/react/lib/grid-stack-render-provider.tsx deleted file mode 100644 index 82c8e1f24..000000000 --- a/react/lib/grid-stack-render-provider.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { - PropsWithChildren, - useCallback, - useLayoutEffect, - useMemo, - useRef, -} from "react"; -import { useGridStackContext } from "./grid-stack-context"; -import { GridStack, GridStackOptions, GridStackWidget } from "gridstack"; -import { GridStackRenderContext } from "./grid-stack-render-context"; -import isEqual from "react-fast-compare"; - -export function GridStackRenderProvider({ children }: PropsWithChildren) { - const { - _gridStack: { value: gridStack, set: setGridStack }, - initialOptions, - } = useGridStackContext(); - - const widgetContainersRef = useRef>(new Map()); - const containerRef = useRef(null); - const optionsRef = useRef(initialOptions); - - const renderCBFn = useCallback( - (element: HTMLElement, widget: GridStackWidget) => { - if (widget.id) { - widgetContainersRef.current.set(widget.id, element); - } - }, - [] - ); - - const initGrid = useCallback(() => { - if (containerRef.current) { - GridStack.renderCB = renderCBFn; - return GridStack.init(optionsRef.current, containerRef.current); - // ! Change event not firing on nested grids (resize, move...) https://github.com/gridstack/gridstack.js/issues/2671 - // .on("change", () => { - // console.log("changed"); - // }) - // .on("resize", () => { - // console.log("resize"); - // }) - } - return null; - }, [renderCBFn]); - - useLayoutEffect(() => { - if (!isEqual(initialOptions, optionsRef.current) && gridStack) { - try { - gridStack.removeAll(false); - gridStack.destroy(false); - widgetContainersRef.current.clear(); - optionsRef.current = initialOptions; - setGridStack(initGrid()); - } catch (e) { - console.error("Error reinitializing gridstack", e); - } - } - }, [initialOptions, gridStack, initGrid, setGridStack]); - - useLayoutEffect(() => { - if (!gridStack) { - try { - setGridStack(initGrid()); - } catch (e) { - console.error("Error initializing gridstack", e); - } - } - }, [gridStack, initGrid, setGridStack]); - - return ( - ({ - getWidgetContainer: (widgetId: string) => { - return widgetContainersRef.current.get(widgetId) || null; - }, - }), - // ! gridStack is required to reinitialize the grid when the options change - // eslint-disable-next-line react-hooks/exhaustive-deps - [gridStack] - )} - > -
{gridStack ? children : null}
-
- ); -} diff --git a/react/lib/grid-stack-render.tsx b/react/lib/grid-stack-render.tsx deleted file mode 100644 index cf9a84893..000000000 --- a/react/lib/grid-stack-render.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { createPortal } from "react-dom"; -import { useGridStackContext } from "./grid-stack-context"; -import { useGridStackRenderContext } from "./grid-stack-render-context"; -import { GridStackWidgetContext } from "./grid-stack-widget-context"; -import { GridStackWidget } from "gridstack"; -import { ComponentType } from "react"; - -export interface ComponentDataType { - name: string; - props: T; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ComponentMap = Record>; - -function parseWeightMetaToComponentData( - meta: GridStackWidget -): ComponentDataType & { error: unknown } { - let error = null; - let name = ""; - let props = {}; - try { - if (meta.content) { - const result = JSON.parse(meta.content) as { - name: string; - props: object; - }; - name = result.name; - props = result.props; - } - } catch (e) { - error = e; - } - return { - name, - props, - error, - }; -} - -export function GridStackRender(props: { componentMap: ComponentMap }) { - const { _rawWidgetMetaMap } = useGridStackContext(); - const { getWidgetContainer } = useGridStackRenderContext(); - - return ( - <> - {Array.from(_rawWidgetMetaMap.value.entries()).map(([id, meta]) => { - const componentData = parseWeightMetaToComponentData(meta); - - const WidgetComponent = props.componentMap[componentData.name]; - - const widgetContainer = getWidgetContainer(id); - - if (!widgetContainer) { - throw new Error(`Widget container not found for id: ${id}`); - } - - return ( - - {createPortal( - , - widgetContainer - )} - - ); - })} - - ); -} diff --git a/react/lib/grid-stack-widget-context.ts b/react/lib/grid-stack-widget-context.ts deleted file mode 100644 index 14ee1c65f..000000000 --- a/react/lib/grid-stack-widget-context.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createContext, useContext } from "react"; - -// TODO: support full widget metadata -export const GridStackWidgetContext = createContext<{ - widget: { - id: string; - }; -} | null>(null); - -export function useGridStackWidgetContext() { - const context = useContext(GridStackWidgetContext); - if (!context) { - throw new Error( - "useGridStackWidgetContext must be used within a GridStackWidgetProvider" - ); - } - return context; -} diff --git a/react/lib/index.ts b/react/lib/index.ts deleted file mode 100644 index e5984f371..000000000 --- a/react/lib/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GridStackProvider } from "./grid-stack-provider"; -import { GridStackRenderProvider } from "./grid-stack-render-provider"; -import { - GridStackRender, - ComponentDataType, - ComponentMap, -} from "./grid-stack-render"; -import { useGridStackContext } from "./grid-stack-context"; -import { useGridStackWidgetContext } from "./grid-stack-widget-context"; - -export { - GridStackProvider, - GridStackRenderProvider, - GridStackRender, - type ComponentDataType, - type ComponentMap, - useGridStackContext, - useGridStackWidgetContext, -}; diff --git a/react/src/App.tsx b/react/src/App.tsx index 16166f868..36c7569d7 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -1,114 +1,22 @@ -import { useState } from "react"; - import "./demo.css"; - -import { - GridStackHandleReInitializer, - GridStackItem, - GridStackProvider, - GridStackRender, -} from "./lib"; -import { GridStackOptions } from "gridstack"; -import { - CUSTOM_DRAGGABLE_HANDLE_CLASSNAME, - defaultGridOptions, -} from "./default-grid-options"; -import { COMPONENT_MAP, ComponentInfo } from "./component-map"; -import { ComplexCardEditableWrapper } from "./components/complex-card"; -import { - ComponentInfoMapProvider, - useComponentInfoMap, -} from "./components/component-info-map"; -import { DebugInfo } from "./components/debug-info"; -import { Toolbar } from "./components/toolbar"; +import { Simple } from "./examples/001-simple"; +import { Nested } from "./examples/002-nested"; +import { CustomHandle } from "./examples/003-custom-handle"; +import { Advanced } from "./examples/009-advanced"; export default function App() { - const [uncontrolledInitialOptions] = - useState(defaultGridOptions); - - const [initialComponentInfoMap] = useState>( - () => ({ - item3: { component: "Text", serializableProps: { content: "Text" } }, - item4: { - component: "Button", - serializableProps: { label: "Click me" }, - }, - item999: { - component: "ComplexCard", - serializableProps: { title: "Complex Card", color: "red" }, - }, - }) - ); - - return ( - - - - - - {/* Simple: Render item with id selector */} - -
hello
-
- - -
grid
-
- - {/* Advanced: Render item with widget map component info */} - - - {/* Experimental: Render item with custom handle */} - - - - - -
- - -
-
- ); -} - -export function DynamicGridStackItems() { - const { componentInfoMap } = useComponentInfoMap(); - return ( - <> - {Array.from(componentInfoMap.entries()).map( - ([widgetId, componentInfo]) => { - const Component = COMPONENT_MAP[componentInfo.component]; - if (!Component) { - throw new Error(`Component ${componentInfo.component} not found`); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const props = componentInfo.serializableProps as any; - - if (componentInfo.component === "ComplexCard") { - return ( - - - - - - ); - } - - return ( - - - - ); - } - )} - +
+

Simple

+

Render content by GridStackItem with id selector.

+ +

Nested

+

Only use gridstack.js native subGridOpts.

+ +

Custom Handle

+ +

Advanced

+ +
); } diff --git a/react/src/components/toolbar.tsx b/react/src/components/toolbar.tsx deleted file mode 100644 index e734504d0..000000000 --- a/react/src/components/toolbar.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { BREAKPOINTS, CELL_HEIGHT } from "../default-grid-options"; -import { useGridStackContext } from "../lib"; -import { newId } from "../utils"; -import { useComponentInfoMap } from "./component-info-map"; - -export function Toolbar() { - const { addWidget } = useGridStackContext(); - const { addComponentInfo } = useComponentInfoMap(); - - return ( -
- - - -
- ); -} diff --git a/react/src/default-grid-options.ts b/react/src/default-grid-options.ts index a67fe163a..1297cb6d2 100644 --- a/react/src/default-grid-options.ts +++ b/react/src/default-grid-options.ts @@ -30,78 +30,8 @@ export const defaultGridOptions: GridStackOptions = { margin: 8, minRow: 2, cellHeight: CELL_HEIGHT, + alwaysShowResizeHandle: false, + column: "auto", + layout: "list", }, - children: [ - { - id: "item1", - h: 2, - w: 2, - x: 0, - y: 0, - }, - { - id: "item2", - h: 2, - w: 2, - x: 2, - y: 0, - }, - { - id: "sub-grid-1", - h: 5, - sizeToContent: true, - subGridOpts: { - acceptWidgets: true, - cellHeight: CELL_HEIGHT, - alwaysShowResizeHandle: false, - column: "auto", - minRow: 2, - layout: "list", - margin: 8, - children: [ - { - id: "sub-grid-1-title", - locked: true, - noMove: true, - noResize: true, - w: 12, - x: 0, - y: 0, - content: "Sub Grid 1", - }, - { - id: "item3", - h: 2, - w: 2, - x: 0, - y: 1, - }, - { - id: "item4", - h: 2, - w: 2, - x: 2, - y: 0, - }, - ], - }, - w: 12, - x: 0, - y: 2, - }, - { - id: "item5", - w: 2, - h: 2, - x: 0, - y: 2, - }, - { - id: "item999", - w: 4, - h: 4, - x: 0, - y: 2, - }, - ], }; diff --git a/react/src/examples/001-simple/index.tsx b/react/src/examples/001-simple/index.tsx new file mode 100644 index 000000000..ee7d83843 --- /dev/null +++ b/react/src/examples/001-simple/index.tsx @@ -0,0 +1,66 @@ +import { GridStackOptions } from "gridstack"; +import { useState } from "react"; +import { defaultGridOptions } from "../../default-grid-options"; +import { + GridStackItem, + GridStackProvider, + GridStackRender, + useGridStackContext, +} from "../../lib"; +import { newId } from "../../utils"; + +export function Simple() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [ + { id: "item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + ], + })); + + return ( + + + + + +
hello
+
+ + +
grid
+
+
+
+ ); +} + +export function Toolbar() { + const { addWidget } = useGridStackContext(); + + function handleAddText(w: number, h: number) { + const widgetId = newId(); + addWidget({ id: widgetId, w, h, x: 0, y: 0, content: "text-" + widgetId }); + } + + return ( +
+ +
+ ); +} diff --git a/react/src/examples/002-nested/index.tsx b/react/src/examples/002-nested/index.tsx new file mode 100644 index 000000000..122ac1402 --- /dev/null +++ b/react/src/examples/002-nested/index.tsx @@ -0,0 +1,135 @@ +import { GridStackOptions } from "gridstack"; +import { useState } from "react"; +import { defaultGridOptions } from "../../default-grid-options"; +import { + GridStackItem, + GridStackProvider, + GridStackRender, + useGridStackContext, +} from "../../lib"; +import { newId } from "../../utils"; + +export function Nested() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [ + { id: "item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + { + id: "sub-grid-1", + h: 5, + sizeToContent: true, + subGridOpts: { + children: [ + { + id: "sub-grid-1-title", + locked: true, + noMove: true, + noResize: true, + w: 12, + x: 0, + y: 0, + content: "Sub Grid 1", + }, + { id: "item3", h: 2, w: 2, x: 0, y: 1 }, + { id: "item4", h: 2, w: 2, x: 2, y: 0 }, + ], + }, + w: 12, + x: 0, + y: 2, + }, + ], + })); + + return ( + + + + + +
hello
+
+ + +
grid
+
+ + +
nested one
+
+ + +
nested two
+
+
+
+ ); +} + +export function Toolbar() { + const { addWidget } = useGridStackContext(); + + function handleAddText(w: number, h: number) { + const widgetId = newId(); + addWidget({ id: widgetId, w, h, x: 0, y: 0, content: "text-" + widgetId }); + } + + function handleAddSubGrid() { + const subGridId = newId(); + const item1Id = newId(); + const item2Id = newId(); + addWidget({ + id: "sub-grid-" + subGridId, + h: 5, + sizeToContent: true, + subGridOpts: { + children: [ + { + id: "sub-grid-" + subGridId + "-title", + locked: true, + noMove: true, + noResize: true, + w: 12, + x: 0, + y: 0, + content: "Sub Grid " + subGridId, + }, + { id: item1Id, h: 2, w: 2, x: 0, y: 1, content: "item" + item1Id }, + { id: item2Id, h: 2, w: 2, x: 2, y: 0, content: "item" + item2Id }, + ], + }, + w: 4, + x: 0, + y: 0, + }); + } + + return ( +
+ + +
+ ); +} diff --git a/react/src/examples/003-custom-handle/index.tsx b/react/src/examples/003-custom-handle/index.tsx new file mode 100644 index 000000000..040592cca --- /dev/null +++ b/react/src/examples/003-custom-handle/index.tsx @@ -0,0 +1,36 @@ +import { GridStackOptions } from "gridstack"; +import { useState } from "react"; +import { + CUSTOM_DRAGGABLE_HANDLE_CLASSNAME, + defaultGridOptions, +} from "../../default-grid-options"; +import { + GridStackHandleReInitializer, + GridStackItem, + GridStackProvider, + GridStackRender, +} from "../../lib"; + +export function CustomHandle() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [{ id: "item1", h: 2, w: 2, x: 0, y: 0 }], + })); + + return ( + + + +
Custom Handle
+ + {/* Experimental: Render item with custom handle */} + + + +
+
+
+ ); +} diff --git a/react/src/component-map.tsx b/react/src/examples/009-advanced/component-map.tsx similarity index 77% rename from react/src/component-map.tsx rename to react/src/examples/009-advanced/component-map.tsx index 587686e06..598b0c648 100644 --- a/react/src/component-map.tsx +++ b/react/src/examples/009-advanced/component-map.tsx @@ -1,9 +1,11 @@ import { ComponentProps } from "react"; import { ComplexCard } from "./components/complex-card"; +import { Counter } from "./components/counter"; +import { Text } from "./components/text"; export const COMPONENT_MAP = { - Text: (props: { content: string }) =>
{props.content}
, - Button: (props: { label: string }) => , + Text, + Counter, ComplexCard, // ... more components here }; diff --git a/react/src/components/complex-card.tsx b/react/src/examples/009-advanced/components/complex-card.tsx similarity index 96% rename from react/src/components/complex-card.tsx rename to react/src/examples/009-advanced/components/complex-card.tsx index 31077c812..056e6794c 100644 --- a/react/src/components/complex-card.tsx +++ b/react/src/examples/009-advanced/components/complex-card.tsx @@ -3,9 +3,9 @@ import { GridStackHandleReInitializer, useGridStackContext, useGridStackItemContext, -} from "../lib"; -import { newId } from "../utils"; -import { CUSTOM_DRAGGABLE_HANDLE_CLASSNAME } from "../default-grid-options"; +} from "../../../lib"; +import { newId } from "../../../utils"; +import { CUSTOM_DRAGGABLE_HANDLE_CLASSNAME } from "../../../default-grid-options"; import { useComponentInfoMap } from "./component-info-map"; type ComplexCardProps = { diff --git a/react/src/components/component-info-map/component-info-map-context.ts b/react/src/examples/009-advanced/components/component-info-map/component-info-map-context.ts similarity index 100% rename from react/src/components/component-info-map/component-info-map-context.ts rename to react/src/examples/009-advanced/components/component-info-map/component-info-map-context.ts diff --git a/react/src/components/component-info-map/component-info-map-provider.tsx b/react/src/examples/009-advanced/components/component-info-map/component-info-map-provider.tsx similarity index 100% rename from react/src/components/component-info-map/component-info-map-provider.tsx rename to react/src/examples/009-advanced/components/component-info-map/component-info-map-provider.tsx diff --git a/react/src/components/component-info-map/index.ts b/react/src/examples/009-advanced/components/component-info-map/index.ts similarity index 100% rename from react/src/components/component-info-map/index.ts rename to react/src/examples/009-advanced/components/component-info-map/index.ts diff --git a/react/src/components/component-info-map/use-component-info-map.ts b/react/src/examples/009-advanced/components/component-info-map/use-component-info-map.ts similarity index 100% rename from react/src/components/component-info-map/use-component-info-map.ts rename to react/src/examples/009-advanced/components/component-info-map/use-component-info-map.ts diff --git a/react/src/examples/009-advanced/components/counter.tsx b/react/src/examples/009-advanced/components/counter.tsx new file mode 100644 index 000000000..9e49d0dfd --- /dev/null +++ b/react/src/examples/009-advanced/components/counter.tsx @@ -0,0 +1,15 @@ +import { useState } from "react"; + +type CounterProps = { + label: string; +}; + +export function Counter(props: CounterProps) { + const [count, setCount] = useState(0); + + return ( + + ); +} diff --git a/react/src/examples/009-advanced/components/text.tsx b/react/src/examples/009-advanced/components/text.tsx new file mode 100644 index 000000000..7200c33d1 --- /dev/null +++ b/react/src/examples/009-advanced/components/text.tsx @@ -0,0 +1,7 @@ +type TextProps = { + content: string; +}; + +export function Text(props: TextProps) { + return
{props.content}
; +} diff --git a/react/src/examples/009-advanced/index.tsx b/react/src/examples/009-advanced/index.tsx new file mode 100644 index 000000000..6c75bbcf2 --- /dev/null +++ b/react/src/examples/009-advanced/index.tsx @@ -0,0 +1,238 @@ +import { useState } from "react"; + +import { + GridStackItem, + GridStackProvider, + GridStackRender, + useGridStackContext, +} from "../../lib"; +import { GridStackOptions } from "gridstack"; +import { defaultGridOptions } from "../../default-grid-options"; +import { COMPONENT_MAP, ComponentInfo } from "./component-map"; +import { ComplexCardEditableWrapper } from "./components/complex-card"; +import { + ComponentInfoMapProvider, + useComponentInfoMap, +} from "./components/component-info-map"; +import { newId } from "../../utils"; + +export function Advanced() { + // Data about layout by gridstack option + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [ + { id: "item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + { + id: "sub-grid-1", + h: 5, + sizeToContent: true, + subGridOpts: { + children: [ + { + id: "sub-grid-1-title", + locked: true, + noMove: true, + noResize: true, + w: 12, + x: 0, + y: 0, + content: "Sub Grid 1", + }, + { id: "item3", h: 2, w: 2, x: 0, y: 1 }, + { id: "item4", h: 2, w: 2, x: 2, y: 0 }, + ], + }, + w: 4, + x: 0, + y: 2, + }, + { id: "item5", w: 4, h: 4, x: 0, y: 2 }, + ], + })); + + // Data about every content + const [initialComponentInfoMap] = useState>( + () => ({ + item1: { component: "Text", serializableProps: { content: "Text" } }, + item2: { component: "Text", serializableProps: { content: "Text" } }, + "sub-grid-1-title": { + component: "Text", + serializableProps: { content: "Sub Grid 1" }, + }, + item3: { component: "Text", serializableProps: { content: "Text" } }, + item4: { + component: "Counter", + serializableProps: { label: "Click me" }, + }, + item5: { + component: "ComplexCard", + serializableProps: { title: "Complex Card", color: "red" }, + }, + }) + ); + + return ( + + + + + + + + + + ); +} + +function DynamicGridStackItems() { + const { componentInfoMap } = useComponentInfoMap(); + + return ( + <> + {Array.from(componentInfoMap.entries()).map( + ([widgetId, componentInfo]) => { + const Component = COMPONENT_MAP[componentInfo.component]; + if (!Component) { + throw new Error(`Component ${componentInfo.component} not found`); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const props = componentInfo.serializableProps as any; + + if (componentInfo.component === "ComplexCard") { + return ( + + + + + + ); + } + + // ... more render conditions here + + return ( + + + + ); + } + )} + + ); +} + +export function Toolbar() { + const { addWidget } = useGridStackContext(); + const { addComponentInfo } = useComponentInfoMap(); + + function handleAddText(w: number, h: number) { + const widgetId = newId(); + + // Add item to layout + addWidget({ id: widgetId, w, h, x: 0, y: 0 }); + + // Add component to item + addComponentInfo(widgetId, { + component: "Text", + serializableProps: { content: "Text " + widgetId }, + }); + } + + function handleAddSubGrid() { + const subGridId = newId(); + const subGridTitleId = newId(); + const item1Id = newId(); + const item2Id = newId(); + + addWidget({ + id: subGridId, + h: 5, + sizeToContent: true, + subGridOpts: { + children: [ + { + id: subGridTitleId, + locked: true, + noMove: true, + noResize: true, + w: 12, + x: 0, + y: 0, + }, + { id: item1Id, h: 2, w: 2, x: 0, y: 1 }, + { id: item2Id, h: 2, w: 2, x: 2, y: 0 }, + ], + }, + w: 4, + x: 0, + y: 0, + }); + + addComponentInfo(subGridTitleId, { + component: "Text", + serializableProps: { content: "Sub Grid " + subGridId }, + }); + + addComponentInfo(item1Id, { + component: "Text", + serializableProps: { content: "Item " + item1Id }, + }); + + addComponentInfo(item2Id, { + component: "Text", + serializableProps: { content: "Item " + item2Id }, + }); + } + + function handleAddComplexCard() { + const widgetId = newId(); + + addWidget({ id: widgetId, w: 4, h: 4 }); // No position + + addComponentInfo(widgetId, { + component: "ComplexCard", + serializableProps: { title: "Complex Card", color: "red" }, + }); + } + + return ( +
+ + + + +
+ ); +} diff --git a/react/src/lib/grid-stack-render.tsx b/react/src/lib/grid-stack-render.tsx index 4281a1507..88b1a39cb 100644 --- a/react/src/lib/grid-stack-render.tsx +++ b/react/src/lib/grid-stack-render.tsx @@ -3,9 +3,14 @@ import { useGridStackContext } from "./grid-stack-context"; import { GridStack, GridStackOptions, GridStackWidget } from "gridstack"; import { GridStackRenderContext } from "./grid-stack-render-context"; -export type GridStackRenderProps = PropsWithChildren; +export type GridStackRenderProps = PropsWithChildren<{ + renderRawContent?: boolean; +}>; -export function GridStackRender({ children }: GridStackRenderProps) { +export function GridStackRender({ + children, + renderRawContent = false, +}: GridStackRenderProps) { const { _gridStack: { value: gridStack, set: setGridStack }, initialOptions, @@ -21,11 +26,18 @@ export function GridStackRender({ children }: GridStackRenderProps) { if (widget.id) { widgetContainersRef.current.set(widget.id, element); } + + // ! Only as a fallback, if content is not set in the widget + if (renderRawContent) { + if (widget.content) { + element.innerHTML = widget.content; + } + } }; return GridStack.init(optionsRef.current, containerRef.current); } return null; - }, []); + }, [renderRawContent]); useLayoutEffect(() => { if (!gridStack) { diff --git a/react/src/main.tsx b/react/src/main.tsx index 0bf19b8ce..a874241f6 100644 --- a/react/src/main.tsx +++ b/react/src/main.tsx @@ -1,15 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' +import "gridstack/dist/gridstack-extra.css"; +import "gridstack/dist/gridstack.css"; -import 'gridstack/dist/gridstack-extra.css'; -import 'gridstack/dist/gridstack.css'; +import App from "./App.tsx"; -import App from './App.tsx' - - -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( - , -) + +); diff --git a/react/src/utils.ts b/react/src/utils.ts index 684f55996..0342b9c51 100644 --- a/react/src/utils.ts +++ b/react/src/utils.ts @@ -1,3 +1,3 @@ export function newId() { - return `widget-${Math.random().toString(36).substring(2, 15)}`; + return `${Math.random().toString(36).substring(2, 15)}`; } From b63cfd083b1afc2aa38fcc29e7d8d83f6409f7f8 Mon Sep 17 00:00:00 2001 From: CNine Date: Fri, 7 Feb 2025 14:12:43 +0800 Subject: [PATCH 3/8] Update react docs --- react/README.md | 103 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/react/README.md b/react/README.md index a213e6e00..85c6ebc7c 100644 --- a/react/README.md +++ b/react/README.md @@ -18,31 +18,35 @@ Welcome to give any suggestions and ideas, you can submit an issue or contact me ## Usage +This is not an npm package, it's just a demo project. Please copy the `src/lib` code to your project to use it. + **Simple** Render item with widget id selector. +Code here: [src/examples/001-simple/index.tsx](src/examples/001-simple/index.tsx) + ```tsx -function App() { - const [uncontrolledInitialOptions] = useState({ - // ... +function Simple() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, children: [ { id: "item1", h: 2, w: 2, x: 0, y: 0 }, { id: "item2", h: 2, w: 2, x: 2, y: 0 }, ], - }); + })); return ( - + -
hello
+
hello
-
grid
+
grid
@@ -56,20 +60,59 @@ Render item with widget map component info. _ComponentInfoMap is just an example, you can use any way you want to store and retrieve component information._ +Code here: [src/examples/009-advanced/index.tsx](src/examples/009-advanced/index.tsx) + ```tsx -function App() { - const [uncontrolledInitialOptions] = useState({ - // ... +function Advanced() { + // Data about layout by gridstack option + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, children: [ { id: "item1", h: 2, w: 2, x: 0, y: 0 }, { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + { + id: "sub-grid-1", + h: 5, + sizeToContent: true, + subGridOpts: { + children: [ + { + id: "sub-grid-1-title", + locked: true, + noMove: true, + noResize: true, + w: 12, + x: 0, + y: 0, + content: "Sub Grid 1", + }, + { id: "item3", h: 2, w: 2, x: 0, y: 1 }, + { id: "item4", h: 2, w: 2, x: 2, y: 0 }, + ], + }, + w: 4, + x: 0, + y: 2, + }, + { id: "item5", w: 4, h: 4, x: 0, y: 2 }, ], - }); + })); + // Data about every content const [initialComponentInfoMap] = useState>( () => ({ item1: { component: "Text", serializableProps: { content: "Text" } }, - item2: { + item2: { component: "Text", serializableProps: { content: "Text" } }, + "sub-grid-1-title": { + component: "Text", + serializableProps: { content: "Sub Grid 1" }, + }, + item3: { component: "Text", serializableProps: { content: "Text" } }, + item4: { + component: "Counter", + serializableProps: { label: "Click me" }, + }, + item5: { component: "ComplexCard", serializableProps: { title: "Complex Card", color: "red" }, }, @@ -89,7 +132,7 @@ function App() { ); } -export function DynamicGridStackItems() { +function DynamicGridStackItems() { const { componentInfoMap } = useComponentInfoMap(); return ( @@ -117,6 +160,8 @@ export function DynamicGridStackItems() { ); } + // ... more render conditions here + return ( @@ -133,14 +178,32 @@ export function DynamicGridStackItems() { Render item with custom handle. +Code here: [src/examples/003-custom-handle/index.tsx](src/examples/003-custom-handle/index.tsx) + ```tsx - - - - - +function CustomHandle() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [{ id: "item1", h: 2, w: 2, x: 0, y: 0 }], + })); + + return ( + + + +
Custom Handle
+ + {/* Experimental: Render item with custom handle */} + + + +
+
+
+ ); +} ``` ## API Reference From 94a994f32c4063238c9623e4ef85ea0bf0871d69 Mon Sep 17 00:00:00 2001 From: CNine Date: Sat, 8 Feb 2025 12:11:32 +0800 Subject: [PATCH 4/8] Add the GridStackContainer and update docs --- react/README.md | 45 +++++++++++++++++++++++-- react/src/App.tsx | 6 +++- react/src/examples/000-simple/index.tsx | 30 +++++++++++++++++ react/src/lib/grid-stack-container.tsx | 23 +++++++++++++ react/src/lib/index.ts | 3 ++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 react/src/examples/000-simple/index.tsx create mode 100644 react/src/lib/grid-stack-container.tsx diff --git a/react/README.md b/react/README.md index 85c6ebc7c..58e502407 100644 --- a/react/README.md +++ b/react/README.md @@ -14,8 +14,6 @@ Open in [CodeSandbox](https://codesandbox.io/p/sandbox/github/gridstack/gridstac - [x] Custom handle - [x] Drag between two grid stacks -Welcome to give any suggestions and ideas, you can submit an issue or contact me by email. :) - ## Usage This is not an npm package, it's just a demo project. Please copy the `src/lib` code to your project to use it. @@ -24,6 +22,37 @@ This is not an npm package, it's just a demo project. Please copy the `src/lib` Render item with widget id selector. +Code here: [src/examples/000-simple/index.tsx](src/examples/000-simple/index.tsx) + +```tsx +function Simple() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [ + { id: "item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + ], + })); + + return ( + + +
hello
+
+ + +
grid
+
+
+ ); +} +``` + +Or split the grid stack container to provide grid stack context and render component for access to grid stack context. + Code here: [src/examples/001-simple/index.tsx](src/examples/001-simple/index.tsx) ```tsx @@ -38,6 +67,7 @@ function Simple() { return ( + {/* Custom toolbar component. Access to grid stack context by useGridStackContext hook. */} @@ -210,6 +240,17 @@ function CustomHandle() { ### Components +#### GridStackContainer + +Top-level component that provides GridStack context and GridStack root container. Equivalent to `GridStackProvider` and `GridStackRender` combined. + +```typescript +type GridStackContainerProps = { + initialOptions: GridStackOptions; // GridStack initialization options + children: React.ReactNode; +}; +``` + #### GridStackProvider Top-level component that provides GridStack context. diff --git a/react/src/App.tsx b/react/src/App.tsx index 36c7569d7..53abfe829 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -1,4 +1,5 @@ import "./demo.css"; +import { Simple0 } from "./examples/000-simple"; import { Simple } from "./examples/001-simple"; import { Nested } from "./examples/002-nested"; import { CustomHandle } from "./examples/003-custom-handle"; @@ -7,8 +8,11 @@ import { Advanced } from "./examples/009-advanced"; export default function App() { return (
-

Simple

+

Simple

Render content by GridStackItem with id selector.

+ +

Simple With Toolbar

+

With toolbar

Nested

Only use gridstack.js native subGridOpts.

diff --git a/react/src/examples/000-simple/index.tsx b/react/src/examples/000-simple/index.tsx new file mode 100644 index 000000000..9870a5914 --- /dev/null +++ b/react/src/examples/000-simple/index.tsx @@ -0,0 +1,30 @@ +import { GridStackOptions } from "gridstack"; +import { useState } from "react"; +import { defaultGridOptions } from "../../default-grid-options"; +import { GridStackItem } from "../../lib"; +import { GridStackContainer } from "../../lib/grid-stack-container"; + +export function Simple0() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [ + { id: "item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + ], + })); + + return ( + + +
hello
+
+ + +
grid
+
+
+ ); +} diff --git a/react/src/lib/grid-stack-container.tsx b/react/src/lib/grid-stack-container.tsx new file mode 100644 index 000000000..c5d288abe --- /dev/null +++ b/react/src/lib/grid-stack-container.tsx @@ -0,0 +1,23 @@ +import { GridStackOptions } from "gridstack"; +import { PropsWithChildren } from "react"; +import { GridStackProvider } from "./grid-stack-provider"; +import { GridStackRender } from "./grid-stack-render"; + +export type GridStackContainerProps = PropsWithChildren<{ + initialOptions: GridStackOptions; + renderRawContent?: boolean; +}>; + +export function GridStackContainer({ + children, + initialOptions, + renderRawContent, +}: GridStackContainerProps) { + return ( + + + {children} + + + ); +} diff --git a/react/src/lib/index.ts b/react/src/lib/index.ts index c153d8ded..f5d514dd6 100644 --- a/react/src/lib/index.ts +++ b/react/src/lib/index.ts @@ -1,3 +1,6 @@ +export type { GridStackContainerProps } from "./grid-stack-container"; +export { GridStackContainer } from "./grid-stack-container"; + export type { GridStackContextType } from "./grid-stack-context"; export { GridStackContext, useGridStackContext } from "./grid-stack-context"; From 44c029a445cf2cd3a4264b349d9fb4dea9db229e Mon Sep 17 00:00:00 2001 From: CNine Date: Sat, 8 Feb 2025 12:12:03 +0800 Subject: [PATCH 5/8] Rename file ext name --- .../{grid-stack-item-context.tsx => grid-stack-item-context.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename react/src/lib/{grid-stack-item-context.tsx => grid-stack-item-context.ts} (100%) diff --git a/react/src/lib/grid-stack-item-context.tsx b/react/src/lib/grid-stack-item-context.ts similarity index 100% rename from react/src/lib/grid-stack-item-context.tsx rename to react/src/lib/grid-stack-item-context.ts From 8d90811cc309ea234e049200905106fa0c396828 Mon Sep 17 00:00:00 2001 From: CNine Date: Sun, 2 Mar 2025 10:18:51 +0800 Subject: [PATCH 6/8] Fix unique id and Add drag in item --- react/README.md | 83 +++++++++++++++++-- react/src/App.tsx | 3 + react/src/examples/000-simple/index.tsx | 13 ++- react/src/examples/001-simple/index.tsx | 10 +-- react/src/examples/002-nested/index.tsx | 22 ++--- .../src/examples/003-custom-handle/index.tsx | 6 +- react/src/examples/004-drag-in/index.tsx | 52 ++++++++++++ react/src/examples/009-advanced/index.tsx | 71 +++++++++++++--- react/src/lib/global.ts | 10 +++ react/src/lib/grid-stack-container.tsx | 6 +- react/src/lib/grid-stack-drag-in-item.tsx | 77 +++++++++++++++++ .../lib/grid-stack-handle-re-initializer.tsx | 6 +- react/src/lib/grid-stack-item.tsx | 2 +- react/src/lib/grid-stack-render-context.ts | 6 +- react/src/lib/grid-stack-render.tsx | 48 +++++------ react/src/lib/index.ts | 3 + 16 files changed, 330 insertions(+), 88 deletions(-) create mode 100644 react/src/examples/004-drag-in/index.tsx create mode 100644 react/src/lib/global.ts create mode 100644 react/src/lib/grid-stack-drag-in-item.tsx diff --git a/react/README.md b/react/README.md index 58e502407..64d227a04 100644 --- a/react/README.md +++ b/react/README.md @@ -35,10 +35,7 @@ function Simple() { })); return ( - +
hello
@@ -70,7 +67,7 @@ function Simple() { {/* Custom toolbar component. Access to grid stack context by useGridStackContext hook. */} - +
hello
@@ -84,6 +81,61 @@ function Simple() { } ``` +**Drag In** + +Drag items from outside into the grid. + +Code here: [src/examples/004-drag-in/index.tsx](src/examples/004-drag-in/index.tsx) + +```tsx +function DragIn() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [ + { id: "004-item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "004-item2", h: 2, w: 2, x: 2, y: 0 }, + ], + })); + + return ( +
+
+ +
+ Drag me add to the grid +
+
+
+ + + +
hello
+
+ + +
grid
+
+
+
+ ); +} +``` + **Advanced** Render item with widget map component info. @@ -219,7 +271,7 @@ function CustomHandle() { return ( - +
Custom Handle
@@ -268,8 +320,6 @@ Render GridStack root container component. ```typescript type GridStackRenderProps = { - renderRawContent?: boolean; - children: React.ReactNode; }; ``` @@ -295,6 +345,20 @@ type GridStackHandleReInitializerProps = { }; ``` +#### GridStackDragInItem + +Experimental component for dragging items from outside into the grid. + +```typescript +type GridStackDragInItemProps = { + widget: Omit; // Widget configuration without content + dragOptions?: DDDragOpt; // Drag options + content?: ReactNode; // Optional content to render in the dragged clone + children: React.ReactNode; + // Plus other div props +}; +``` + ### Contexts #### GridStackContext @@ -342,7 +406,7 @@ Provide rendering related functionality context. ```typescript type GridStackRenderContextType = { - getWidgetContainer: (widgetId: string) => HTMLElement | null; + getContainerByWidgetId: (widgetId: string) => HTMLElement | null; }; ``` @@ -383,5 +447,6 @@ export type { GridStackItemProps, GridStackItemContextType, GridStackHandleReInitializerProps, + GridStackDragInItemProps, }; ``` diff --git a/react/src/App.tsx b/react/src/App.tsx index 53abfe829..90d140894 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -4,6 +4,7 @@ import { Simple } from "./examples/001-simple"; import { Nested } from "./examples/002-nested"; import { CustomHandle } from "./examples/003-custom-handle"; import { Advanced } from "./examples/009-advanced"; +import { DragIn } from "./examples/004-drag-in"; export default function App() { return ( @@ -19,6 +20,8 @@ export default function App() {

Custom Handle

+

Drag In (Copy)

+

Advanced

diff --git a/react/src/examples/000-simple/index.tsx b/react/src/examples/000-simple/index.tsx index 9870a5914..e4cefa8c1 100644 --- a/react/src/examples/000-simple/index.tsx +++ b/react/src/examples/000-simple/index.tsx @@ -8,21 +8,18 @@ export function Simple0() { const [uncontrolledInitialOptions] = useState(() => ({ ...defaultGridOptions, children: [ - { id: "item1", h: 2, w: 2, x: 0, y: 0 }, - { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + { id: "000-item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "000-item2", h: 2, w: 2, x: 2, y: 0 }, ], })); return ( - - + +
hello
- +
grid
diff --git a/react/src/examples/001-simple/index.tsx b/react/src/examples/001-simple/index.tsx index ee7d83843..b42775974 100644 --- a/react/src/examples/001-simple/index.tsx +++ b/react/src/examples/001-simple/index.tsx @@ -13,8 +13,8 @@ export function Simple() { const [uncontrolledInitialOptions] = useState(() => ({ ...defaultGridOptions, children: [ - { id: "item1", h: 2, w: 2, x: 0, y: 0 }, - { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + { id: "001-item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "001-item2", h: 2, w: 2, x: 2, y: 0 }, ], })); @@ -22,12 +22,12 @@ export function Simple() { - - + +
hello
- +
grid
diff --git a/react/src/examples/002-nested/index.tsx b/react/src/examples/002-nested/index.tsx index 122ac1402..daa6690a3 100644 --- a/react/src/examples/002-nested/index.tsx +++ b/react/src/examples/002-nested/index.tsx @@ -13,16 +13,16 @@ export function Nested() { const [uncontrolledInitialOptions] = useState(() => ({ ...defaultGridOptions, children: [ - { id: "item1", h: 2, w: 2, x: 0, y: 0 }, - { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + { id: "002-item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "002-item2", h: 2, w: 2, x: 2, y: 0 }, { - id: "sub-grid-1", + id: "002-sub-grid-1", h: 5, sizeToContent: true, subGridOpts: { children: [ { - id: "sub-grid-1-title", + id: "002-sub-grid-1-title", locked: true, noMove: true, noResize: true, @@ -31,8 +31,8 @@ export function Nested() { y: 0, content: "Sub Grid 1", }, - { id: "item3", h: 2, w: 2, x: 0, y: 1 }, - { id: "item4", h: 2, w: 2, x: 2, y: 0 }, + { id: "002-item3", h: 2, w: 2, x: 0, y: 1 }, + { id: "002-item4", h: 2, w: 2, x: 2, y: 0 }, ], }, w: 12, @@ -46,20 +46,20 @@ export function Nested() { - - + +
hello
- +
grid
- +
nested one
- +
nested two
diff --git a/react/src/examples/003-custom-handle/index.tsx b/react/src/examples/003-custom-handle/index.tsx index 040592cca..f112bb602 100644 --- a/react/src/examples/003-custom-handle/index.tsx +++ b/react/src/examples/003-custom-handle/index.tsx @@ -14,13 +14,13 @@ import { export function CustomHandle() { const [uncontrolledInitialOptions] = useState(() => ({ ...defaultGridOptions, - children: [{ id: "item1", h: 2, w: 2, x: 0, y: 0 }], + children: [{ id: "003-item1", h: 2, w: 2, x: 0, y: 0 }], })); return ( - - + +
Custom Handle
{/* Experimental: Render item with custom handle */} diff --git a/react/src/examples/004-drag-in/index.tsx b/react/src/examples/004-drag-in/index.tsx new file mode 100644 index 000000000..a79149dfc --- /dev/null +++ b/react/src/examples/004-drag-in/index.tsx @@ -0,0 +1,52 @@ +import { GridStackOptions } from "gridstack"; +import { useState } from "react"; +import { defaultGridOptions } from "../../default-grid-options"; +import { GridStackItem, GridStackDragInItem } from "../../lib"; +import { GridStackContainer } from "../../lib/grid-stack-container"; + +export function DragIn() { + const [uncontrolledInitialOptions] = useState(() => ({ + ...defaultGridOptions, + children: [ + { id: "004-item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "004-item2", h: 2, w: 2, x: 2, y: 0 }, + ], + })); + + return ( +
+
+ +
+ Drag me add to the grid +
+
+
+ + + +
hello
+
+ + +
grid
+
+
+
+ ); +} diff --git a/react/src/examples/009-advanced/index.tsx b/react/src/examples/009-advanced/index.tsx index 6c75bbcf2..24d2cbae9 100644 --- a/react/src/examples/009-advanced/index.tsx +++ b/react/src/examples/009-advanced/index.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { + GridStackDragInItem, GridStackItem, GridStackProvider, GridStackRender, @@ -15,22 +16,23 @@ import { useComponentInfoMap, } from "./components/component-info-map"; import { newId } from "../../utils"; +import { Counter } from "./components/counter"; export function Advanced() { // Data about layout by gridstack option const [uncontrolledInitialOptions] = useState(() => ({ ...defaultGridOptions, children: [ - { id: "item1", h: 2, w: 2, x: 0, y: 0 }, - { id: "item2", h: 2, w: 2, x: 2, y: 0 }, + { id: "009-item1", h: 2, w: 2, x: 0, y: 0 }, + { id: "009-item2", h: 2, w: 2, x: 2, y: 0 }, { - id: "sub-grid-1", + id: "009-sub-grid-1", h: 5, sizeToContent: true, subGridOpts: { children: [ { - id: "sub-grid-1-title", + id: "009-sub-grid-1-title", locked: true, noMove: true, noResize: true, @@ -39,33 +41,42 @@ export function Advanced() { y: 0, content: "Sub Grid 1", }, - { id: "item3", h: 2, w: 2, x: 0, y: 1 }, - { id: "item4", h: 2, w: 2, x: 2, y: 0 }, + { id: "009-item3", h: 2, w: 2, x: 0, y: 1 }, + { id: "009-item4", h: 2, w: 2, x: 2, y: 0 }, ], }, w: 4, x: 0, y: 2, }, - { id: "item5", w: 4, h: 4, x: 0, y: 2 }, + { id: "009-item5", w: 4, h: 4, x: 0, y: 2 }, ], })); // Data about every content const [initialComponentInfoMap] = useState>( () => ({ - item1: { component: "Text", serializableProps: { content: "Text" } }, - item2: { component: "Text", serializableProps: { content: "Text" } }, - "sub-grid-1-title": { + "009-item1": { + component: "Text", + serializableProps: { content: "Text" }, + }, + "009-item2": { + component: "Text", + serializableProps: { content: "Text" }, + }, + "009-sub-grid-1-title": { component: "Text", serializableProps: { content: "Sub Grid 1" }, }, - item3: { component: "Text", serializableProps: { content: "Text" } }, - item4: { + "009-item3": { + component: "Text", + serializableProps: { content: "Text" }, + }, + "009-item4": { component: "Counter", serializableProps: { label: "Click me" }, }, - item5: { + "009-item5": { component: "ComplexCard", serializableProps: { title: "Complex Card", color: "red" }, }, @@ -233,6 +244,40 @@ export function Toolbar() { > Append Complex Card (4x4) + + {/* TODO add to the component info map */} + +
+ Drag me add to the grid +
+
+ + {/* TODO add to the component info map */} + + Copied Counter: + + + } + > +
+ Drag Counter like: +
+
); } diff --git a/react/src/lib/global.ts b/react/src/lib/global.ts new file mode 100644 index 000000000..10862cf7a --- /dev/null +++ b/react/src/lib/global.ts @@ -0,0 +1,10 @@ +import { GridStack, GridStackWidget } from "gridstack"; + +export const widgetContainers = new Array<{ + element: HTMLElement; + initWidget: GridStackWidget; +}>(); + +GridStack.renderCB = (element: HTMLElement, widget: GridStackWidget) => { + widgetContainers.push({ element, initWidget: widget }); +}; diff --git a/react/src/lib/grid-stack-container.tsx b/react/src/lib/grid-stack-container.tsx index c5d288abe..0a5646b75 100644 --- a/react/src/lib/grid-stack-container.tsx +++ b/react/src/lib/grid-stack-container.tsx @@ -5,19 +5,15 @@ import { GridStackRender } from "./grid-stack-render"; export type GridStackContainerProps = PropsWithChildren<{ initialOptions: GridStackOptions; - renderRawContent?: boolean; }>; export function GridStackContainer({ children, initialOptions, - renderRawContent, }: GridStackContainerProps) { return ( - - {children} - + {children} ); } diff --git a/react/src/lib/grid-stack-drag-in-item.tsx b/react/src/lib/grid-stack-drag-in-item.tsx new file mode 100644 index 000000000..5337445c4 --- /dev/null +++ b/react/src/lib/grid-stack-drag-in-item.tsx @@ -0,0 +1,77 @@ +import { DDDragOpt, GridStack, GridStackWidget, Utils } from "gridstack"; +import { + ComponentProps, + Fragment, + PropsWithChildren, + ReactNode, + useEffect, + useRef, + useState, +} from "react"; +import { createPortal } from "react-dom"; + +export type GridStackDragInItemProps = PropsWithChildren< + Omit, "content" | "children" | "widget"> & { + widget: Omit; + dragOptions?: DDDragOpt; + content?: ReactNode; + } +>; + +/** + * @experimental + * This is a temporary solution to drag in items to the grid. + * Copy the original element and render it in the portal. + */ +export function GridStackDragInItem({ + children, + widget, + className, + dragOptions, + content, + ...props +}: GridStackDragInItemProps) { + const panelRef = useRef(null); + const [clones, setClones] = useState>(new Map()); + const incrementalId = useRef(0); + + useEffect(() => { + if (panelRef.current) { + GridStack.setupDragIn( + [panelRef.current], + { + ...dragOptions, + helper: (el) => { + const clone = Utils.cloneNode(el); + const id = String(incrementalId.current++); + + setClones((prev) => { + const newMap = new Map(prev); + newMap.set(id, clone); + return newMap; + }); + // ! clear dom copied from the original element + clone.children[0].innerHTML = ""; + return clone; + }, + }, + [widget] + ); + } + }, [clones, dragOptions, widget]); + + return ( + <> +
+
{children}
+
+ + {/* Render the cloned element in the portal */} + {Array.from(clones.entries()).map(([id, clone]) => ( + + {createPortal(content ?? children, clone.children[0])} + + ))} + + ); +} diff --git a/react/src/lib/grid-stack-handle-re-initializer.tsx b/react/src/lib/grid-stack-handle-re-initializer.tsx index 8d0f732ec..24af15923 100644 --- a/react/src/lib/grid-stack-handle-re-initializer.tsx +++ b/react/src/lib/grid-stack-handle-re-initializer.tsx @@ -23,11 +23,11 @@ export function GridStackHandleReInitializer( _gridStack: { value: gridStack }, } = useGridStackContext(); const { id: widgetId } = useGridStackItemContext(); - const { getWidgetContainer } = useGridStackRenderContext(); + const { getContainerByWidgetId } = useGridStackRenderContext(); useLayoutEffect(() => { if (gridStack) { - const widgetContainer = getWidgetContainer(widgetId); + const widgetContainer = getContainerByWidgetId(widgetId); if (widgetContainer) { const element = Utils.getElement( widgetContainer.parentElement! @@ -50,7 +50,7 @@ export function GridStackHandleReInitializer( } } } - }, [getWidgetContainer, gridStack, widgetId]); + }, [getContainerByWidgetId, gridStack, widgetId]); return <>{props.children}; } diff --git a/react/src/lib/grid-stack-item.tsx b/react/src/lib/grid-stack-item.tsx index 76a8a0f18..fca47c503 100644 --- a/react/src/lib/grid-stack-item.tsx +++ b/react/src/lib/grid-stack-item.tsx @@ -11,7 +11,7 @@ export type GridStackItemProps = PropsWithChildren<{ export function GridStackItem(props: GridStackItemProps) { const renderContext = useGridStackRenderContext(); - const widgetContainer = renderContext.getWidgetContainer(props.id); + const widgetContainer = renderContext.getContainerByWidgetId(props.id); const { removeWidget, _gridStack } = useGridStackContext(); diff --git a/react/src/lib/grid-stack-render-context.ts b/react/src/lib/grid-stack-render-context.ts index 26b0e0257..0bfeb5ac5 100644 --- a/react/src/lib/grid-stack-render-context.ts +++ b/react/src/lib/grid-stack-render-context.ts @@ -1,13 +1,13 @@ import { createContext, useContext } from "react"; export type GridStackRenderContextType = { - getWidgetContainer: (widgetId: string) => HTMLElement | null; + getContainerByWidgetId: (widgetId: string) => HTMLElement | null; }; export const GridStackRenderContext = createContext( { - getWidgetContainer: () => { - console.error("getWidgetContainer not implemented"); + getContainerByWidgetId: () => { + console.error("getContainerByWidgetId not implemented"); return null; }, } diff --git a/react/src/lib/grid-stack-render.tsx b/react/src/lib/grid-stack-render.tsx index 88b1a39cb..83be4cfec 100644 --- a/react/src/lib/grid-stack-render.tsx +++ b/react/src/lib/grid-stack-render.tsx @@ -1,43 +1,32 @@ -import { PropsWithChildren, useCallback, useLayoutEffect, useRef } from "react"; +import { + ComponentProps, + PropsWithChildren, + useCallback, + useLayoutEffect, + useRef, +} from "react"; import { useGridStackContext } from "./grid-stack-context"; -import { GridStack, GridStackOptions, GridStackWidget } from "gridstack"; +import { GridStack, GridStackOptions } from "gridstack"; import { GridStackRenderContext } from "./grid-stack-render-context"; +import { widgetContainers } from "./global"; -export type GridStackRenderProps = PropsWithChildren<{ - renderRawContent?: boolean; -}>; +export type GridStackRenderProps = PropsWithChildren>; -export function GridStackRender({ - children, - renderRawContent = false, -}: GridStackRenderProps) { +export function GridStackRender({ children, ...props }: GridStackRenderProps) { const { _gridStack: { value: gridStack, set: setGridStack }, initialOptions, } = useGridStackContext(); - const widgetContainersRef = useRef>(new Map()); const containerRef = useRef(null); const optionsRef = useRef(initialOptions); const initGrid = useCallback(() => { if (containerRef.current) { - GridStack.renderCB = (element: HTMLElement, widget: GridStackWidget) => { - if (widget.id) { - widgetContainersRef.current.set(widget.id, element); - } - - // ! Only as a fallback, if content is not set in the widget - if (renderRawContent) { - if (widget.content) { - element.innerHTML = widget.content; - } - } - }; return GridStack.init(optionsRef.current, containerRef.current); } return null; - }, [renderRawContent]); + }, []); useLayoutEffect(() => { if (!gridStack) { @@ -49,13 +38,18 @@ export function GridStackRender({ } }, [gridStack, initGrid, setGridStack]); - const getWidgetContainer = useCallback((widgetId: string) => { - return widgetContainersRef.current.get(widgetId) || null; + const getContainerByWidgetId = useCallback((widgetId: string) => { + return ( + widgetContainers.find((container) => container.initWidget.id === widgetId) + ?.element || null + ); }, []); return ( - -
{gridStack ? children : null}
+ +
+ {gridStack ? children : null} +
); } diff --git a/react/src/lib/index.ts b/react/src/lib/index.ts index f5d514dd6..c5b269754 100644 --- a/react/src/lib/index.ts +++ b/react/src/lib/index.ts @@ -27,3 +27,6 @@ export { export type { GridStackHandleReInitializerProps } from "./grid-stack-handle-re-initializer"; export { GridStackHandleReInitializer } from "./grid-stack-handle-re-initializer"; + +export type { GridStackDragInItemProps } from "./grid-stack-drag-in-item"; +export { GridStackDragInItem } from "./grid-stack-drag-in-item"; From 022f3ad56bbe920e3f975a1aa20821830f55980c Mon Sep 17 00:00:00 2001 From: CNine Date: Sun, 2 Mar 2025 10:20:16 +0800 Subject: [PATCH 7/8] Update react readme --- react/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/react/README.md b/react/README.md index 64d227a04..8d19d1d50 100644 --- a/react/README.md +++ b/react/README.md @@ -11,8 +11,10 @@ Open in [CodeSandbox](https://codesandbox.io/p/sandbox/github/gridstack/gridstac - [x] Nested Sub Grid - [x] Remove Widget - [x] Copy(Duplicate) Widget -- [x] Custom handle - [x] Drag between two grid stacks +- [x] Custom handle (Experimental) +- [x] Drag in item (Experimental) +- [ ] Save/Load grid stack options from storage ## Usage From 980d73ee1f27cfb240c820b1288c9aea9007b686 Mon Sep 17 00:00:00 2001 From: CNine Date: Sun, 2 Mar 2025 10:40:44 +0800 Subject: [PATCH 8/8] Fix prepareDragDrop for GridStackHandleReInitializer --- react/src/lib/grid-stack-handle-re-initializer.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/react/src/lib/grid-stack-handle-re-initializer.tsx b/react/src/lib/grid-stack-handle-re-initializer.tsx index 24af15923..0d86af13f 100644 --- a/react/src/lib/grid-stack-handle-re-initializer.tsx +++ b/react/src/lib/grid-stack-handle-re-initializer.tsx @@ -44,9 +44,16 @@ export function GridStackHandleReInitializer( // https://github.com/gridstack/gridstack.js/blob/a917afcada4bd2892963678c8b1bde7630bb9528/src/gridstack.ts#L2402 const g = gridStack as GridStack & { - _prepareDragDropByNode: (node: GridStackNode) => void; + _prepareDragDropByNode?: (node: GridStackNode) => void; }; - g._prepareDragDropByNode(node); + if (g._prepareDragDropByNode) { + g._prepareDragDropByNode(node); + } else { + // https://github.com/gridstack/gridstack.js/blob/90a014d5f396ac335962c4192d6aa434f04bf223/src/gridstack.ts#L2413 + if (g.prepareDragDrop) { + g.prepareDragDrop(element); + } + } } } }