ModelResource

Tables

Basics

In CrudResource (ModelResource) on the indexPage as well as on the DetailPage, TableBuilder is used to display the main data, so we recommend you also study the documentation section TableBuilder.

Sorting

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:4] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; use MoonShine\Support\Enums\SortDirection; class PostResource extends ModelResource { // Default sort field protected string $sortColumn = 'created_at'; // Default sort type protected SortDirection $sortDirection = SortDirection::DESC; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\Support\Enums\SortDirection;
 
class PostResource extends ModelResource
{
// Default sort field
protected string $sortColumn = 'created_at';
 
// Default sort type
protected SortDirection $sortDirection = SortDirection::DESC;
 
// ...
}

Buttons

To add buttons to the table, you can use ActionButton and the methods indexButtons(), as well as detailButtons() for the detail page.

After the main buttons:

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:2] use MoonShine\Support\ListOf; use MoonShine\UI\Components\ActionButton; protected function indexButtons(): ListOf { return parent::indexButtons() ->add(ActionButton::make('Link', '/endpoint')); }
 namespaces
use MoonShine\Support\ListOf;
use MoonShine\UI\Components\ActionButton;
 
protected function indexButtons(): ListOf
{
return parent::indexButtons()
->add(ActionButton::make('Link', '/endpoint'));
}

Before the main buttons:

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:2] use MoonShine\Support\ListOf; use MoonShine\UI\Components\ActionButton; protected function indexButtons(): ListOf { return parent::indexButtons() ->prepend(ActionButton::make('Link', '/endpoint')); }
 namespaces
use MoonShine\Support\ListOf;
use MoonShine\UI\Components\ActionButton;
 
protected function indexButtons(): ListOf
{
return parent::indexButtons()
->prepend(ActionButton::make('Link', '/endpoint'));
}

Remove the delete button:

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:2] use MoonShine\Support\ListOf; use MoonShine\UI\Components\ActionButton; protected function indexButtons(): ListOf { return parent::indexButtons() ->except(fn(ActionButton $btn) => $btn->getName() === 'resource-delete-button'); }
 namespaces
use MoonShine\Support\ListOf;
use MoonShine\UI\Components\ActionButton;
 
protected function indexButtons(): ListOf
{
return parent::indexButtons()
->except(fn(ActionButton $btn) => $btn->getName() === 'resource-delete-button');
}

Clear the button set and add your own:

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:2] use MoonShine\Support\ListOf; use MoonShine\UI\Components\ActionButton; protected function indexButtons(): ListOf { parent::indexButtons() ->empty() ->add(ActionButton::make('Link', '/endpoint')); }
 namespaces
use MoonShine\Support\ListOf;
use MoonShine\UI\Components\ActionButton;
 
protected function indexButtons(): ListOf
{
parent::indexButtons()
->empty()
->add(ActionButton::make('Link', '/endpoint'));
}

The same approach is used for the table on the detail page, only through the method detailButtons().

For bulk actions, you need to add the bulk() method.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:2] use MoonShine\Support\ListOf; use MoonShine\UI\Components\ActionButton; protected function indexButtons(): ListOf { return parent::indexButtons() ->add(ActionButton::make('Link', '/endpoint')->bulk()); }
 namespaces
use MoonShine\Support\ListOf;
use MoonShine\UI\Components\ActionButton;
 
protected function indexButtons(): ListOf
{
return parent::indexButtons()
->add(ActionButton::make('Link', '/endpoint')->bulk());
}

By default, all buttons in the table are displayed in a line, but you can change the behavior and display them through a drop-down list. To do this, change the $indexButtonsInDropdown property in the resource:

