Skip to content

Commit c1e7853

Browse files
feat: add F0GridStack component (#2994)
1 parent 6d604c1 commit c1e7853

17 files changed

+1768
-0
lines changed

packages/react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
"embla-carousel-autoplay": "^8.5.2",
237237
"embla-carousel-react": "^8.6.0",
238238
"embla-carousel-wheel-gestures": "^8.0.1",
239+
"gridstack": "^12.3.3",
239240
"prosemirror-state": "^1.4.3",
240241
"prosemirror-view": "^1.38.1",
241242
"react-remove-scroll": "^2.7.1",
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
GridItemHTMLElement,
3+
GridStackOptions,
4+
GridStackWidget,
5+
} from "gridstack"
6+
import "gridstack/dist/gridstack.css"
7+
import { forwardRef, useImperativeHandle, useMemo } from "react"
8+
import { useGridStackContext } from "./components/grid-stack-context"
9+
import { GridStackProvider } from "./components/grid-stack-provider"
10+
import { GridStackRender } from "./components/grid-stack-render"
11+
import { GridStackRenderProvider } from "./components/grid-stack-render-provider"
12+
13+
export type GridStackReactOptions = Omit<GridStackOptions, "children">
14+
15+
export type GridStackReactSize = { w: number; h: number }
16+
17+
export interface GridStackReactWidget extends GridStackWidget {
18+
id: Required<GridStackWidget>["id"]
19+
allowedSizes?: GridStackReactSize[]
20+
renderFn?: () => React.ReactElement
21+
}
22+
23+
/**
24+
* Represents a node in the grid layout.
25+
*/
26+
export interface GridStackWidgetPosition {
27+
id: string
28+
w: number
29+
h: number
30+
x: number
31+
y: number
32+
meta?: Record<string, unknown>
33+
}
34+
35+
export interface F0GridStackProps {
36+
options: GridStackReactOptions
37+
widgets: GridStackReactWidget[]
38+
onChange?: (layout: GridStackWidgetPosition[]) => void
39+
}
40+
41+
/**
42+
* Methods exposed via ref to control the grid programmatically.
43+
* @example
44+
* ```tsx
45+
* const gridRef = useRef<F0GridStackRef>(null)
46+
*
47+
* // Add a widget
48+
* gridRef.current?.addWidget({
49+
* id: 'new-widget',
50+
* w: 2,
51+
* h: 2,
52+
* renderFn: () => <div>Content</div>
53+
* meta: {
54+
* // Your metadata associated with the widget
55+
* }
56+
* })
57+
*
58+
* // Remove a widget
59+
* gridRef.current?.removeWidget('widget-id')
60+
*
61+
* // Remove all widgets
62+
* gridRef.current?.removeAll()
63+
*
64+
* // Save current layout
65+
* const layout = gridRef.current?.saveOptions()
66+
* ```
67+
*/
68+
export interface F0GridStackRef {
69+
addWidget: (widget: GridStackReactWidget) => void
70+
removeWidget: (id: string) => void
71+
addSubGrid: (
72+
subGrid: GridStackReactWidget & {
73+
id: Required<GridStackWidget>["id"]
74+
subGridOpts: Required<GridStackWidget>["subGridOpts"] & {
75+
children: Array<
76+
GridStackWidget & { id: Required<GridStackWidget>["id"] }
77+
>
78+
}
79+
}
80+
) => void
81+
removeAll: () => void
82+
}
83+
84+
const RefHandler = forwardRef<F0GridStackRef, object>((_, ref) => {
85+
const context = useGridStackContext()
86+
87+
useImperativeHandle(
88+
ref,
89+
() => ({
90+
addWidget: context.addWidget,
91+
removeWidget: context.removeWidget,
92+
addSubGrid: context.addSubGrid,
93+
removeAll: context.removeAll,
94+
}),
95+
[context]
96+
)
97+
98+
return null
99+
})
100+
101+
RefHandler.displayName = "RefHandler"
102+
103+
export const F0GridStack = forwardRef<F0GridStackRef, F0GridStackProps>(
104+
({ options, widgets, onChange }, ref) => {
105+
const gridOptions = useMemo(
106+
() => ({
107+
...options,
108+
children: widgets,
109+
}),
110+
// eslint-disable-next-line react-hooks/exhaustive-deps
111+
[widgets]
112+
)
113+
114+
const closestAllowed = (
115+
w: number,
116+
h: number,
117+
allowed: { w: number; h: number }[]
118+
) => {
119+
let best = allowed[0],
120+
bestDist = Infinity
121+
for (const a of allowed) {
122+
const dx = a.w - w,
123+
dy = a.h - h
124+
const dist = dx * dx + dy * dy
125+
if (dist < bestDist) {
126+
bestDist = dist
127+
best = a
128+
}
129+
}
130+
return best
131+
}
132+
133+
const onResizeStop = (_: Event, el: GridItemHTMLElement) => {
134+
// el is the DOM element of the grid item
135+
const node = el.gridstackNode // node contains w,h,x,y
136+
if (!node) return
137+
138+
const allowed = el.gridstackNode?.allowedSizes ?? []
139+
if (allowed.length === 0) {
140+
return
141+
}
142+
143+
const target = closestAllowed(node.w ?? 1, node.h ?? 1, allowed)
144+
145+
if (node.w !== target.w || node.h !== target.h) {
146+
// update will reposition if necessary
147+
node.grid?.update(el, { w: target.w, h: target.h })
148+
}
149+
}
150+
151+
return (
152+
<GridStackProvider
153+
initialOptions={gridOptions}
154+
onResizeStop={onResizeStop}
155+
onChange={onChange}
156+
>
157+
<RefHandler ref={ref} />
158+
<GridStackRenderProvider>
159+
<GridStackRender />
160+
</GridStackRenderProvider>
161+
</GridStackProvider>
162+
)
163+
}
164+
)
165+
166+
F0GridStack.displayName = "F0GridStack"
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { Canvas, Meta, Controls } from "@storybook/addon-docs/blocks"
2+
3+
import * as GridStackStories from "./GridStackReact.stories"
4+
5+
<Meta of={GridStackStories} />
6+
7+
# GridStack
8+
9+
## Introduction
10+
11+
### Definition
12+
13+
F0GridStack is a React wrapper for the GridStack library that enables the
14+
creation of resizable and draggable grid layouts. It provides a flexible,
15+
interactive grid system where widgets can be dynamically arranged, resized, and
16+
repositioned by users or programmatically through ref methods.
17+
18+
### Purpose
19+
20+
The purpose of F0GridStack in our design system is as follows:
21+
22+
- **Interactive Layouts**: Enables users to customize their dashboard or
23+
workspace layouts by dragging and resizing widgets
24+
- **Dynamic Content Management**: Allows programmatic addition and removal of
25+
widgets at runtime
26+
- **Responsive Grid System**: Provides a flexible grid that adapts to different
27+
screen sizes and widget configurations
28+
- **Layout Persistence**: Supports saving and restoring widget positions and
29+
sizes through the onChange callback
30+
- **Nested Grids**: Supports sub-grids for complex, hierarchical layouts
31+
32+
## Basic Usage
33+
34+
The default GridStack configuration demonstrates a simple grid layout with
35+
multiple widgets that can be dragged and resized.
36+
37+
<Canvas of={GridStackStories.Default} />
38+
<Controls of={GridStackStories.Default} />
39+
40+
## Programmatic Control with Ref Methods
41+
42+
F0GridStack exposes several methods through a ref that allow you to
43+
programmatically control the grid layout. This is useful for dynamic widget
44+
management, such as adding or removing widgets based on user actions or
45+
application state.
46+
47+
### Accessing Ref Methods
48+
49+
To use the ref methods, create a ref using `useRef<F0GridStackRef>(null)` and
50+
attach it to the `F0GridStack` component:
51+
52+
```tsx
53+
import { useRef } from "react"
54+
import { F0GridStack, type F0GridStackRef } from "@/components/Utilities/F0GridStack"
55+
56+
const gridRef = useRef<F0GridStackRef>(null)
57+
58+
<F0GridStack ref={gridRef} options={{ column: 12 }} widgets={[]} />
59+
```
60+
61+
### Available Methods
62+
63+
#### `addWidget`
64+
65+
Adds a new widget to the grid programmatically.
66+
67+
**Parameters:**
68+
69+
- `widget`: A `GridStackReactWidget` object with the following properties:
70+
- `id`: Unique identifier for the widget (required)
71+
- `w`: Width in grid columns
72+
- `h`: Height in grid rows
73+
- `x`: X position in grid columns (optional, will auto-place if not provided)
74+
- `y`: Y position in grid rows (optional, will auto-place if not provided)
75+
- `renderFn`: Function that returns the React element to render
76+
- `meta`: Optional metadata object associated with the widget
77+
- `allowedSizes`: Optional array of allowed size combinations
78+
- `allowResize`: Whether the widget can be resized (default: true)
79+
- `allowMove`: Whether the widget can be moved (default: true)
80+
81+
**Example:**
82+
83+
```tsx
84+
gridRef.current?.addWidget({
85+
id: "new-widget-1",
86+
w: 2,
87+
h: 2,
88+
meta: {
89+
title: "New Widget",
90+
type: "chart",
91+
},
92+
renderFn: () => (
93+
<div className="h-full rounded-md bg-f1-background-secondary p-4">
94+
New Widget Content
95+
</div>
96+
),
97+
})
98+
```
99+
100+
#### `removeWidget`
101+
102+
Removes a widget from the grid by its ID.
103+
104+
**Parameters:**
105+
106+
- `id`: The unique identifier of the widget to remove (string)
107+
108+
**Example:**
109+
110+
```tsx
111+
gridRef.current?.removeWidget("widget-1")
112+
```
113+
114+
#### `addSubGrid`
115+
116+
Adds a nested grid (sub-grid) to the main grid. This is useful for creating
117+
hierarchical layouts where widgets can be grouped within a parent widget.
118+
119+
**Parameters:**
120+
121+
- `subGrid`: A `GridStackReactWidget` with additional `subGridOpts` property
122+
containing:
123+
- `children`: Array of widgets that will be placed inside the sub-grid
124+
125+
**Example:**
126+
127+
```tsx
128+
gridRef.current?.addSubGrid({
129+
id: "subgrid-1",
130+
w: 4,
131+
h: 4,
132+
subGridOpts: {
133+
column: 6,
134+
children: [
135+
{
136+
id: "nested-widget-1",
137+
w: 2,
138+
h: 2,
139+
renderFn: () => <div>Nested Widget 1</div>,
140+
},
141+
{
142+
id: "nested-widget-2",
143+
w: 2,
144+
h: 2,
145+
renderFn: () => <div>Nested Widget 2</div>,
146+
},
147+
],
148+
},
149+
renderFn: () => <div>Sub-grid Container</div>,
150+
})
151+
```
152+
153+
#### `removeAll`
154+
155+
Removes all widgets from the grid at once.
156+
157+
**Example:**
158+
159+
```tsx
160+
gridRef.current?.removeAll()
161+
```
162+
163+
### Complete Example
164+
165+
The following example demonstrates all ref methods in action:
166+
167+
<Canvas of={GridStackStories.WithRefMethods} />
168+
169+
This example shows:
170+
171+
- **Add Widget**: Dynamically adds new widgets to the grid
172+
- **Remove Last Widget**: Removes the most recently added widget
173+
- **Remove All**: Clears all widgets from the grid
174+
175+
## Best Practices
176+
177+
1. **Widget IDs**: Always use unique, stable IDs for widgets to ensure proper
178+
tracking and removal
179+
2. **Layout Persistence**: Use the `onChange` callback to save widget positions
180+
and restore them on component mount
181+
3. **Performance**: For large numbers of widgets, consider debouncing the
182+
`onChange` callback
183+
4. **Responsive Design**: Configure appropriate column counts for different
184+
screen sizes using the `options.column` property
185+
5. **Widget Constraints**: Use `allowedSizes` to restrict widget dimensions to
186+
specific size combinations
187+
6. **Metadata**: Store widget-specific data in the `meta` property for easy
188+
retrieval and persistence
189+
190+
## Props
191+
192+
### `options`
193+
194+
Configuration object for the grid layout.
195+
196+
- `column`: Number of columns in the grid (default: 12)
197+
- `row`: Number of rows in the grid (optional)
198+
- `margin`: Margin between widgets in pixels (optional)
199+
- `draggable`: Whether widgets can be dragged (default: true)
200+
- `acceptWidgets`: Whether the grid can accept widgets from other grids
201+
(default: false)
202+
203+
### `widgets`
204+
205+
Array of initial widgets to render in the grid. Each widget follows the
206+
`GridStackReactWidget` interface.
207+
208+
### `onChange`
209+
210+
Callback function that is called whenever the layout changes (widget moved,
211+
resized, added, or removed). Receives an array of `GridStackWidgetPosition`
212+
objects containing the current state of all widgets.

0 commit comments

Comments
 (0)