Skip to content

Commit 527ebc5

Browse files
authored
Merge pull request #2783 from damien-schneider/add-react-wrapper
Init react wrapper creation
2 parents 05b6da0 + 2baf515 commit 527ebc5

23 files changed

+1950
-0
lines changed

react/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

react/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# React + TypeScript + Vite
2+
3+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4+
5+
Currently, two official plugins are available:
6+
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
9+
10+
## Expanding the ESLint configuration
11+
12+
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13+
14+
- Configure the top-level `parserOptions` property like this:
15+
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,
23+
},
24+
},
25+
})
26+
```
27+
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+
})
50+
```

react/eslint.config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
7+
export default tseslint.config(
8+
{ ignores: ['dist'] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ['**/*.{ts,tsx}'],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
'react-hooks': reactHooks,
18+
'react-refresh': reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
'react-refresh/only-export-components': [
23+
'warn',
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
},
28+
)

react/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

react/lib/gridstack-context.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// gridstack-context.tsx
2+
"use client";
3+
4+
import * as React from "react";
5+
import type { GridStack } from "gridstack";
6+
import "gridstack/dist/gridstack-extra.css";
7+
import "gridstack/dist/gridstack.css";
8+
import type { ItemRefType } from "./gridstack-item";
9+
10+
type GridStackContextType = {
11+
grid: GridStack | null | undefined;
12+
setGrid: React.Dispatch<React.SetStateAction<GridStack | null>>;
13+
addItemRefToList: (id: string, ref: ItemRefType) => void;
14+
removeItemRefFromList: (id: string) => void;
15+
itemRefList: ItemRefListType;
16+
getItemRefFromListById: (id: string) => ItemRefType | null;
17+
};
18+
19+
type ItemRefListType = {
20+
id: string;
21+
ref: ItemRefType;
22+
}[];
23+
24+
export const GridstackContext = React.createContext<GridStackContextType | null>(null);
25+
26+
export const GridstackProvider = ({ children }: { children: React.ReactNode }) => {
27+
const [grid, setGrid] = React.useState<GridStack | null>(null);
28+
const [itemRefList, setItemRefList] = React.useState<ItemRefListType>([]);
29+
30+
const addItemRefToList = React.useCallback((id: string, ref: ItemRefType) => {
31+
setItemRefList((prev) => [...prev, { id, ref }]);
32+
}, []);
33+
34+
const removeItemRefFromList = React.useCallback((id: string) => {
35+
setItemRefList((prev) => prev.filter((item) => item.id !== id));
36+
}, []);
37+
38+
const getItemRefFromListById = React.useCallback((id: string) => {
39+
const item = itemRefList.find((item) => item.id === id);
40+
return item?.ref ?? null;
41+
}, [itemRefList]);
42+
43+
// Memoize the context value to prevent unnecessary re-renders
44+
const value = React.useMemo(
45+
() => ({
46+
grid,
47+
setGrid,
48+
addItemRefToList,
49+
removeItemRefFromList,
50+
itemRefList,
51+
getItemRefFromListById,
52+
}),
53+
[grid, itemRefList, addItemRefToList, removeItemRefFromList, getItemRefFromListById]
54+
);
55+
56+
return (
57+
<GridstackContext.Provider value={value}>
58+
{children}
59+
</GridstackContext.Provider>
60+
);
61+
};

react/lib/gridstack-grid.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// gridstack-context.tsx
2+
"use client";
3+
4+
import { GridStack, type GridStackOptions } from "gridstack";
5+
import "gridstack/dist/gridstack-extra.css";
6+
import "gridstack/dist/gridstack.css";
7+
8+
import * as React from "react";
9+
import {useGridstackContext} from "./use-gridstack-context";
10+
11+
// Create a context for the GridStack instance
12+
13+
export const GridstackGrid = ({
14+
options,
15+
children,
16+
}: {
17+
options: GridStackOptions;
18+
children: React.ReactNode;
19+
}) => {
20+
const { grid, setGrid } = useGridstackContext();
21+
const containerRef = React.useRef<HTMLDivElement>(null);
22+
const optionsRef = React.useRef<GridStackOptions>(options);
23+
24+
React.useEffect(() => {
25+
optionsRef.current = options;
26+
}, [options]);
27+
28+
React.useLayoutEffect(() => {
29+
if (!grid && containerRef.current) {
30+
const gridInstance = GridStack.init(optionsRef.current, containerRef.current);
31+
setGrid(gridInstance);
32+
}
33+
return () => {
34+
if (grid) {
35+
//? grid.destroy(false);
36+
grid.removeAll(false);
37+
grid.destroy(false);
38+
setGrid(null);
39+
}
40+
};
41+
}, [grid, setGrid]);
42+
43+
return <div ref={containerRef}>{children}</div>;
44+
};

react/lib/gridstack-item.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"use client";
2+
// gridstack-item.tsx
3+
4+
import type {
5+
GridItemHTMLElement,
6+
7+
GridStackWidget,
8+
} from "gridstack";
9+
import * as React from "react";
10+
import {useGridstackContext} from "./use-gridstack-context";
11+
12+
13+
14+
interface GridstackItemComponentProps {
15+
initOptions?: GridStackWidget;
16+
id: string;
17+
children: React.ReactNode;
18+
className?: string;
19+
}
20+
21+
export type ItemRefType = React.MutableRefObject<GridItemHTMLElement | null>;
22+
23+
/**
24+
* Component for rendering a grid item in a Gridstack layout.
25+
*
26+
* @component
27+
* @param {GridstackItemComponentProps} props - The component props.
28+
* @param {GridStackWidget} props.initOptions - The initial options for the grid item.
29+
* @param {string} props.id - The unique identifier for the grid item.
30+
* @param {ReactNode} props.children - The content to be rendered inside the grid item.
31+
* @param {string} props.className - The CSS class name for the grid item.
32+
* @returns {JSX.Element} The rendered grid item component.
33+
*/
34+
export const GridstackItemComponent = ({
35+
initOptions,
36+
id,
37+
children,
38+
className,
39+
}: GridstackItemComponentProps) => {
40+
const containerRef = React.useRef<HTMLDivElement>(null);
41+
const optionsRef = React.useRef<GridStackWidget | undefined>(initOptions);
42+
const { grid, addItemRefToList, removeItemRefFromList } = useGridstackContext();
43+
const itemRef = React.useRef<GridItemHTMLElement | null>(null);
44+
45+
// Update the optionsRef when initOptions changes
46+
React.useEffect(() => {
47+
optionsRef.current = initOptions;
48+
}, [initOptions]);
49+
50+
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
51+
React.useLayoutEffect(() => {
52+
if (!grid || !containerRef.current) return;
53+
if (grid && containerRef.current) {
54+
// Initialize the widget
55+
56+
grid.batchUpdate(true);
57+
itemRef.current = grid.addWidget(containerRef.current, optionsRef.current);
58+
grid.batchUpdate(false);
59+
60+
addItemRefToList(id, itemRef);
61+
62+
63+
// Cleanup function to remove the widget
64+
return () => {
65+
if (grid && itemRef.current) {
66+
try {
67+
grid.batchUpdate(true);
68+
grid.removeWidget(itemRef.current, false);
69+
grid.batchUpdate(false);
70+
71+
removeItemRefFromList(id);
72+
73+
74+
} catch (error) {
75+
console.error("Error removing widget", error);
76+
//! Doing nothing here is a bad practice, but we don't want to crash the app (Temporary fix)
77+
// Do nothing
78+
}
79+
}
80+
};
81+
}
82+
83+
// eslint-disable-next-line react-hooks/exhaustive-deps
84+
}, [grid]);
85+
86+
return (
87+
<div ref={containerRef} id="">
88+
<div className={`w-full h-full ${className}`}>{children}</div>
89+
</div>
90+
);
91+
};

react/lib/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './gridstack-item';
2+
export * from './gridstack-grid';
3+
export * from './gridstack-context';
4+
export * from './use-gridstack-context';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// useGridstackContext.ts
2+
import { useContext } from 'react';
3+
import { GridstackContext } from './gridstack-context'; // Adjust the path as necessary
4+
5+
export const useGridstackContext = () => {
6+
const gridstackContext = useContext(GridstackContext);
7+
if (!gridstackContext) {
8+
throw new Error("useGridstack must be used within a GridstackProvider");
9+
}
10+
return gridstackContext;
11+
}

react/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "react",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"lint": "eslint .",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"gridstack": "^10.3.1",
14+
"react": "^18.3.1",
15+
"react-dom": "^18.3.1"
16+
},
17+
"devDependencies": {
18+
"@eslint/js": "^9.9.0",
19+
"@types/react": "^18.3.3",
20+
"@types/react-dom": "^18.3.0",
21+
"@vitejs/plugin-react-swc": "^3.5.0",
22+
"eslint": "^9.9.0",
23+
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
24+
"eslint-plugin-react-refresh": "^0.4.9",
25+
"globals": "^15.9.0",
26+
"typescript": "^5.5.3",
27+
"typescript-eslint": "^8.0.1",
28+
"vite": "^5.4.1"
29+
}
30+
}

0 commit comments

Comments
 (0)