Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 109 additions & 39 deletions react/README.md
Original file line number Diff line number Diff line change
@@ -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 <div>{content}</div>;
}

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({
Copy link
Member

@adumesny adumesny Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a good reason to have a json structure here instead of just
content: 'Item 1'
I suppose you can make it other thing than name:'text' (should be type) but real apps are likely to use actual components looked up by unique id or something (see angular wrapper 'selector' field) and not content field. see Angular demo with component readme.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I didn't think of doing that at the time. Since initially I wanted to store react component information on the same set of data structures, I'll try to do it your way, and may need to maintain an additional copy of the mapping relationship.

Copy link
Member

@adumesny adumesny Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great, and also try to use the same class names and fields as I have done for Angular wrapper if possible ('selector' is an angular thing since that is the field we use, so pick the native React equivalent).

Maybe @damien-schneider and @GowthamTG can also chime on this so maybe between the 3 of you we can get an official React wrapper as well. I appreciate the help as I don't know React.
Also demo should be more like two.html (maybe still with nested grid) so we make sure dragging between grids also works. The key is to let Gridstack handle the grid/container creation/reparenting/layout and have React get called back when content (or in the Angular case the grid and gridItem as parents also have to be Angular based) needs to be created using native platform way.

https://github.com/gridstack/gridstack.js/tree/master/angular#more-complete-example

name: "Text",
props: { content: "Item 1" },
}),
},
},
})
// ... other grid items
],
};

function App() {
return (
<GridStackProvider initialOptions={gridOptions}>
<!-- Maybe a toolbar here. Access to addWidget and addSubGrid by useGridStackContext() -->

<!-- Grid Stack Root Element -->
<GridStackRenderProvider>
<!-- Grid Stack Default Render -->
<GridStackRender componentMap={COMPONENT_MAP} />
</GridStackRenderProvider>

<!-- Maybe other UI here -->
</GridStackProvider>
);
}
```

- 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 (
<div>
<button onClick={() => addWidget(/* ... */)}>Add Component</button>
<button onClick={() => addSubGrid(/* ... */)}>Add SubGrid</button>
</div>
);
}
```

### 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
32 changes: 0 additions & 32 deletions react/lib/constants.ts

This file was deleted.

35 changes: 35 additions & 0 deletions react/lib/grid-stack-context.ts
Original file line number Diff line number Diff line change
@@ -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<GridStackWidget, "id">) => void;
removeWidget: (id: string) => void;
addSubGrid: (
fn: (
id: string,
withWidget: (w: Omit<GridStackWidget, "id">) => GridStackWidget
) => Omit<GridStackWidget, "id">
) => void;
saveOptions: () => GridStackOptions | GridStackWidget[] | undefined;

_gridStack: {
value: GridStack | null;
set: React.Dispatch<React.SetStateAction<GridStack | null>>;
};
_rawWidgetMetaMap: {
value: Map<string, GridStackWidget>;
set: React.Dispatch<React.SetStateAction<Map<string, GridStackWidget>>>;
};
} | null>(null);

export function useGridStackContext() {
const context = useContext(GridStackContext);
if (!context) {
throw new Error(
"useGridStackContext must be used within a GridStackProvider"
);
}
return context;
}
113 changes: 113 additions & 0 deletions react/lib/grid-stack-provider.tsx
Original file line number Diff line number Diff line change
@@ -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<GridStack | null>(null);
const [rawWidgetMetaMap, setRawWidgetMetaMap] = useState(() => {
const map = new Map<string, GridStackWidget>();
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<GridStackWidget, "id">) => {
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<string, GridStackWidget>(prev);
newMap.set(newId, widget);
return newMap;
});
},
[gridStack]
);

const addSubGrid = useCallback(
(
fn: (
id: string,
withWidget: (w: Omit<GridStackWidget, "id">) => GridStackWidget
) => Omit<GridStackWidget, "id">
) => {
const newId = `sub-grid-${Math.random().toString(36).substring(2, 15)}`;
const subWidgetIdMap = new Map<string, GridStackWidget>();

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<string, GridStackWidget>(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<string, GridStackWidget>(prev);
newMap.delete(id);
return newMap;
});
},
[gridStack]
);

const saveOptions = useCallback(() => {
return gridStack?.save(true, true, (_, widget) => widget);
}, [gridStack]);

return (
<GridStackContext.Provider
value={{
initialOptions,
gridStack,

addWidget,
removeWidget,
addSubGrid,
saveOptions,

_gridStack: {
value: gridStack,
set: setGridStack,
},
_rawWidgetMetaMap: {
value: rawWidgetMetaMap,
set: setRawWidgetMetaMap,
},
}}
>
{children}
</GridStackContext.Provider>
);
}
15 changes: 15 additions & 0 deletions react/lib/grid-stack-render-context.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading