evo-ui.module-table is the default table/list surface for Evolution CMS
manager modules. It is config-driven and provider-backed.
For the human-facing component guide, standard anatomy, donor module patterns and anti-drift checklist, start with Table.
return [
'key' => 'vendor.module.items',
'provider' => Vendor\Module\Tables\ItemsTableData::class,
'wire_target' => 'search,perPage,applyMultiFilter,setSort,switchView',
'per_page' => 10,
'per_page_options' => [10, 20, 50, 100],
'views' => ['table', 'list'],
'default_view' => 'table',
'default_sort' => 'published_at',
'default_direction' => 'desc',
'storage_key' => null,
'row_states' => [],
'search' => ['enabled' => true, 'state' => 'search'],
'actions' => [],
'filters' => [],
'columns' => [],
'list' => [],
'row_actions' => [],
'inline' => [],
'reorder' => [],
'modal' => [],
];Module tables remember current manager-session state on the server: search, page, per-page count, filters, sorting, direction, and table/list view. This keeps tab switching and iframe refreshes stable without adding a client-side storage protocol to the first release.
The default storage key is generated from the table preset and stable context
values such as type, site, and module.
Use storage_key only when a module needs to intentionally share or isolate the
state beyond the default preset/context boundary:
'storage_key' => 'vendor.module.items.default',Explicit URL parameters (q, page, sort, dir, perPage, f, view)
take priority over session state, so bookmarked or shared manager URLs still
open exactly as requested.
Every row is an array. Required:
[
'id' => 123,
'wire_key' => 'item-row-123',
]Recommended:
[
'edit_url' => 'index.php?...',
'delete_url' => 'index.php?...',
'delete_name' => 'Human-readable name',
]Typed values:
'title' => [
'label' => 'Article title',
'href' => '/article/',
'target' => '_blank',
'strong' => true,
],
'cover' => [
'src' => EVO_SITE_URL . 'assets/example.svg',
'alt' => 'Article title',
],
'tags' => ['Design', 'Release'],
'views' => 153,
'gender' => [
'icon' => 'gender-female',
'tone' => 'female',
'label' => 'Woman',
],Do not pass arbitrary arrays into text/date/badge cells. If a value is structured, use a supported typed cell.
[
'key' => 'title',
'type' => 'link',
'label' => 'global.name',
'class' => 'evo-ui-table__title-column',
'sortable' => true,
'sort_field' => 'items.title',
]Supported type values:
textlinkimagechipsbadgeicondateposition
Optional column keys:
value: row path if it differs fromkeyclass: header classcell_class: body cell classmeta_icon: icon used in list metasortable: enables header/list sortingsort_field: provider-level field name for sortingdefault_direction:ascordesceditable: renders a compact inline editor for this columnedit_field: provider field key for inline saving; defaults tokeyedit_type: inline input type, currentlytextornumberrules: validation rules for inline or modal form input
For small dictionaries, use inline editing for existing rows. Creation can be inline for very small maintenance screens, or modal-based when adding an empty row would feel noisy:
'inline' => [
'create_provider' => 'createInlineRow',
'save_provider' => 'updateInlineField',
],
'actions' => [
[
'key' => 'create',
'type' => 'wire',
'method' => 'createInlineRow',
'icon' => 'plus',
'label' => 'module::global.add_item',
'tone' => 'success',
'icon_only' => true,
],
],
'columns' => [
[
'key' => 'name',
'type' => 'text',
'label' => 'module::global.name',
'editable' => true,
'rules' => ['required', 'string', 'max:255'],
'sortable' => true,
],
],Inline fields save on blur or Enter and revert the local value on Escape. The provider hooks are:
public function createInlineRow(): int;
public function updateInlineField(int $id, string $field, string $value, array $column = []): string;updateInlineField() should return the normalized saved value so the input can
stay in sync after slugging, trimming, or uniqueness checks.
For dictionaries with a position column, enable row reordering in config:
'reorder' => [
'enabled' => true,
'sort' => 'position',
'move_provider' => 'moveRow',
'reorder_provider' => 'reorderRow',
],
'columns' => [
[
'key' => 'position',
'type' => 'position',
'label' => 'global.position',
'sortable' => true,
'sort_field' => 'position',
],
],type => position renders compact up/down controls around the position badge.
When reorder is enabled, table and list rows are also draggable. The provider
owns persistence:
public function moveRow(int $id, string $direction = 'up'): void;
public function reorderRow(int $id, int $targetId, string $placement = 'before'): void;After a reorder action the table switches back to the configured position sort, so the user immediately sees the canonical order.
'list' => [
'image' => 'cover',
'icon' => 'file-text',
'media' => true,
'title' => 'title',
'subtitle' => 'section',
'meta' => ['published_at', 'tags', 'views'],
],title and subtitle can reference normal columns or plain row keys. image
defaults to cover; icon is used when there is no image. Set
'media' => false for dictionary-like lists where an image/icon column would
only waste space.
The list uses the same visual atoms as the table: image, link, chips, badge, icon badge, position controls, and dimmed state.
Multi-select:
[
'state' => 'tag',
'type' => 'multi-select',
'icon' => 'hash',
'label' => 'Tags',
'search_label' => 'Filter by tag',
]Date range:
[
'state' => 'published_at',
'type' => 'date-range',
'icon' => 'calendar',
'label' => 'Published',
'default' => ['from' => '', 'to' => ''],
]Segmented:
[
'state' => 'availability',
'type' => 'segmented',
'label' => 'Publication',
'options' => [
['value' => 'all', 'icon' => 'list', 'label' => 'All'],
['value' => 'published', 'icon' => 'eye', 'label' => 'Published'],
['value' => 'unpublished', 'icon' => 'eye-off', 'label' => 'Unpublished'],
],
]Provider filterGroups() must return selectable options for select-like
filters:
public function filterGroups(): array
{
return [
[
'key' => 'tag',
'items' => [
['id' => 1, 'label' => 'Design'],
],
],
];
}Link action:
[
'key' => 'create',
'icon' => 'plus',
'label' => 'global.add',
'href_provider' => 'createUrl',
'tone' => 'success',
'icon_only' => true,
]Wire action:
[
'key' => 'duplicate',
'type' => 'wire',
'method' => 'duplicateSelected',
'icon' => 'copy',
'label' => 'global.duplicate',
'tone' => 'info',
'icon_only' => true,
'selection' => 'single',
]Selection-aware actions are disabled until one row is selected:
'selection' => 'single'[
'key' => 'edit',
'type' => 'link',
'href' => 'edit_url',
'icon' => 'edit',
'label' => 'global.edit',
'tone' => 'primary',
'attributes' => [
'data-evo-manager-link' => true,
'data-tab-back' => 'edit_back',
],
]Delete action:
[
'key' => 'delete',
'type' => 'delete',
'href' => '#',
'icon' => 'trash',
'label' => 'global.remove',
'tone' => 'danger',
'attributes' => [
'data-href' => 'delete_url',
'data-delete' => 'id',
'data-name' => 'delete_name',
],
]type => delete is intercepted by evo-ui.js and shown through the shared
evo-ui confirmation modal before opening data-href.
State-driven action:
[
'key' => 'publish',
'type' => 'wire',
'method' => 'togglePublished',
'argument' => 'id',
'icon_field' => 'published',
'icon_true' => 'eye',
'icon_false' => 'eye-off',
'tone_field' => 'published',
'tone_true' => 'success',
'tone_false' => 'danger',
]Use row states to make important row state visible outside action buttons:
'row_states' => [
[
'field' => 'published',
'value' => false,
'class' => 'is-dimmed',
],
],is-dimmed lowers opacity for the content while keeping row actions readable.
The provider owns data access and should apply:
- search
- filters
- sort
- pagination
- mutation methods for row actions
The Livewire component owns:
- URL state
- selected row
- toolbar state
- table/list rendering
- pagination UI
Keep provider methods deterministic and side-effect free except explicit action
methods such as duplicate() or togglePublished().
Module tables can open create/edit forms in the shared evo-ui modal:
'actions' => [
[
'key' => 'create',
'type' => 'wire',
'method' => 'openCreateModal',
'icon' => 'plus',
'label' => 'global.add',
'tone' => 'success',
'icon_only' => true,
],
[
'key' => 'edit',
'type' => 'wire',
'method' => 'openEditModal',
'icon' => 'edit',
'label' => 'global.edit',
'tone' => 'primary',
'icon_only' => true,
'selection' => 'single',
],
],
'modal' => [
'enabled' => true,
'row_dblclick' => true,
'icon' => 'user',
'title_create' => 'module::global.add_author',
'title_edit' => 'module::global.edit_author',
'fields' => [
['name' => 'image', 'type' => 'image', 'label' => 'module::global.image'],
['name' => 'name', 'type' => 'text', 'label' => 'module::global.name', 'rules' => ['required']],
['name' => 'alias', 'type' => 'alias', 'source' => ['name']],
['name' => 'gender', 'type' => 'radio', 'options' => [
['value' => 'man', 'label' => 'module::global.gender_man', 'icon' => 'gender-male'],
['value' => 'woman', 'label' => 'module::global.gender_woman', 'icon' => 'gender-female'],
]],
['name' => 'answers', 'type' => 'repeater', 'label' => 'module::global.answers', 'fields' => [
['name' => 'answer', 'type' => 'text', 'label' => 'module::global.answer', 'rules' => ['required']],
['name' => 'votes', 'type' => 'number', 'label' => 'module::global.votes'],
]],
],
],Set row_dblclick to false when the modal is only used for creation and row
editing remains inline.
Supported modal field types:
textemailnumberdateselecttextareaaliasradioimagerepeater
For alias fields, provide source fields. The table keeps aliases generated
from source fields until the user edits the alias manually.
repeater fields edit nested modal arrays. Configure default_item plus item
fields; each item field can use text, number, textarea, static, or
badge. The table exposes addModalItem(), removeModalItem(), and
moveModalItem() for the modal UI and validates nested rules as
modalData.<name>.*.<item>.
Provider hooks:
public function modalDefaults(): array;
public function modalData(int $id): array;
public function modalAlias(string $source, ?int $id = null): string;
public function saveModal(array $data, ?int $id = null, string $mode = 'create'): int;saveModal() must return the saved row id so the table can keep the row
selected after saving.
Toolbar and row actions are declarative. A table action should describe the UI command and point to a provider or Livewire method that owns the behavior.
Common toolbar action keys:
'actions' => [
[
'key' => 'create',
'type' => 'wire',
'method' => 'openCreateModal',
'icon' => 'plus',
'label' => 'global.add',
'tone' => 'success',
'icon_only' => true,
],
],Common row action keys:
'row_actions' => [
[
'key' => 'publish',
'type' => 'wire',
'method' => 'togglePublished',
'icon' => 'eye',
'label' => 'global.publish',
],
[
'key' => 'duplicate',
'type' => 'wire',
'method' => 'duplicate',
'icon' => 'copy',
'label' => 'global.duplicate',
],
[
'key' => 'delete',
'type' => 'wire',
'method' => 'deleteRow',
'icon' => 'trash',
'tone' => 'danger',
'confirm' => true,
],
],Action rules:
type => wirecalls a Livewire method or a provider-backed table method.type => linkopens a URL resolved directly or through a provider.selection => singlerequires a selected row.confirm => truemust use the shared delete/confirm UI, not a module-local browser prompt.- destructive providers must enforce their own delete guards.
Provider hooks commonly used by actions:
public function createUrl(array $action): string;
public function selectedEditUrl(array $action, ?int $selectedId): string;
public function selectedDeleteHref(array $action, ?int $selectedId): string;
public function selectedDeleteActionAttributes(array $action, ?int $selectedId): array;
public function duplicate(int $id): void;
public function togglePublished(int $id): void;
public function deleteRow(int $id): void;Column header actions are for compact operations tied to one column, such as auto-translate on an sLang language column. Define them on the column:
[
'key' => 'uk',
'type' => 'text',
'label' => 'Українська',
'editable' => true,
'header_actions' => [
[
'key' => 'auto_translate',
'icon' => 'wand-sparkles',
'label' => 'Auto translate',
'provider' => 'autoTranslateInlineField',
],
],
],The table validates the action against the configured column and then delegates to the provider method. Header actions must be compact and column-scoped; larger operations belong in the toolbar.
The modal or provider may prevent deletion when a row is in use. The provider must be the source of truth because only the module knows its domain relations.
Recommended provider shape:
public function deleteGuard(int $id): array
{
return [
'blocked' => true,
'message' => 'This status is used by existing issues.',
'count' => 12,
];
}The generic UI should render the guard state, but the consumer provider decides whether deletion is allowed.
Real consumers use more than the minimal modal field set. Supported table modal fields include:
textemailnumberdatedatetime-localselecttextareacheckboxaliasradiochoicesimagefileeditorrepeaterbuildercolor-picker
Field behavior is documented in the form/field catalogue. Table modal configs may use the same validation metadata, option providers and media/editor markers as forms, but table providers own modal defaults, row data and persistence.
Use these consumers as references:
sArticlesfor article tables, relation choices, media/editor fields, publish/duplicate/delete actions and content builder fields.dIssuesfor settings taxonomy tables, color picker fields, delete guards and issue table filters.sLangfor inline dictionary editing and language-column header actions.sSeofor redirects table CRUD and compact settings-linked module tabs.
Before adding a new table preset, confirm:
- the preset key and config merge key match;
- the provider returns deterministic
total(),rows()andfilterGroups(); - every sortable column has a provider-safe
sort_field; - every filter has a stable
state; - table/list views use the same row data;
- modal fields have validation rules where user input is saved;
- destructive actions are guarded by the provider;
- reorder providers validate row ids and placement;
- generic UI behavior stays in
evo-ui; - module-specific persistence stays in the consumer module.