class PostResource extends ModelResource { protected bool $indexButtonsInDropdown = true; // ... }
class PostResource extends ModelResource
{
protected bool $indexButtonsInDropdown = true;
 
// ...
}

Attributes

To add attributes for the td element of the table, you can use the customWrapperAttributes() method on the field that represents the cell you need.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\UI\Fields\Text; protected function indexFields(): iterable { return [ // ... Text::make('Title') ->customWrapperAttributes(['width' => '20%']); ]; }
 namespaces
use MoonShine\UI\Fields\Text;
 
protected function indexFields(): iterable
{
return [
// ...
Text::make('Title')
->customWrapperAttributes(['width' => '20%']);
];
}

You can also customize tr and td for the table with data through the resource. To do this, you need to use the corresponding methods trAttributes() and tdAttributes(), to which you need to pass a closure that returns an array of attributes for the table component.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:6] namespace App\MoonShine\Resources; use Closure; use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract; use MoonShine\Laravel\Resources\ModelResource; use MoonShine\UI\Fields\Text; class PostResource extends ModelResource { // ... protected function tdAttributes(): Closure { return fn(?DataWrapperContract $data, int $row, int $cell) => [ 'width' => '20%' ]; } protected function trAttributes(): Closure { return fn(?DataWrapperContract $data, int $row) => [ 'data-tr' => $row ]; } }
 namespaces
namespace App\MoonShine\Resources;
 
use Closure;
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Fields\Text;
 
class PostResource extends ModelResource
{
// ...
 
protected function tdAttributes(): Closure
{
return fn(?DataWrapperContract $data, int $row, int $cell) => [
'width' => '20%'
];
}
 
protected function trAttributes(): Closure
{
return fn(?DataWrapperContract $data, int $row) => [
'data-tr' => $row
];
}
}

img

Click Actions

By default, clicking on tr does nothing, but you can change the behavior to navigate to editing, selection, or to the detailed view.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\Support\Enums\ClickAction; // ClickAction::SELECT, ClickAction::DETAIL, ClickAction::EDIT protected ?ClickAction $clickAction = ClickAction::SELECT;
 namespaces
use MoonShine\Support\Enums\ClickAction;
 
// ClickAction::SELECT, ClickAction::DETAIL, ClickAction::EDIT
 
protected ?ClickAction $clickAction = ClickAction::SELECT;

Sticky Header

The model resource property stickyTable allows you to fix the header when scrolling a table with a large number of elements.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] namespace App\MoonShine\Resources; use MoonShine\Laravle\Resources\ModelResource; class PostResource extends ModelResource { protected bool $stickyTable = true; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravle\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $stickyTable = true;
 
// ...
}

Column Display

You can allow users to independently determine which columns to display in the table while retaining their selection. To do this, you need to set the parameter $columnSelection for the resource.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected bool $columnSelection = true; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $columnSelection = true;
 
// ...
}

If you need to exclude fields from the selection, use the columnSelection() method.

public function columnSelection(bool $active = true)
public function columnSelection(bool $active = true)
// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:6] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; use MoonShine\UI\Fields\ID; use MoonShine\UI\Fields\Text; use MoonShine\UI\Fields\Textarea; class PostResource extends ModelResource { protected bool $columnSelection = true; // ... protected function indexFields(): iterable { return [ ID::make() ->columnSelection(false), Text::make('Title'), Textarea::make('Body'), ]; } }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use MoonShine\UI\Fields\Textarea;
 
class PostResource extends ModelResource
{
protected bool $columnSelection = true;
 
// ...
 
protected function indexFields(): iterable
{
return [
ID::make()
->columnSelection(false),
Text::make('Title'),
Textarea::make('Body'),
];
}
}

Sticky columns

You can freeze cells in large tables, suitable for ID columns and buttons.

To fix buttons in the table, switch the resource to stickyButtons mode.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected bool $stickyButtons = true; // ... }
 namespaces
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $stickyButtons = true;
 
// ...
}

To fix a column, call the sticky() method on the Field.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\UI\Fields\ID; protected function indexFields(): iterable { return [ ID::make()->sticky(), ]; }
 namespaces
use MoonShine\UI\Fields\ID;
 
protected function indexFields(): iterable
{
return [
ID::make()->sticky(),
];
}

