Skip to main content

Inertia Core Modals

Overview

Inly Core provides an abstraction layer over InertiaUI Modal (@inertiaui/modal-react) that integrates with our design system and server-side configuration. Key Components:
  • CoreModal (React): Wraps InertiaUI’s HeadlessModal and renders our UI components
  • CoreTableModal (React): Specialized modal for table actions that automatically merges row and bulk action IDs
  • CoreModalPage (PHP): Server-side page configuration for modals
  • CoreModalLink: React component for opening modals via links
Reference: For complete InertiaUI Modal documentation, see inertiaui.com/docs/modal

Differences from InertiaUI Modal

1. Server-Side Configuration

InertiaUI Modal: Configuration happens entirely on the client via visitModal() options. Inly Core: Configuration can be set server-side using CoreModalPage:
use Inly\Core\Pages\CoreModalPage;
use Inly\Core\Enums\SheetPosition;

return CoreModalPage::make('users/create')
    ->title(__('New user'))
    ->description(__('Create a new user account'))
    ->sheet()
    ->position(SheetPosition::LEFT)
    ->with(['users' => $users]);
The configuration is automatically passed to the client via pageData prop.

2. Automatic Title & Description

InertiaUI Modal: Requires manually passing title/description as props or handling them yourself. Inly Core: Automatically reads title and description from server-side pageData:
export default function UserCreate() {
  return (
    <CoreModal>
      {/* Title and description automatically read from pageData */}
      <UserForm />
    </CoreModal>
  );
}
On the server:
CoreModalPage::make('users/create')
    ->title(__('New user'))
    ->description(__('Description'))  // Modal description

3. Type-Safe Sheet Position

InertiaUI Modal: Uses string literals for position: 'right' | 'left' | 'top' | 'bottom' Inly Core: Uses PHP enum SheetPosition:
use Inly\Core\Enums\SheetPosition;

->position(SheetPosition::LEFT)  // Type-safe enum
->position('left')                // String also works with validation

4. Unified API for Dialogs & Sheets

InertiaUI Modal: Different component APIs for dialog vs slideover modes. Inly Core: Single CoreModal component handles both automatically based on configuration:
// Same component for both
<CoreModal title="Example" description="Description">
  {/* Content */}
</CoreModal>

// Becomes dialog by default
// Becomes sheet if sheet: true is configured

5. Default Max Width via className

InertiaUI Modal: Uses maxWidth prop for sizing. Inly Core: Uses className prop mapped to maxWidth internally:
<CoreModal className="max-w-sm">{/* Auto-configures modal size */}</CoreModal>

6. Dialog & Sheet Props Merging

InertiaUI Modal: Requires separate handling of dialog vs sheet props. Inly Core: Sheet/Dialog props can be passed via coreSheetProps and coreDialogProps:
<CoreModal
  coreDialogProps={{ footer: <CustomFooter /> }}
  coreSheetProps={{ side: 'left' }}
>
  {children}
</CoreModal>

Usage

Opening Modals

You can open modals using either:
  1. Client-side: Use visitModal() from useModalStack() hook
  2. Server-side: Return CoreModalPage from your controller when request()->boolean('modal') is true

Client-Side Opening

import { useModalStack } from '@inertiaui/modal-react';
import { Button } from '@/core/components/ui/button';

export default function ExampleOpeners() {
  const { visitModal } = useModalStack();

  return (
    <div className="flex gap-2">
      <Button onClick={() => visitModal(route('users.create'))}>
        {__('Open dialog')}
      </Button>
      <Button
        onClick={() =>
          visitModal(route('filters.panel'), {
            slideover: true, // InertiaUI's prop name (we use 'sheet' internally)
            position: 'right',
          })
        }
      >
        {__('Open sheet')}
      </Button>
    </div>
  );
}
import { CoreModalLink } from '@/core/components/core-modal';
import { Button } from '@/core/components/ui/button';

