diff --git a/react/README.md b/react/README.md index 74872fd4a..38185e285 100644 --- a/react/README.md +++ b/react/README.md @@ -1,50 +1,120 @@ -# React + TypeScript + Vite +# React GridStack Wrapper Demo -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +A React wrapper component for GridStack that provides better TypeScript support and React integration experience. -Currently, two official plugins are available: +## TODO -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- [x] Component mapping +- [x] SubGrid support +- [ ] Save and restore layout +- [ ] Publish to npm -## Expanding the ESLint configuration +## Basic Usage -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +This is not an npm package, it's just a demo project. Please copy the relevant code to your project to use it. -- Configure the top-level `parserOptions` property like this: +```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"; -```js -export default tseslint.config({ - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, +function Text({ content }: { content: string }) { + return
{content}
; +} + +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 + ], +}; + +function App() { + return ( + + + + + + + + + + + + ); +} ``` -- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` -- Optionally add `...tseslint.configs.stylisticTypeChecked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: - -```js -// eslint.config.js -import react from 'eslint-plugin-react' - -export default tseslint.config({ - // Set the react version - settings: { react: { version: '18.3' } }, - plugins: { - // Add the react plugin - react, - }, - rules: { - // other rules... - // Enable its recommended rules - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - }, -}) +## Advanced Features + +### Toolbar Operations + +Provide APIs to add new components and sub-grids: + +```tsx +function Toolbar() { + const { addWidget, addSubGrid } = useGridStackContext(); + + return ( +
+ + +
+ ); +} ``` + +### Layout Saving + +Get the current layout: + +```tsx +const { saveOptions } = useGridStackContext(); + +const currentLayout = saveOptions(); +``` + +## API Reference + +### GridStackProvider + +The main context provider, accepts the following properties: + +- `initialOptions`: Initial configuration options for GridStack + +### GridStackRender + +The core component for rendering the grid, accepts the following properties: + +- `componentMap`: A mapping from component names to actual React components + +### 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 diff --git a/react/lib/constants.ts b/react/lib/constants.ts deleted file mode 100644 index d18a1ed7d..000000000 --- a/react/lib/constants.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GridStackOptions } from 'gridstack'; - -export const BREAKPOINTS = [ - { c: 1, w: 700 }, - { c: 3, w: 850 }, - { c: 6, w: 950 }, - { c: 8, w: 1100 }, -]; -const cellHeight = 50; - -export const SUB_GRID_OPTIONS: GridStackOptions = { - acceptWidgets: true, - columnOpts: { - breakpoints: BREAKPOINTS, - layout: 'moveScale', - }, - margin: 8, - minRow: 2, - cellHeight, -} as const; - -export const GRID_OPTIONS: GridStackOptions = { - acceptWidgets: true, - columnOpts: { - breakpointForWindow: true, - breakpoints: BREAKPOINTS, - layout: 'moveScale', - }, - margin: 8, - cellHeight, - subGridOpts: SUB_GRID_OPTIONS, -} as const; diff --git a/react/lib/grid-stack-context.ts b/react/lib/grid-stack-context.ts new file mode 100644 index 000000000..51e950700 --- /dev/null +++ b/react/lib/grid-stack-context.ts @@ -0,0 +1,35 @@ +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 new file mode 100644 index 000000000..1afe4348c --- /dev/null +++ b/react/lib/grid-stack-provider.tsx @@ -0,0 +1,113 @@ +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 new file mode 100644 index 000000000..1135f8a44 --- /dev/null +++ b/react/lib/grid-stack-render-context.ts @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000..82c8e1f24 --- /dev/null +++ b/react/lib/grid-stack-render-provider.tsx @@ -0,0 +1,87 @@ +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 new file mode 100644 index 000000000..cf9a84893 --- /dev/null +++ b/react/lib/grid-stack-render.tsx @@ -0,0 +1,69 @@ +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 new file mode 100644 index 000000000..14ee1c65f --- /dev/null +++ b/react/lib/grid-stack-widget-context.ts @@ -0,0 +1,18 @@ +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/gridstack-context.tsx b/react/lib/gridstack-context.tsx deleted file mode 100644 index 20298ebde..000000000 --- a/react/lib/gridstack-context.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { GridStack, GridStackWidget,GridStackOptions } from 'gridstack'; -import React, { - createContext, - PropsWithChildren, - useCallback, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react'; -import isEqual from 'react-fast-compare'; - -type GridStackContextType = { - getWidgetContent: (widgetId: string) => HTMLElement | null; -}; - -interface GridstackProviderProps extends PropsWithChildren { - options: GridStackOptions; -} - -export const GridstackContext = createContext(null); - -export const GridstackProvider = ({ children, options }: GridstackProviderProps) => { - const widgetContentRef = useRef>({}); - const containerRef = useRef(null); - const optionsRef = useRef(options); - - const [parentGrid, setParentGrid] = useState(null); - - const renderCBFn = useCallback((element: HTMLElement, widget: GridStackWidget) => { - if (widget.id) { - widgetContentRef.current[widget.id] = element; - } - }, []); - - const getWidgetContent = useCallback((widgetId: string) => { - return widgetContentRef.current[widgetId] || null; - }, []); - - const initGrid = useCallback(() => { - if (containerRef.current) { - GridStack.renderCB = renderCBFn; - return GridStack.init(optionsRef.current, containerRef.current); - } - return null; - }, [renderCBFn]); - - useLayoutEffect(() => { - if (!isEqual(options, optionsRef.current) && parentGrid) { - try { - parentGrid.removeAll(false); - parentGrid.destroy(false); - widgetContentRef.current = {}; - optionsRef.current = options; - - setParentGrid(initGrid()); - } catch (e) { - console.error("Error reinitializing gridstack", e); - } - } - }, [options, parentGrid, initGrid]); - - useLayoutEffect(() => { - if (!parentGrid) { - try { - setParentGrid(initGrid()); - } catch (e) { - console.error("Error initializing gridstack", e); - } - } - }, [parentGrid, initGrid]); - - const value = useMemo( - () => ({ - getWidgetContent, - }), - // parentGrid is required to reinitialize the grid when the options change - [getWidgetContent, parentGrid], - ); - - return ( - -
{parentGrid ? children : null}
-
- ); -}; \ No newline at end of file diff --git a/react/lib/gridstack-item.tsx b/react/lib/gridstack-item.tsx deleted file mode 100644 index 9e3b7fa07..000000000 --- a/react/lib/gridstack-item.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { type GridItemHTMLElement } from 'gridstack'; -import React, { PropsWithChildren } from 'react'; -import { createPortal } from 'react-dom'; - -import { useGridstackContext } from './use-gridstack-context'; - -export interface GridstackItemProps extends PropsWithChildren { - id: string; -} - -export type ItemRefType = React.MutableRefObject; - -export const GridstackItem = ({ id, children }: GridstackItemProps) => { - const { getWidgetContent } = useGridstackContext(); - const widgetContent = getWidgetContent(id); - - if (!widgetContent) { - return null; - } - - return createPortal(children, widgetContent); -}; diff --git a/react/lib/index.ts b/react/lib/index.ts index 788a53258..e5984f371 100644 --- a/react/lib/index.ts +++ b/react/lib/index.ts @@ -1,4 +1,19 @@ +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 } from './gridstack-context'; -export { GridstackItem } from './gridstack-item'; -export { useGridstackContext } from './use-gridstack-context'; \ No newline at end of file +export { + GridStackProvider, + GridStackRenderProvider, + GridStackRender, + type ComponentDataType, + type ComponentMap, + useGridStackContext, + useGridStackWidgetContext, +}; diff --git a/react/lib/use-gridstack-context.ts b/react/lib/use-gridstack-context.ts deleted file mode 100644 index 4f40766de..000000000 --- a/react/lib/use-gridstack-context.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; - -import { GridstackContext } from "./gridstack-context"; - -export const useGridstackContext = () => { - const gridstackContext = useContext(GridstackContext); - if (!gridstackContext) { - throw new Error('useGridstack must be used within a GridstackProvider'); - } - return gridstackContext; -}; diff --git a/react/src/App.tsx b/react/src/App.tsx index cf48b64cf..72c9508a5 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -1,13 +1,14 @@ -import { GridStackDemo } from './demo/demo' +import { GridStackDemo } from "./demo/demo"; function App() { - return ( <>

Gridstack React Wrapper Demo

+ +

(Uncontrolled)

- ) + ); } -export default App +export default App; diff --git a/react/src/demo/demo.css b/react/src/demo/demo.css index c8544a1ee..f8acbd739 100644 --- a/react/src/demo/demo.css +++ b/react/src/demo/demo.css @@ -1,47 +1,7 @@ -.btn-primary { - color: #fff; - background-color: #007bff; -} - -.btn { - display: inline-block; - padding: .375rem .75rem; - line-height: 1.5; - border-radius: .25rem; -} - -a { - text-decoration: none; -} - -h1 { - font-size: 2.5rem; - margin-bottom: .5rem; -} - -.sidebar { - background: rgb(215, 243, 215); - padding: 25px 0; - height: 100px; - text-align: center; -} -.sidebar > .grid-stack-item, -.sidebar-item { - width: 100px; - height: 50px; - border: 2px dashed green; - text-align: center; - line-height: 35px; - background: rgb(192, 231, 192); - cursor: default; - display: inline-block; -} - .grid-stack { background: #FAFAD2; } -.sidebar > .grid-stack-item, .grid-stack-item-content { text-align: center; background-color: #18bc9c; @@ -58,7 +18,7 @@ h1 { /* make nested grid have slightly darker bg take almost all space (need some to tell them apart) so items inside can have similar to external size+margin */ .grid-stack > .grid-stack-item.grid-stack-sub-grid > .grid-stack-item-content { background: rgba(0,0,0,0.1); - inset: 0 2px; + inset: 8px 8px; } .grid-stack.grid-stack-nested { background: none; diff --git a/react/src/demo/demo.tsx b/react/src/demo/demo.tsx index acf48e706..2e1499b59 100644 --- a/react/src/demo/demo.tsx +++ b/react/src/demo/demo.tsx @@ -1,157 +1,281 @@ -import React, { useRef, useState } from 'react'; -import { GRID_OPTIONS, SUB_GRID_OPTIONS } from '../../lib/constants'; -import { GridstackItem, GridstackProvider } from '../../lib'; -import { GridStackOptions } from 'gridstack'; -import './demo.css'; +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: [ - { h: 2, id: 'item1', w: 2, x: 0, y: 0 }, - { h: 2, id: 'item2', w: 2, x: 0, y: 0 }, { - h: 5, - id: 'sub-grid-1', - noResize: true, - sizeToContent: true, - subGridOpts: { - ...SUB_GRID_OPTIONS, - children: [ - { - h: 1, - id: 'sub-grid-1-title', - locked: true, - noMove: true, - noResize: true, - w: 12, - x: 0, - y: 0, - }, - { h: 2, id: 'item3', w: 2, x: 0, y: 0 }, - { h: 2, id: 'item4', w: 2, x: 0, y: 0 }, - ], - }, - w: 12, + 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 }, - ], - ...GRID_OPTIONS, -}; - - -const gridOptions2: GridStackOptions = { - children: [ - { h: 2, id: 'item6', w: 4, x: 0, y: 0 }, - { h: 2, id: 'item7', w: 6, x: 0, y: 0 }, { + 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, - id: 'sub-grid-2', - noResize: true, sizeToContent: true, subGridOpts: { - ...SUB_GRID_OPTIONS, + acceptWidgets: true, + cellHeight: CELL_HEIGHT, + alwaysShowResizeHandle: false, + column: "auto", + minRow: 2, + layout: "list", + margin: 8, children: [ { - h: 1, - id: 'sub-grid-2-title', + 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" }, + }), }, - { h: 2, id: 'item8', w: 4, x: 0, y: 0 }, - { h: 2, id: 'item9', w: 6, x: 0, y: 0 }, ], }, w: 12, x: 0, - y: 0, + y: 2, }, ], - ...GRID_OPTIONS, }; -const WIDGETS_NODE_MAP: Record = { - item1:
Item 1
, - item2:
Item 2
, - 'sub-grid-1': ( - <> - -
- Section Title Locked -
-
- -
Item 3
-
- -
Item 4
-
- - ), - item6:
Item 6
, - item7:
Item 7
, - 'sub-grid-2': ( - <> - -
- Section Title Locked -
-
- -
Item 8
-
- -
Item 9
-
- - ), -}; +export function GridStackDemo() { + // ! Uncontrolled + const [initialOptions] = useState(gridOptions); + + return ( + + -export const GridStackDemo = () => { - const [currentGridOptions, setCurrentGridOptions] = useState(gridOptions); - const currentGridName = useRef('Grid A'); + + + + + + ); +} + +function Toolbar() { + const { addWidget, addSubGrid } = useGridStackContext(); return ( -
- - {currentGridName.current} - - - +
+ + +
); -}; +} + +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]); -const GridDemo = ({ options }: {options: GridStackOptions}) => { return ( - <> - {options.children?.map((widget) => { - if (!widget.id) { - return null; - } - - if (widget.subGridOpts) { - return WIDGETS_NODE_MAP[widget.id]; - } - - return ( - - {WIDGETS_NODE_MAP[widget.id]} - - ); - })} - +
+

Debug Info

+
+
+

Initial Options

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

Realtime Options (2s refresh)

+
+            {JSON.stringify(realtimeOptions, null, 2)}
+          
+
+
+
); -}; +} diff --git a/react/tsconfig.app.tsbuildinfo b/react/tsconfig.app.tsbuildinfo new file mode 100644 index 000000000..2d7328826 --- /dev/null +++ b/react/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/demo/demo.tsx"],"version":"5.6.2"} \ No newline at end of file diff --git a/react/tsconfig.node.tsbuildinfo b/react/tsconfig.node.tsbuildinfo new file mode 100644 index 000000000..98ef2f996 --- /dev/null +++ b/react/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.6.2"} \ No newline at end of file