Pagination

To change the number of items per page, use the property $itemsPerPage.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected int $itemsPerPage = 25; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected int $itemsPerPage = 25;
 
// ...
}

Cursor

When dealing with a large volume of data, the best solution is to use cursor pagination.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected bool $cursorPaginate = true; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $cursorPaginate = true;
 
// ...
}

Simple

If you do not plan to display the total number of pages, use Simple Pagination. This avoids additional queries for the total number of records in the database.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected bool $simplePaginate = true; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $simplePaginate = true;
 
// ...
}

img

Disable Pagination

If you do not plan to use pagination, it can be disabled.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected bool $usePagination = false; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $usePagination = false;
 
// ...
}

Async Mode

In the resource, async mode is used by default. This mode allows for pagination, filtering, and sorting without page reloads. However, if you want to disable async mode, you can use the property $isAsync.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] namespace App\MoonShine\Resources; use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected bool $isAsync = false; // ... }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $isAsync = false;
 
// ...
}

Updating a row

You can asynchronously update a row in the table by triggering the event:

table-row-updated-{{componentName}}-{{row-id}}
table-row-updated-{{componentName}}-{{row-id}}
  • {{componentName}} - the name of the component;
  • {{row-id}} - the key of the row item.

To add an event, you can use the helper class:

AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, 'main-table-{row-id}')
AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, 'main-table-{row-id}')
  • {row-id} - shortcode for the id of the current model record.
// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:8] namespace App\MoonShine\Resources; use MoonShine\UI\Fields\ID; use MoonShine\UI\Fields\Switcher; use MoonShine\UI\Fields\Text; use MoonShine\Laravel\Resources\ModelResource; use MoonShine\Support\AlpineJs; use MoonShine\Support\Enums\JsEvent; class PostResource extends ModelResource { // ... protected function indexFields(): iterable { return [ ID::make(), Text::make('Title'), Switcher::make('Active') ->updateOnPreview( events: [AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, 'index-table-{row-id}')] ) ]; } }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Switcher;
use MoonShine\UI\Fields\Text;
use MoonShine\Laravel\Resources\ModelResource;
use MoonShine\Support\AlpineJs;
use MoonShine\Support\Enums\JsEvent;
 
class PostResource extends ModelResource
{
// ...
 
protected function indexFields(): iterable
{
return [
ID::make(),
Text::make('Title'),
Switcher::make('Active')
->updateOnPreview(
events: [AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, 'index-table-{row-id}')]
)
];
}
}

The withUpdateRow() method is also available, which helps simplify the assignment of events.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:6] namespace App\MoonShine\Resources; use MoonShine\UI\Fields\ID; use MoonShine\UI\Fields\Switcher; use MoonShine\UI\Fields\Text; use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { // ... protected function indexFields(): iterable { return [ ID::make(), Text::make('Title'), Switcher::make('Active') ->withUpdateRow($this->getListComponentName()) ]; } }
 namespaces
namespace App\MoonShine\Resources;
 
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Switcher;
use MoonShine\UI\Fields\Text;
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
// ...
 
protected function indexFields(): iterable
{
return [
ID::make(),
Text::make('Title'),
Switcher::make('Active')
->withUpdateRow($this->getListComponentName())
];
}
}

Lazy mode

If you want to display a page without waiting for data to load, and then send a query to get the table data, use Lazy mode.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\Laravel\Resources\ModelResource; class PostResource extends ModelResource { protected bool $isLazy = true; }
 namespaces
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
protected bool $isLazy = true;
}

Modifiers

Components

You can completely replace or modify the resource's TableBuilder for both the index and detail pages. Use the modifyListComponent() or modifyDetailComponent() methods for this.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\Contracts\UI\ComponentContract; public function modifyListComponent(ComponentContract $component): ComponentContract { return parent::modifyListComponent($component)->customAttributes([ 'data-my-attr' => 'value' ]); }
 namespaces