<CoreModalLink href={route('users.create')} modalClassName="max-w-sm">
  <Button>Open Modal</Button>
</CoreModalLink>;

Server-Side Configuration

Use CoreModalPage in your controllers:
use Inly\Core\Pages\CoreModalPage;
use Inly\Core\Enums\SheetPosition;

public function create()
{
    if (request()->boolean('modal')) {
        return CoreModalPage::make('users/create')
            ->title(__('New user'))
            ->description(__('Create a new user account'))
            ->sheet()
            ->position(SheetPosition::LEFT)
            ->with(['users' => $users]);
    }

    // Regular page
    return CoreShowPage::make('users/create')
        ->title(__('New user'))
        ->with(['users' => $users]);
}

React Component Usage

The CoreModal component automatically reads configuration from pageData:
import { CoreModal } from '@/core/components/core-modal';

export default function UserCreate() {
  return (
    <CoreModal>
      {/* Title and description automatically read from pageData */}
      <UserForm />
    </CoreModal>
  );
}
Props can override pageData:
export default function UserCreate() {
  return (
    <CoreModal
      coreDialogProps={{
        title: __('Custom Title'), // Overrides pageData.title
        description: __('Custom Desc'), // Overrides pageData.description
      }}
    >
      <UserForm />
    </CoreModal>
  );
}

Function Children & Modal Context

Access modal context using function children:
export default function UserCreate() {
  return (
    <CoreModal>
      {({ close, reload, isOpen, config }) => (
        <div>
          <p>Modal is currently: {isOpen ? 'open' : 'closed'}</p>
          <Button onClick={() => close()}>{__('Close')}</Button>
          <Button onClick={() => reload()}>{__('Reload')}</Button>
        </div>
      )}
    </CoreModal>
  );
}

API Reference

CoreModalPage (PHP)

Namespace: Inly\Core\Pages\CoreModalPage Methods:
  • make(string $component): static - Create a new modal page
  • title(?string $title): static - Set modal title
  • description(?string $description): static - Set modal description
  • sheet(bool $sheet = true, SheetPosition|string|null $position = null): static - Enable sheet mode with optional position
  • position(SheetPosition|string $position): static - Set sheet position (only valid when sheet is enabled)
  • with(array $data): static - Add additional data to the page
  • fromDomainObject(DomainObjectContract|string $domainObject): static - Configure from domain object
Example:
// Set position in sheet() method
CoreModalPage::make('users/create')
    ->title(__('New user'))
    ->description(__('Create a new user account'))
    ->sheet(true, SheetPosition::LEFT)
    ->with(['roles' => $roles]);

// Or use separate position() method
CoreModalPage::make('users/create')
    ->title(__('New user'))
    ->description(__('Create a new user account'))
    ->sheet()
    ->position(SheetPosition::LEFT)
    ->with(['roles' => $roles]);

CoreModal (React)

Props: Extends HeadlessModalProps with these additions:
  • className?: string - Max width class (e.g., 'max-w-sm')
  • coreSheetProps?: Omit<CoreSheetProps, 'open' | 'onOpenChange' | 'children'> - Props for sheet mode
  • coreDialogProps?: Omit<CoreDialogProps, 'open' | 'onOpenChange' | 'children'> - Props for dialog mode
Automatically reads from pageData:
  • title - Modal title
  • description - Modal description
  • sheet - Whether to render as sheet
  • position - Sheet position

CoreTableModal (React)

Props: Extends CoreModalProps (excluding children) with these additions:
  • selectedItems: string[] - Array of selected item IDs from bulk actions (required)
  • children: (args: { ids: string[]; close: () => void }) => React.ReactNode - Render prop that receives:
    • ids: string[] - Merged array of IDs (either [rowActionId] or selectedItems)
    • close: () => void - Function to close the modal (automatically resets rowActionId)
Behavior:
  • Automatically merges rowActionId (from useTableAction()) and selectedItems prop
  • If rowActionId exists, uses [rowActionId]; otherwise uses selectedItems
  • Resets rowActionId when modal closes
  • All other props are passed through to CoreModal

