Skip to content

Commit 0b5548e

Browse files
committed
react demo with more use case and readme
1 parent d791743 commit 0b5548e

17 files changed

+716
-358
lines changed

react/README.md

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,120 @@
1-
# React + TypeScript + Vite
1+
# React GridStack Wrapper Demo
22

3-
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
3+
A React wrapper component for GridStack that provides better TypeScript support and React integration experience.
44

5-
Currently, two official plugins are available:
5+
## TODO
66

7-
- [@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
8-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
7+
- [x] Component mapping
8+
- [x] SubGrid support
9+
- [ ] Save and restore layout
10+
- [ ] Publish to npm
911

10-
## Expanding the ESLint configuration
12+
## Basic Usage
1113

12-
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
14+
This is not an npm package, it's just a demo project. Please copy the relevant code to your project to use it.
1315

14-
- Configure the top-level `parserOptions` property like this:
16+
```tsx
17+
import {
18+
GridStackProvider,
19+
GridStackRender,
20+
GridStackRenderProvider,
21+
} from "path/to/lib";
22+
import "gridstack/dist/gridstack.css";
23+
import "gridstack/dist/gridstack-extra.css";
24+
import "path/to/demo.css";
1525

16-
```js
17-
export default tseslint.config({
18-
languageOptions: {
19-
// other options...
20-
parserOptions: {
21-
project: ['./tsconfig.node.json', './tsconfig.app.json'],
22-
tsconfigRootDir: import.meta.dirname,
26+
function Text({ content }: { content: string }) {
27+
return <div>{content}</div>;
28+
}
29+
30+
const COMPONENT_MAP = {
31+
Text,
32+
// ... other components
33+
};
34+
35+
// Grid options
36+
const gridOptions = {
37+
acceptWidgets: true,
38+
margin: 8,
39+
cellHeight: 50,
40+
children: [
41+
{
42+
id: "item1",
43+
h: 2,
44+
w: 2,
45+
content: JSON.stringify({
46+
name: "Text",
47+
props: { content: "Item 1" },
48+
}),
2349
},
24-
},
25-
})
50+
// ... other grid items
51+
],
52+
};
53+
54+
function App() {
55+
return (
56+
<GridStackProvider initialOptions={gridOptions}>
57+
<!-- Maybe a toolbar here. Access to addWidget and addSubGrid by useGridStackContext() -->
58+
59+
<!-- Grid Stack Root Element -->
60+
<GridStackRenderProvider>
61+
<!-- Grid Stack Default Render -->
62+
<GridStackRender componentMap={COMPONENT_MAP} />
63+
</GridStackRenderProvider>
64+
65+
<!-- Maybe other UI here -->
66+
</GridStackProvider>
67+
);
68+
}
2669
```
2770

28-
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29-
- Optionally add `...tseslint.configs.stylisticTypeChecked`
30-
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31-
32-
```js
33-
// eslint.config.js
34-
import react from 'eslint-plugin-react'
35-
36-
export default tseslint.config({
37-
// Set the react version
38-
settings: { react: { version: '18.3' } },
39-
plugins: {
40-
// Add the react plugin
41-
react,
42-
},
43-
rules: {
44-
// other rules...
45-
// Enable its recommended rules
46-
...react.configs.recommended.rules,
47-
...react.configs['jsx-runtime'].rules,
48-
},
49-
})
71+
## Advanced Features
72+
73+
### Toolbar Operations
74+
75+
Provide APIs to add new components and sub-grids:
76+
77+
```tsx
78+
function Toolbar() {
79+
const { addWidget, addSubGrid } = useGridStackContext();
80+
81+
return (
82+
<div>
83+
<button onClick={() => addWidget(/* ... */)}>Add Component</button>
84+
<button onClick={() => addSubGrid(/* ... */)}>Add SubGrid</button>
85+
</div>
86+
);
87+
}
5088
```
89+
90+
### Layout Saving
91+
92+
Get the current layout:
93+
94+
```tsx
95+
const { saveOptions } = useGridStackContext();
96+
97+
const currentLayout = saveOptions();
98+
```
99+
100+
## API Reference
101+
102+
### GridStackProvider
103+
104+
The main context provider, accepts the following properties:
105+
106+
- `initialOptions`: Initial configuration options for GridStack
107+
108+
### GridStackRender
109+
110+
The core component for rendering the grid, accepts the following properties:
111+
112+
- `componentMap`: A mapping from component names to actual React components
113+
114+
### Hooks
115+
116+
- `useGridStackContext()`: Access GridStack context and operations
117+
- `addWidget`: Add a new component
118+
- `addSubGrid`: Add a new sub-grid
119+
- `saveOptions`: Save current layout
120+
- `initialOptions`: Initial configuration options

react/lib/constants.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

react/lib/grid-stack-context.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { GridStack, GridStackOptions, GridStackWidget } from "gridstack";
2+
import { createContext, useContext } from "react";
3+
4+
export const GridStackContext = createContext<{
5+
initialOptions: GridStackOptions;
6+
gridStack: GridStack | null;
7+
addWidget: (fn: (id: string) => Omit<GridStackWidget, "id">) => void;
8+
removeWidget: (id: string) => void;
9+
addSubGrid: (
10+
fn: (
11+
id: string,
12+
withWidget: (w: Omit<GridStackWidget, "id">) => GridStackWidget
13+
) => Omit<GridStackWidget, "id">
14+
) => void;
15+
saveOptions: () => GridStackOptions | GridStackWidget[] | undefined;
16+
17+
_gridStack: {
18+
value: GridStack | null;
19+
set: React.Dispatch<React.SetStateAction<GridStack | null>>;
20+
};
21+
_rawWidgetMetaMap: {
22+
value: Map<string, GridStackWidget>;
23+
set: React.Dispatch<React.SetStateAction<Map<string, GridStackWidget>>>;
24+
};
25+
} | null>(null);
26+
27+
export function useGridStackContext() {
28+
const context = useContext(GridStackContext);
29+
if (!context) {
30+
throw new Error(
31+
"useGridStackContext must be used within a GridStackProvider"
32+
);
33+
}
34+
return context;
35+
}

react/lib/grid-stack-provider.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { GridStack, GridStackOptions, GridStackWidget } from "gridstack";
2+
import { type PropsWithChildren, useCallback, useState } from "react";
3+
import { GridStackContext } from "./grid-stack-context";
4+
5+
export function GridStackProvider({
6+
children,
7+
initialOptions,
8+
}: PropsWithChildren<{ initialOptions: GridStackOptions }>) {
9+
const [gridStack, setGridStack] = useState<GridStack | null>(null);
10+
const [rawWidgetMetaMap, setRawWidgetMetaMap] = useState(() => {
11+
const map = new Map<string, GridStackWidget>();
12+
const deepFindNodeWithContent = (obj: GridStackWidget) => {
13+
if (obj.id && obj.content) {
14+
map.set(obj.id, obj);
15+
}
16+
if (obj.subGridOpts?.children) {
17+
obj.subGridOpts.children.forEach((child: GridStackWidget) => {
18+
deepFindNodeWithContent(child);
19+
});
20+
}
21+
};
22+
initialOptions.children?.forEach((child: GridStackWidget) => {
23+
deepFindNodeWithContent(child);
24+
});
25+
return map;
26+
});
27+
28+
const addWidget = useCallback(
29+
(fn: (id: string) => Omit<GridStackWidget, "id">) => {
30+
const newId = `widget-${Math.random().toString(36).substring(2, 15)}`;
31+
const widget = fn(newId);
32+
gridStack?.addWidget({ ...widget, id: newId });
33+
setRawWidgetMetaMap((prev) => {
34+
const newMap = new Map<string, GridStackWidget>(prev);
35+
newMap.set(newId, widget);
36+
return newMap;
37+
});
38+
},
39+
[gridStack]
40+
);
41+
42+
const addSubGrid = useCallback(
43+
(
44+
fn: (
45+
id: string,
46+
withWidget: (w: Omit<GridStackWidget, "id">) => GridStackWidget
47+
) => Omit<GridStackWidget, "id">
48+
) => {
49+
const newId = `sub-grid-${Math.random().toString(36).substring(2, 15)}`;
50+
const subWidgetIdMap = new Map<string, GridStackWidget>();
51+
52+
const widget = fn(newId, (w) => {
53+
const subWidgetId = `widget-${Math.random()
54+
.toString(36)
55+
.substring(2, 15)}`;
56+
subWidgetIdMap.set(subWidgetId, w);
57+
return { ...w, id: subWidgetId };
58+
});
59+
60+
gridStack?.addWidget({ ...widget, id: newId });
61+
62+
setRawWidgetMetaMap((prev) => {
63+
const newMap = new Map<string, GridStackWidget>(prev);
64+
subWidgetIdMap.forEach((meta, id) => {
65+
newMap.set(id, meta);
66+
});
67+
return newMap;
68+
});
69+
},
70+
[gridStack]
71+
);
72+
73+
const removeWidget = useCallback(
74+
(id: string) => {
75+
gridStack?.removeWidget(id);
76+
setRawWidgetMetaMap((prev) => {
77+
const newMap = new Map<string, GridStackWidget>(prev);
78+
newMap.delete(id);
79+
return newMap;
80+
});
81+
},
82+
[gridStack]
83+
);
84+
85+
const saveOptions = useCallback(() => {
86+
return gridStack?.save(true, true, (_, widget) => widget);
87+
}, [gridStack]);
88+
89+
return (
90+
<GridStackContext.Provider
91+
value={{
92+
initialOptions,
93+
gridStack,
94+
95+
addWidget,
96+
removeWidget,
97+
addSubGrid,
98+
saveOptions,
99+
100+
_gridStack: {
101+
value: gridStack,
102+
set: setGridStack,
103+
},
104+
_rawWidgetMetaMap: {
105+
value: rawWidgetMetaMap,
106+
set: setRawWidgetMetaMap,
107+
},
108+
}}
109+
>
110+
{children}
111+
</GridStackContext.Provider>
112+
);
113+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createContext, useContext } from "react";
2+
3+
export const GridStackRenderContext = createContext<{
4+
getWidgetContainer: (widgetId: string) => HTMLElement | null;
5+
} | null>(null);
6+
7+
export function useGridStackRenderContext() {
8+
const context = useContext(GridStackRenderContext);
9+
if (!context) {
10+
throw new Error(
11+
"useGridStackRenderContext must be used within a GridStackProvider"
12+
);
13+
}
14+
return context;
15+
}

0 commit comments

Comments
 (0)