use MoonShine\Contracts\UI\ComponentContract;
 
public function modifyListComponent(ComponentContract $component): ComponentContract
{
return parent::modifyListComponent($component)->customAttributes([
'data-my-attr' => 'value'
]);
}
// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\Contracts\UI\ComponentContract; public function modifyDetailComponent(ComponentContract $component): ComponentContract { return parent::modifyDetailComponent($component)->customAttributes([ 'data-my-attr' => 'value' ]); }
 namespaces
use MoonShine\Contracts\UI\ComponentContract;
 
public function modifyDetailComponent(ComponentContract $component): ComponentContract
{
return parent::modifyDetailComponent($component)->customAttributes([
'data-my-attr' => 'value'
]);
}

Elements thead, tbody, tfoot

If it is not enough to just automatically output fields in thead, tbody, and tfoot, you can override or extend this logic based on the resource methods thead(), tbody(), tfoot().

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:5] use Closure; use MoonShine\Contracts\UI\Collection\TableRowsContract; use MoonShine\Contracts\UI\TableRowContract; use MoonShine\UI\Collections\TableCells; use MoonShine\UI\Collections\TableRows; protected function thead(): null|TableRowsContract|Closure { return static fn(TableRowContract $default) => TableRows::make([$default])->pushRow( TableCells::make()->pushCell( 'td content' ) ); } protected function tbody(): null|TableRowsContract|Closure { return static fn(TableRowsContract $default) => $default->pushRow( TableCells::make()->pushCell( 'td content' ) ); } protected function tfoot(): null|TableRowsContract|Closure { return static fn(?TableRowContract $default) => TableRows::make([$default])->pushRow( TableCells::make()->pushCell( 'td content' ) ); }
 namespaces
use Closure;
use MoonShine\Contracts\UI\Collection\TableRowsContract;
use MoonShine\Contracts\UI\TableRowContract;
use MoonShine\UI\Collections\TableCells;
use MoonShine\UI\Collections\TableRows;
 
protected function thead(): null|TableRowsContract|Closure
{
return static fn(TableRowContract $default) => TableRows::make([$default])->pushRow(
TableCells::make()->pushCell(
'td content'
)
);
}
 
protected function tbody(): null|TableRowsContract|Closure
{
return static fn(TableRowsContract $default) => $default->pushRow(
TableCells::make()->pushCell(
'td content'
)
);
}
 
protected function tfoot(): null|TableRowsContract|Closure
{
return static fn(?TableRowContract $default) => TableRows::make([$default])->pushRow(
TableCells::make()->pushCell(
'td content'
)
);
}

Example of adding a row in tfoot

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:7] use Closure; use MoonShine\Contracts\UI\Collection\TableRowsContract; use MoonShine\Contracts\UI\TableRowContract; use MoonShine\UI\Collections\TableCells; use MoonShine\UI\Collections\TableRows; use MoonShine\UI\Components\Table\TableBuilder; use MoonShine\UI\Components\Table\TableRow; protected function tfoot(): null|TableRowsContract|Closure { return static function(?TableRowContract $default, TableBuilder $table) { $cells = TableCells::make(); $cells->pushCell('Balance:'); $cells->pushCell('$1000'); return TableRows::make([TableRow::make($cells), $default]); }; }
 namespaces
use Closure;
use MoonShine\Contracts\UI\Collection\TableRowsContract;
use MoonShine\Contracts\UI\TableRowContract;
use MoonShine\UI\Collections\TableCells;
use MoonShine\UI\Collections\TableRows;
use MoonShine\UI\Components\Table\TableBuilder;
use MoonShine\UI\Components\Table\TableRow;
 
protected function tfoot(): null|TableRowsContract|Closure
{
return static function(?TableRowContract $default, TableBuilder $table) {
$cells = TableCells::make();
 
$cells->pushCell('Balance:');
$cells->pushCell('$1000');
 
return TableRows::make([TableRow::make($cells), $default]);
};
}