SheetPosition Enum

Namespace: Inly\Core\Enums\SheetPosition Cases:
  • SheetPosition::RIGHT - Slide from right (default)
  • SheetPosition::LEFT - Slide from left
  • SheetPosition::TOP - Slide from top
  • SheetPosition::BOTTOM - Slide from bottom
Usage:
->position(SheetPosition::LEFT)  // Type-safe enum
->position('left')                // String with validation

Data Flow

Complete Example

1. Controller (Server-side):
public function create()
{
    if (request()->boolean('modal')) {
        return CoreModalPage::make('users/create')
            ->title(__('New user'))
            ->description(__('Create a new user account'))
            ->with(['roles' => $roles]);
    }

    return CoreShowPage::make('users/create')
        ->title(__('New user'))
        ->with(['roles' => $roles]);
}
2. Component (Client-side):
export default function UserCreate({ roles }) {
  return (
    <CoreModal>
      <UserForm roles={roles} />
    </CoreModal>
  );
}
3. Link to open modal:
<CoreModalLink href={route('users.create')} modalClassName="max-w-sm">
  <Button>New User</Button>
</CoreModalLink>

Flow Summary

  1. User clicks CoreModalLink → triggers visitModal() with modal config
  2. Request includes ?modal=true parameter
  3. Controller returns CoreModalPage with configuration
  4. Configuration is passed to React via pageData prop
  5. CoreModal reads pageData and renders as dialog or sheet
  6. Title and description are rendered automatically

CoreTableModal

CoreTableModal is a specialized wrapper around CoreModal designed for table actions. It automatically merges IDs from row actions and bulk actions, eliminating the need to manually handle ID merging logic in your dialog components.

When to Use CoreTableModal

Use CoreTableModal when:
  • You need a modal that works with both row actions (single item) and bulk actions (multiple items)
  • You want to avoid manually merging rowActionId and selectedItems in every dialog component
  • Your modal needs to update or operate on table rows

How It Works

CoreTableModal:
  1. Uses useTableAction() hook to access rowActionId (from row actions)
  2. Accepts selectedItems prop (from bulk actions via table state)
  3. Automatically merges them: if rowActionId exists, uses [rowActionId], otherwise uses selectedItems
  4. Provides merged ids array to children via render prop
  5. Handles cleanup of rowActionId when modal closes

Usage Example

In your table component:
import { CoreTableModal } from '@/core/components/core-modal';
import { InertiaTable } from '@/core/components/inertia-table';

export default function UsersIndex({ usersTable }) {
  return (
    <InertiaTable
      resource={usersTable}
      modals={({ actions }) => (
        <CoreTableModal
          name="update-user-status"
          selectedItems={actions.selectedItems}
          coreDialogProps={{
            title: __('Update status'),
            description: __('Update the status for selected users'),
          }}
        >
          {({ ids, close }) => (
            <UpdateUserStatusDialog ids={ids} close={close} />
          )}
        </CoreTableModal>
      )}
    />
  );
}
In your dialog component:
import {
  CoreForm,
  CoreFormSelect,
  CoreFormSubmitButton,
} from '@/core/components/core-form';
import { useForm } from '@inertiajs/react';

interface Props {
  ids: string[];
  close: () => void;
}

export default function UpdateUserStatusDialog({ ids, close }: Props) {
  const form = useForm({
    ids,
    status: '',
  });

  function handleSubmit() {
    form.put(route('users.update'), {
      onSuccess: () => {
        form.reset();
        close();
      },
    });
  }

  return (
    <CoreForm form={form} onSubmit={handleSubmit}>
      <CoreFormSelect
        name="status"
        label={__('Status')}
        options={statusOptions}
        required
      />
      <CoreFormSubmitButton>{__('Update')}</CoreFormSubmitButton>
    </CoreForm>
  );
}

Server-Side Handling with WithTableQuery

