Edit Modal
Action::edit() opens a configured edit modal containing an auto-generated Symfony Form. The bundle ships with Bootstrap 5 templates and a Bootstrap modal adapter by default, but both the Twig template and the JavaScript adapter can be overridden.
Requirements
symfony/formmust be installed (composer require symfony/form)symfony/twig-bundlemust be installed to render the modal templates- Bootstrap is only required when you use the default Bootstrap modal adapter
How It Works
- User clicks the Edit button on a row
- The Stimulus controller fetches a pre-filled form from
GET /datatables/ajax/edit-form - The configured modal adapter opens the returned modal HTML
- On submit, a
POST /datatables/ajax/edit-formvalidates and persists the changes - If the form is invalid, only the modal body is re-rendered and replaced in place
- On success, the modal closes and the table reloads automatically (with Mercure broadcast if enabled)
Modal Template Overrides
Override the edit modal Twig template globally or per table.
Global Configuration
# config/packages/data_tables.yaml
data_tables:
edit_modal:
template: '@DataTables/modal/bootstrap5/edit_modal.html.twig'
body_template: '@DataTables/modal/_form_body_bs5.html.twig'
default_title: 'Edit'
Per-Table via Attribute
#[AsDataTable(
entityClass: Product::class,
editModalTemplate: 'datatables/product_edit_modal.html.twig',
)]
final class ProductsDataTable extends AbstractDataTable
{
}
Per-Table via configureDataTable()
protected function configureDataTable(DataTable $table): DataTable
{
return $table
->editModalTemplate('datatables/product_edit_modal.html.twig')
->editModalAdapter('tw');
}
Required DOM Hooks
Custom modal templates must preserve these attributes:
| Attribute / ID | Element | Purpose |
|---|---|---|
| `data-ux-datatables-modal` | Modal root | Used by the adapter to open/close the modal |
| `data-ux-datatables-modal-body` | Body container | Replaced in-place after validation errors |
| `data-ux-datatables-submit` | Submit button | Wired to the form submit handler |
| `id="ux-datatables-edit-form"` | `<form>` element | Required for form serialization |
Column-to-Form Mapping
The form builder maps each column to a Symfony Form type:
| Column Pattern | Form Type |
|---|---|
| `BooleanColumn` (`renderAsSwitch`) | `CheckboxType` |
| `ChoiceColumn` (has `choices`) | `ChoiceType` |
| `DateColumn` (has `dateFormat`) | `DateType` (single_text widget) |
| `NumberColumn` (`num`, `num-fmt`, …) | `NumberType` |
| `TextColumn` (`string`, `string-utf8`) | `TextType` |
| `html` type | `TextareaType` |
| `ActionColumn`, `TemplateColumn`, `UrlColumn` | Skipped automatically |
Primary Key Fields
Columns matching the entity’s Doctrine identifier are rendered as disabled fields in the form. They are visible for reference but cannot be modified.
Excluding Columns from the Form
Use hideWhenUpdating() on any column to exclude it from the edit modal entirely:
public function configureColumns(): iterable
{
yield NumberColumn::new('id', 'ID');
yield TextColumn::new('name', 'Name');
yield DateColumn::new('createdAt', 'Created')
->hideWhenUpdating(); // Excluded from the edit form
}
Ajax Endpoints
GET /datatables/ajax/edit-form— returns the rendered modal HTMLPOST /datatables/ajax/edit-form— validates and persists the form data, returns only the modal body HTML when invalid
These endpoints are only available when symfony/form is installed.
Full Example
use App\Entity\Product;
use Pentiminax\UX\DataTables\Attribute\AsDataTable;
use Pentiminax\UX\DataTables\Column\BooleanColumn;
use Pentiminax\UX\DataTables\Column\NumberColumn;
use Pentiminax\UX\DataTables\Column\TextColumn;
use Pentiminax\UX\DataTables\Model\AbstractDataTable;
use Pentiminax\UX\DataTables\Model\Action;
use Pentiminax\UX\DataTables\Model\Actions;
#[AsDataTable(Product::class)]
final class ProductsDataTable extends AbstractDataTable
{
public function configureColumns(): iterable
{
yield NumberColumn::new('id', 'ID');
yield TextColumn::new('name', 'Name');
yield NumberColumn::new('price', 'Price');
yield BooleanColumn::new('active', 'Active');
}
public function configureActions(Actions $actions): Actions
{
return $actions->add(
Action::edit('Edit')
->setIcon('bi bi-pencil')
);
}
}
Custom Modal Adapter
Implement the ModalAdapter interface and register it via modalAdapters.
// assets/modal/dialog-adapter.ts
import type { ModalAdapter, ModalHandlers } from '@pentiminax/ux-datatables/modal/ModalAdapter'
export class DialogModalAdapter implements ModalAdapter {
private dialog: HTMLDialogElement | null = null
private handlers: ModalHandlers | null = null
async show(html: string, handlers: ModalHandlers): Promise<void> {
this.handlers = handlers
const template = document.createElement('template')
template.innerHTML = html.trim()
const root = template.content.querySelector<HTMLElement>('[data-ux-datatables-modal]')
if (!root) return
this.dialog = root as HTMLDialogElement
document.body.appendChild(this.dialog)
this.dialog.querySelector('[data-ux-datatables-submit]')
?.addEventListener('click', async () => {
const form = this.dialog!.querySelector<HTMLFormElement>('#ux-datatables-edit-form')
if (form) await handlers.onSubmit(Object.fromEntries(new FormData(form)))
})
this.dialog.addEventListener('close', () => handlers.onCancel?.())
this.dialog.showModal()
}
replaceBody(html: string): void {
const body = this.dialog?.querySelector('[data-ux-datatables-modal-body]')
if (body) body.innerHTML = html
}
async hide(): Promise<void> {
this.dialog?.close()
this.dialog?.remove()
this.dialog = null
}
isOpen(): boolean {
return this.dialog?.open ?? false
}
}
Then register it once (e.g. in your app entrypoint):
import { modalAdapters } from '@pentiminax/ux-datatables/modal/ModalAdapterRegistry'
import { DialogModalAdapter } from './modal/dialog-adapter'
modalAdapters.register('dialog', () => new DialogModalAdapter())
Activate it globally via config or per-table:
# config/packages/data_tables.yaml
data_tables:
edit_modal:
adapter: 'dialog'
// Per-table
protected function configureDataTable(DataTable $table): DataTable
{
return $table->editModalAdapter('dialog');
}