>), // 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 (
-
- {
- addWidget((id) => ({
- w: 2,
- h: 2,
- x: 0,
- y: 0,
- content: JSON.stringify({
- name: "Text",
- props: { content: id },
- }),
- }));
- }}
- >
- Add Text (2x2)
-
-
- {
- addSubGrid((id, withWidget) => ({
- h: 5,
- noResize: false,
- sizeToContent: true,
- subGridOpts: {
- acceptWidgets: true,
- columnOpts: { breakpoints: BREAKPOINTS, layout: "moveScale" },
- margin: 8,
- minRow: 2,
- cellHeight: CELL_HEIGHT,
- children: [
- withWidget({
- h: 1,
- locked: true,
- noMove: true,
- noResize: true,
- w: 12,
- x: 0,
- y: 0,
- content: JSON.stringify({
- name: "Text",
- props: { content: "Sub Grid 1 Title" + id },
- }),
- }),
- ],
- },
- w: 12,
- x: 0,
- y: 0,
- }));
- }}
- >
- Add Sub Grid (12x1)
-
-
- );
-}
-
-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/examples/000-simple/index.tsx b/react/src/examples/000-simple/index.tsx
new file mode 100644
index 00000000..e4cefa8c
--- /dev/null
+++ b/react/src/examples/000-simple/index.tsx
@@ -0,0 +1,27 @@
+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: "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
new file mode 100644
index 00000000..b4277597
--- /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: "001-item1", h: 2, w: 2, x: 0, y: 0 },
+ { id: "001-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 (
+
+ {
+ handleAddText(2, 2);
+ }}
+ >
+ Add Text (2x2)
+
+
+ );
+}
diff --git a/react/src/examples/002-nested/index.tsx b/react/src/examples/002-nested/index.tsx
new file mode 100644
index 00000000..daa6690a
--- /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: "002-item1", h: 2, w: 2, x: 0, y: 0 },
+ { id: "002-item2", h: 2, w: 2, x: 2, y: 0 },
+ {
+ id: "002-sub-grid-1",
+ h: 5,
+ sizeToContent: true,
+ subGridOpts: {
+ children: [
+ {
+ id: "002-sub-grid-1-title",
+ locked: true,
+ noMove: true,
+ noResize: true,
+ w: 12,
+ x: 0,
+ y: 0,
+ content: "Sub Grid 1",
+ },
+ { id: "002-item3", h: 2, w: 2, x: 0, y: 1 },
+ { id: "002-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 (
+
+ {
+ handleAddText(2, 2);
+ }}
+ >
+ Add Text (2x2)
+
+ {
+ handleAddSubGrid();
+ }}
+ >
+ Add Sub Grid (4x5)
+
+
+ );
+}
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 00000000..f112bb60
--- /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: "003-item1", h: 2, w: 2, x: 0, y: 0 }],
+ }));
+
+ return (
+
+
+
+ Custom Handle
+
+ {/* Experimental: Render item with custom handle */}
+
+
+ Handle ONLY HERE
+
+
+
+
+
+ );
+}
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 00000000..a79149df
--- /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/component-map.tsx b/react/src/examples/009-advanced/component-map.tsx
new file mode 100644
index 00000000..598b0c64
--- /dev/null
+++ b/react/src/examples/009-advanced/component-map.tsx
@@ -0,0 +1,22 @@
+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,
+ Counter,
+ 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/examples/009-advanced/components/complex-card.tsx b/react/src/examples/009-advanced/components/complex-card.tsx
new file mode 100644
index 00000000..056e6794
--- /dev/null
+++ b/react/src/examples/009-advanced/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}
+
+
{
+ const widgetId = newId();
+
+ addWidget({
+ id: widgetId,
+ ...getBounds()?.current,
+ });
+
+ addComponentInfo(widgetId, {
+ component: "ComplexCard",
+ serializableProps: { ...props.serializableProps },
+ });
+ }}
+ >
+ Duplicate
+
+
+
{
+ remove();
+ }}
+ >
+ Remove
+
+
+
setDialogEditOpen(false)}>
+
+
+ Title:
+ setTitle(e.target.value)} />
+
+
+ Color:
+ setColor(e.target.value)}>
+ Red
+ Blue
+ Green
+ Yellow
+
+
+
+ Size:
+ {
+ const [w, h] = e.target.value.split("x").map(Number);
+ setSize({ w, h });
+ }}
+ >
+ 4x4
+ 6x6
+ 8x8
+
+
+
+ setDialogEditOpen(false)}>
+ Close
+
+
+
+
+
+
{
+ setDialogEditOpen(true);
+ const bounds = getBounds();
+ setSize({
+ w: bounds?.current.w ?? 0,
+ h: bounds?.current.h ?? 0,
+ });
+ }}
+ >
+ Edit
+
+
+
+
+ Move
+
+
+
+ >
+ );
+}
diff --git a/react/src/examples/009-advanced/components/component-info-map/component-info-map-context.ts b/react/src/examples/009-advanced/components/component-info-map/component-info-map-context.ts
new file mode 100644
index 00000000..6781eb9f
--- /dev/null
+++ b/react/src/examples/009-advanced/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/examples/009-advanced/components/component-info-map/component-info-map-provider.tsx b/react/src/examples/009-advanced/components/component-info-map/component-info-map-provider.tsx
new file mode 100644
index 00000000..87dee6e6
--- /dev/null
+++ b/react/src/examples/009-advanced/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/examples/009-advanced/components/component-info-map/index.ts b/react/src/examples/009-advanced/components/component-info-map/index.ts
new file mode 100644
index 00000000..a4eabda6
--- /dev/null
+++ b/react/src/examples/009-advanced/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/examples/009-advanced/components/component-info-map/use-component-info-map.ts b/react/src/examples/009-advanced/components/component-info-map/use-component-info-map.ts
new file mode 100644
index 00000000..7abf92ef
--- /dev/null
+++ b/react/src/examples/009-advanced/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/examples/009-advanced/components/counter.tsx b/react/src/examples/009-advanced/components/counter.tsx
new file mode 100644
index 00000000..9e49d0df
--- /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 (
+ setCount(count + 1)}>
+ {props.label} {count}
+
+ );
+}
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 00000000..7200c33d
--- /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 00000000..24d2cbae
--- /dev/null
+++ b/react/src/examples/009-advanced/index.tsx
@@ -0,0 +1,283 @@
+import { useState } from "react";
+
+import {
+ GridStackDragInItem,
+ 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";
+import { Counter } from "./components/counter";
+
+export function Advanced() {
+ // Data about layout by gridstack option
+ const [uncontrolledInitialOptions] = useState(() => ({
+ ...defaultGridOptions,
+ children: [
+ { id: "009-item1", h: 2, w: 2, x: 0, y: 0 },
+ { id: "009-item2", h: 2, w: 2, x: 2, y: 0 },
+ {
+ id: "009-sub-grid-1",
+ h: 5,
+ sizeToContent: true,
+ subGridOpts: {
+ children: [
+ {
+ id: "009-sub-grid-1-title",
+ locked: true,
+ noMove: true,
+ noResize: true,
+ w: 12,
+ x: 0,
+ y: 0,
+ content: "Sub Grid 1",
+ },
+ { 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: "009-item5", w: 4, h: 4, x: 0, y: 2 },
+ ],
+ }));
+
+ // Data about every content
+ const [initialComponentInfoMap] = useState>(
+ () => ({
+ "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" },
+ },
+ "009-item3": {
+ component: "Text",
+ serializableProps: { content: "Text" },
+ },
+ "009-item4": {
+ component: "Counter",
+ serializableProps: { label: "Click me" },
+ },
+ "009-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 (
+
+
{
+ handleAddText(2, 2);
+ }}
+ >
+ Add Text (2x2)
+
+
+
{
+ handleAddSubGrid();
+ }}
+ >
+ Add Sub Grid (4x5)
+
+
{
+ handleAddComplexCard();
+ }}
+ >
+ 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 00000000..10862cf7
--- /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
new file mode 100644
index 00000000..0a5646b7
--- /dev/null
+++ b/react/src/lib/grid-stack-container.tsx
@@ -0,0 +1,19 @@
+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;
+}>;
+
+export function GridStackContainer({
+ children,
+ initialOptions,
+}: GridStackContainerProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/react/src/lib/grid-stack-context.ts b/react/src/lib/grid-stack-context.ts
new file mode 100644
index 00000000..ef3a0011
--- /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-drag-in-item.tsx b/react/src/lib/grid-stack-drag-in-item.tsx
new file mode 100644
index 00000000..5337445c
--- /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 (
+ <>
+
+
+ {/* 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
new file mode 100644
index 00000000..0d86af13
--- /dev/null
+++ b/react/src/lib/grid-stack-handle-re-initializer.tsx
@@ -0,0 +1,63 @@
+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 { getContainerByWidgetId } = useGridStackRenderContext();
+
+ useLayoutEffect(() => {
+ if (gridStack) {
+ const widgetContainer = getContainerByWidgetId(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;
+ };
+ 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);
+ }
+ }
+ }
+ }
+ }
+ }, [getContainerByWidgetId, gridStack, widgetId]);
+
+ return <>{props.children}>;
+}
diff --git a/react/src/lib/grid-stack-item-context.ts b/react/src/lib/grid-stack-item-context.ts
new file mode 100644
index 00000000..72886922
--- /dev/null
+++ b/react/src/lib/grid-stack-item-context.ts
@@ -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 00000000..fca47c50
--- /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.getContainerByWidgetId(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 00000000..3fcd3777
--- /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 00000000..0bfeb5ac
--- /dev/null
+++ b/react/src/lib/grid-stack-render-context.ts
@@ -0,0 +1,24 @@
+import { createContext, useContext } from "react";
+
+export type GridStackRenderContextType = {
+ getContainerByWidgetId: (widgetId: string) => HTMLElement | null;
+};
+
+export const GridStackRenderContext = createContext(
+ {
+ getContainerByWidgetId: () => {
+ console.error("getContainerByWidgetId 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 00000000..83be4cfe
--- /dev/null
+++ b/react/src/lib/grid-stack-render.tsx
@@ -0,0 +1,55 @@
+import {
+ ComponentProps,
+ PropsWithChildren,
+ useCallback,
+ useLayoutEffect,
+ useRef,
+} from "react";
+import { useGridStackContext } from "./grid-stack-context";
+import { GridStack, GridStackOptions } from "gridstack";
+import { GridStackRenderContext } from "./grid-stack-render-context";
+import { widgetContainers } from "./global";
+
+export type GridStackRenderProps = PropsWithChildren>;
+
+export function GridStackRender({ children, ...props }: GridStackRenderProps) {
+ const {
+ _gridStack: { value: gridStack, set: setGridStack },
+ initialOptions,
+ } = useGridStackContext();
+
+ const containerRef = useRef(null);
+ const optionsRef = useRef(initialOptions);
+
+ const initGrid = useCallback(() => {
+ if (containerRef.current) {
+ 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 getContainerByWidgetId = useCallback((widgetId: string) => {
+ return (
+ widgetContainers.find((container) => container.initWidget.id === widgetId)
+ ?.element || null
+ );
+ }, []);
+
+ return (
+
+
+ {gridStack ? children : null}
+
+
+ );
+}
diff --git a/react/src/lib/index.ts b/react/src/lib/index.ts
new file mode 100644
index 00000000..c5b26975
--- /dev/null
+++ b/react/src/lib/index.ts
@@ -0,0 +1,32 @@
+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";
+
+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";
+
+export type { GridStackDragInItemProps } from "./grid-stack-drag-in-item";
+export { GridStackDragInItem } from "./grid-stack-drag-in-item";
diff --git a/react/src/main.tsx b/react/src/main.tsx
index 0bf19b8c..a874241f 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
new file mode 100644
index 00000000..0342b9c5
--- /dev/null
+++ b/react/src/utils.ts
@@ -0,0 +1,3 @@
+export function newId() {
+ return `${Math.random().toString(36).substring(2, 15)}`;
+}
diff --git a/react/vite.config.ts b/react/vite.config.ts
index 861b04b3..71158220 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
+ },
+});