-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
react demo with more use case and readme #2898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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({ | ||
| 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 | ||
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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