When using CoreTableModal for bulk operations, you can use the WithTableQuery trait in your Form Request classes to handle both single item updates (via route parameters) and bulk updates (via ids array) seamlessly. The trait automatically derives the model from your table’s resource builder. In your Form Request:
use Inly\Core\Http\Requests\Traits\WithTableQuery;
use Illuminate\Foundation\Http\FormRequest;
use InertiaUI\Table\Table;
use App\Tables\UserTable;

class UpdateUserStatusRequest extends FormRequest
{
    use WithTableQuery;

    protected function getTable(): Table|string
    {
        return UserTable::class;
    }

    public function rules(): array
    {
        return [
            'status' => ['required', 'string', Rule::in(['active', 'inactive'])],
        ];
    }
}
In your controller:
public function update(UpdateUserStatusRequest $request)
{
    $query = $request->getTableQuery();
    $validated = $request->validated(); // Automatically excludes 'ids'

    $query->each(fn ($user) => $user->update($validated));

    return back()->withSuccess(__('Users updated successfully'));
}
Key features of WithTableQuery:
  • getTable(): Returns the table class or instance used for bulk operations (required abstract method)
  • getTableQuery(): Returns a query builder scoped to the provided IDs or route parameter, handling both single and bulk updates. Automatically derives the model from the table’s resource builder.
  • getRouteKeyName(): Returns the route key name (defaults to 'id'). Override this method if your route uses a different parameter name.
  • validated(): Automatically excludes the ids field from validated data, so you only get the fields you need to update
  • Route parameter detection: Automatically detects if a route parameter exists (single item) vs bulk update (ids array)
  • Automatic model resolution: The model is automatically retrieved from $table->resourceBuilder()->getModel(), so you don’t need to specify it separately
Handling “select all” (*): The trait also supports the special '*' value in the IDs array, which represents “select all items”. When '*' is present as the first ID, getTableQuery() applies table filters and search from the previous URL:
// Frontend sends: { ids: ['*'], status: 'active' }
$query = $request->getTableQuery(); // Returns query with table filters/search applied
$query->lazy()->each->update($request->validated()); // Updates all filtered users
Error handling: If neither a route parameter nor ids array is provided, the trait will abort with a 400 status code and the message “Missing route parameter or ids for bulk update”.

Props

CoreTableModal extends CoreModalProps (excluding children) and adds:
  • selectedItems: string[] - Array of selected item IDs from bulk actions (required)
  • children: (args: { ids: string[]; close: () => void }) => React.ReactNode - Render prop that receives:
    • ids: string[] - Merged array of IDs (either [rowActionId] or selectedItems)
    • close: () => void - Function to close the modal (automatically resets rowActionId)

Benefits

  • No manual ID merging: Automatically handles the logic of combining row and bulk action IDs
  • Cleaner components: Dialog components don’t need to import useTableAction or handle ID merging
  • Consistent behavior: Ensures ID merging works the same way across all table modals
  • Automatic cleanup: Resets rowActionId when modal closes

Comparison with CoreModal

Use CoreModal when:
  • Modal is not related to table actions
  • You need full control over modal content and behavior
  • Modal doesn’t need to work with table row/bulk selections
Use CoreTableModal when:
  • Modal is triggered from table actions (row or bulk)
  • You need to operate on selected table items
  • You want automatic ID merging without manual logic

Additional Notes

  • Server-side configuration is preferred: Use CoreModalPage for consistency and easier maintenance
  • Props override pageData: React props always take precedence over server configuration
  • Position validation: Passing position() without sheet() throws an exception
  • Modal detection: Controllers should check request()->boolean('modal') to return CoreModalPage vs regular page
  • For local state modals: Use CoreDialog or CoreSheet directly (not route-driven)
  • Sheet vs Slideover: We use “sheet” in our API, which maps to InertiaUI’s “slideover” prop internally
  • Table modals: Use CoreTableModal for modals that work with table actions to automatically handle ID merging