Skip to content

Instantly share code, notes, and snippets.

@arman-h
Last active September 8, 2025 13:02
Show Gist options
  • Select an option

  • Save arman-h/7af4face5429e920e71ebcdd6ae17526 to your computer and use it in GitHub Desktop.

Select an option

Save arman-h/7af4face5429e920e71ebcdd6ae17526 to your computer and use it in GitHub Desktop.
Rails + Interia
// components/ResourceableControls.jsx
import useResourceableStore from '@/stores/resourceableStore'
/**
* This renders the Edit/Save/Cancel buttons.
* It's generic and works with any resource type.
*/
export default function ResourceableControls({
resource, // The resource being edited
resourceType, // Type of resource ('user', 'case', etc)
additionalControls = [] // Extra buttons (optional)
}) {
// Connect to our global store
const { enableEdit, disableEdit, startSaving, getResourceState } = useResourceableStore()
// Get current state for this resource
const resourceState = getResourceState(resourceType, resource.id)
/**
* When user clicks the Edit button
*/
const handleEditClick = () => {
enableEdit(resourceType, resource.id)
}
/**
* When user clicks the Cancel button
*/
const handleCancelClick = () => {
disableEdit(resourceType, resource.id)
}
/**
* When user clicks the Save button
* This tells the store that a save is starting,
* the form will notice and submit via Inertia.
*/
const handleSaveClick = () => {
startSaving(resourceType, resource.id)
}
return (
<div className="resourceable-controls">
<div className="control-buttons flex gap-3">
{/* Show Edit button when NOT editing */}
{!resourceState.isEditing && (
<button
onClick={handleEditClick}
className="btn btn-primary"
disabled={resourceState.isLoading}
>
Edit
</button>
)}
{/* Show Save and Cancel buttons when editing */}
{resourceState.isEditing && (
<>
<button
onClick={handleSaveClick}
disabled={resourceState.isLoading}
className="btn btn-success"
>
{resourceState.isLoading ? 'Saving...' : 'Save'}
</button>
<button
onClick={handleCancelClick}
disabled={resourceState.isLoading}
className="btn btn-secondary"
>
Cancel
</button>
</>
)}
{/* Render any additional buttons passed in */}
{additionalControls.map((AdditionalControl, index) => (
<AdditionalControl
key={index}
resource={resource}
resourceType={resourceType}
isEditing={resourceState.isEditing}
isLoading={resourceState.isLoading}
/>
))}
</div>
</div>
)
}
// components/ResourceableShow.jsx
import ResourceableControls from "./ResourceableControls"
/**
* This is a GENERIC wrapper that can be used for ANY resource type.
* It renders the Edit/Save/Cancel buttons and connects them to the store.
*
* Usage:
* <ResourceableShow resource={user} resourceType="user">
* <UserForm user={user} roles={roles} />
* </ResourceableShow>
*/
export default function ResourceableShow({
resource, // The resource object (user, case, project, etc)
resourceType, // String identifying the type ('user', 'case', etc)
children, // The form component to render inside
additionalControls = [] // Extra buttons to show (optional)
}) {
return (
<div className="resourceable-container">
{/* The Edit/Save/Cancel buttons */}
<ResourceableControls
resource={resource}
resourceType={resourceType}
additionalControls={additionalControls}
/>
{/* The actual form (UserForm, CaseForm, etc) goes here */}
<div className="resourceable-content">
{children}
</div>
</div>
)
}
// components/UserForm.jsx
import { useEffect } from 'react'
import { useForm } from '@inertiajs/react'
import useResourceableStore from '@/stores/resourceableStore'
/**
* UserForm is responsible for rendering and submitting
* the form to update a user.
*
* This form reacts to store changes (edit/save state).
*/
export default function UserForm({ user, roles }) {
const { data, setData, put, errors } = useForm({
name: user.name || '',
email: user.email || '',
role: user.role || '',
})
const { getResourceState, finishSaving, disableEdit } = useResourceableStore()
const resourceState = getResourceState('user', user.id)
/**
* Watch for saveTriggered from the store.
* When Save is clicked, we submit the form via Inertia.
*/
useEffect(() => {
if (resourceState.saveTriggered) {
put(route('users.update', user.id), {
preserveScroll: true,
onSuccess: () => {
// Let the store know saving is complete
finishSaving('user', user.id)
},
onError: () => {
// Stop loading but stay in edit mode to fix errors
finishSaving('user', user.id)
enableEdit('user', user.id)
}
})
}
}, [resourceState.saveTriggered])
/**
* Handle field changes
*/
const handleChange = (e) => {
setData(e.target.name, e.target.value)
}
return (
<form className="user-form space-y-4">
{/* Name field */}
<div>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
value={data.name}
onChange={handleChange}
disabled={!resourceState.isEditing || resourceState.isLoading}
className="input"
/>
{errors.name && <div className="text-red-500">{errors.name}</div>}
</div>
{/* Email field */}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
value={data.email}
onChange={handleChange}
disabled={!resourceState.isEditing || resourceState.isLoading}
className="input"
/>
{errors.email && <div className="text-red-500">{errors.email}</div>}
</div>
{/* Role field */}
<div>
<label htmlFor="role">Role</label>
<select
id="role"
name="role"
value={data.role}
onChange={handleChange}
disabled={!resourceState.isEditing || resourceState.isLoading}
className="select"
>
{roles.map((role) => (
<option key={role} value={role}>
{role}
</option>
))}
</select>
{errors.role && <div className="text-red-500">{errors.role}</div>}
</div>
</form>
)
}
// stores/resourceableStore.js
import { create } from 'zustand'
/**
* This store manages the UI state for ANY type of form (user, case, project, etc).
* It tracks whether a form is in "edit mode" or "view mode"
* and handles loading states during save operations.
*
* Each resource is identified by a unique key: "resourceType-resourceId"
* Example: "user-123", "case-456", "project-789"
*/
const useResourceableStore = create((set, get) => {
/**
* Helper function to generate unique key for a resource
* @param {string} resourceType - The type of resource (e.g., 'user', 'case', 'project')
* @param {number|string} resourceId - The ID of the specific resource
* @returns {string} Unique key like "user-123"
*/
const getKey = (resourceType, resourceId) => `${resourceType}-${resourceId}`
return {
// This object stores the state for each resource by their unique key
// Example: { "user-123": { isEditing: true }, "case-456": { isEditing: false } }
resourceStates: {},
/**
* Put a resource form into "edit mode" - makes form fields editable
*/
enableEdit: (resourceType, resourceId) => {
const key = getKey(resourceType, resourceId)
set((state) => ({
resourceStates: {
...state.resourceStates,
[key]: {
...state.resourceStates[key],
isEditing: true,
isLoading: false,
},
},
}))
},
/**
* Put a resource form into "view mode" - makes form fields read-only
*/
disableEdit: (resourceType, resourceId) => {
const key = getKey(resourceType, resourceId)
set((state) => ({
resourceStates: {
...state.resourceStates,
[key]: {
...state.resourceStates[key],
isEditing: false,
isLoading: false,
},
},
}))
},
/**
* Show loading spinner during save operations
*/
setLoading: (resourceType, resourceId, isLoading) => {
const key = getKey(resourceType, resourceId)
set((state) => ({
resourceStates: {
...state.resourceStates,
[key]: {
...state.resourceStates[key],
isLoading,
},
},
}))
},
/**
* Tell the form "hey, the user clicked Save, you should submit now!"
*/
triggerSave: (resourceType, resourceId) => {
const key = getKey(resourceType, resourceId)
set((state) => ({
resourceStates: {
...state.resourceStates,
[key]: {
...state.resourceStates[key],
shouldSave: true,
},
},
}))
},
/**
* Clear the "should save" flag after the form has processed it
*/
clearSaveTrigger: (resourceType, resourceId) => {
const key = getKey(resourceType, resourceId)
set((state) => ({
resourceStates: {
...state.resourceStates,
[key]: {
...state.resourceStates[key],
shouldSave: false,
},
},
}))
},
/**
* Get the current state for a specific resource
* Returns default values if no state exists yet
*/
getResourceState: (resourceType, resourceId) => {
const key = getKey(resourceType, resourceId)
return (
get().resourceStates[key] || {
isEditing: false,
isLoading: false,
shouldSave: false,
}
)
},
/**
* Clean up state when component is destroyed
* Prevents memory leaks by removing unused state
*/
clearResourceState: (resourceType, resourceId) => {
const key = getKey(resourceType, resourceId)
set((state) => {
const { [key]: removedState, ...remainingStates } = state.resourceStates
return { resourceStates: remainingStates }
})
},
}
})
export default useResourceableStore
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment