ModelResource

Pages

Basics

MoonShine provides the ability to configure CRUD pages. To do this, you need to choose the resource type Model resource with pages when creating a resource via the command.

This will create a model resource class and additional classes for the index, detail view, and form pages. The page classes will, by default, be located in the app/MoonShine/Pages directory.

In the created model resource, CRUD pages will be registered in the pages() method.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:start] namespace App\MoonShine\Resources; use App\MoonShine\Pages\Post\PostIndexPage; use App\MoonShine\Pages\Post\PostFormPage; use App\MoonShine\Pages\Post\PostDetailPage; use MoonShine\Laravel\Resources\ModelResource; // [tl! collapse:end] class PostResource extends ModelResource { // ... protected function pages(): array { return [ PostIndexPage::class, PostFormPage::class, PostDetailPage::class, ]; } }
 namespaces
namespace App\MoonShine\Resources;
 
use App\MoonShine\Pages\Post\PostIndexPage;
use App\MoonShine\Pages\Post\PostFormPage;
use App\MoonShine\Pages\Post\PostDetailPage;
use MoonShine\Laravel\Resources\ModelResource;
 
class PostResource extends ModelResource
{
// ...
 
protected function pages(): array
{
return [
PostIndexPage::class,
PostFormPage::class,
PostDetailPage::class,
];
}
}

Page Types

To specify the page type in ModelResource, the enum class PageType is used.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\Support\Enums\PageType; PageType::INDEX; // Index page PageType::FORM; // Form page PageType::DETAIL; // Detail page
 namespaces
use MoonShine\Support\Enums\PageType;
 
PageType::INDEX; // Index page
PageType::FORM; // Form page
PageType::DETAIL; // Detail page

Adding Fields

Fields in MoonShine are used not only for data input but also for output. The fields() method in the CRUD page class allows you to specify the necessary fields.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:5] namespace App\MoonShine\Pages\Post; use MoonShine\Laravel\Pages\Crud\IndexPage; use MoonShine\UI\Fields\ID; use MoonShine\UI\Fields\Text; class PostIndexPage extends IndexPage { // ... protected function fields(): iterable { return [ ID::make(), Text::make('Title'), ]; } }
 namespaces
namespace App\MoonShine\Pages\Post;
 
use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
 
class PostIndexPage extends IndexPage
{
// ...
 
protected function fields(): iterable
{
return [
ID::make(),
Text::make('Title'),
];
}
}

Main Components

In MoonShine, you can quickly change the main component on the page.

IndexPage

The getItemsComponent() method allows you to change the main component of the index page.

getItemsComponent(iterable $items, Fields $fields): ComponentContract
getItemsComponent(iterable $items, Fields $fields): ComponentContract
  • $items - field values,
  • $fields - fields.
// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:4] use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Contracts\UI\TableBuilderContract; use MoonShine\Laravel\Pages\Crud\IndexPage; use MoonShine\UI\Components\Table\TableBuilder; class ArticleIndexPage extends IndexPage { // ... protected function getItemsComponent(iterable $items, Fields $fields): ComponentContract { return TableBuilder::make(items: $items) ->name($this->getListComponentName()) ->fields($fields) ->cast($this->getResource()->getCaster()) ->withNotFound() ->when( ! is_null($head = $this->getResource()->getHeadRows()), fn (TableBuilderContract $table): TableBuilderContract => $table->headRows($head) ) ->when( ! is_null($body = $this->getResource()->getRows()), fn (TableBuilderContract $table): TableBuilderContract => $table->rows($body) ) ->when( ! is_null($foot = $this->getResource()->getFootRows()), fn (TableBuilderContract $table): TableBuilderContract => $table->footRows($foot) ) ->when( ! is_null($this->getResource()->getTrAttributes()), fn (TableBuilderContract $table): TableBuilderContract => $table->trAttributes( $this->getResource()->getTrAttributes() ) ) ->when( ! is_null($this->getResource()->getTdAttributes()), fn (TableBuilderContract $table): TableBuilderContract => $table->tdAttributes( $this->getResource()->getTdAttributes() ) ) ->buttons($this->getResource()->getIndexButtons()) ->clickAction($this->getResource()->getClickAction()) ->when($this->getResource()->isAsync(), static function (TableBuilderContract $table): void { $table->async()->pushState(); }) ->when($this->getResource()->isStickyTable(), function (TableBuilderContract $table): void { $table->sticky(); }) ->when($this->getResource()->isColumnSelection(), function (TableBuilderContract $table): void { $table->columnSelection(); }); } }
 namespaces
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Contracts\UI\TableBuilderContract;
use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\UI\Components\Table\TableBuilder;
 
class ArticleIndexPage extends IndexPage
{
// ...
 
protected function getItemsComponent(iterable $items, Fields $fields): ComponentContract
{
return TableBuilder::make(items: $items)
->name($this->getListComponentName())
->fields($fields)
->cast($this->getResource()->getCaster())
->withNotFound()
->when(
! is_null($head = $this->getResource()->getHeadRows()),
fn (TableBuilderContract $table): TableBuilderContract => $table->headRows($head)
)
->when(
! is_null($body = $this->getResource()->getRows()),
fn (TableBuilderContract $table): TableBuilderContract => $table->rows($body)
)
->when(
! is_null($foot = $this->getResource()->getFootRows()),
fn (TableBuilderContract $table): TableBuilderContract => $table->footRows($foot)
)
->when(
! is_null($this->getResource()->getTrAttributes()),
fn (TableBuilderContract $table): TableBuilderContract => $table->trAttributes(
$this->getResource()->getTrAttributes()
)
)
->when(
! is_null($this->getResource()->getTdAttributes()),
fn (TableBuilderContract $table): TableBuilderContract => $table->tdAttributes(
$this->getResource()->getTdAttributes()
)
)
->buttons($this->getResource()->getIndexButtons())
->clickAction($this->getResource()->getClickAction())
->when($this->getResource()->isAsync(), static function (TableBuilderContract $table): void {
$table->async()->pushState();
})
->when($this->getResource()->isStickyTable(), function (TableBuilderContract $table): void {
$table->sticky();
})
->when($this->getResource()->isColumnSelection(), function (TableBuilderContract $table): void {
$table->columnSelection();
});
}
}

Example of an index page with the CardsBuilder component in the Recipes section.

DetailPage

The getDetailComponent() method allows you to change the main component of the detail page.

getDetailComponent(?DataWrapperContract $item, Fields $fields): ComponentContract
getDetailComponent(?DataWrapperContract $item, Fields $fields): ComponentContract
  • $item - data,
  • $fields - fields.
// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:4] use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract; use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Laravel\Collections\Fields; use MoonShine\UI\Components\Table\TableBuilder; class ArticleDetailPage extends DetailPage { // ... protected function getDetailComponent(?DataWrapperContract $item, Fields $fields): ComponentContract { return TableBuilder::make($fields) ->cast($this->getResource()->getCaster()) ->items([$item]) ->vertical() ->simple() ->preview(); } }
 namespaces
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Laravel\Collections\Fields;
use MoonShine\UI\Components\Table\TableBuilder;
 
class ArticleDetailPage extends DetailPage
{
// ...
 
protected function getDetailComponent(?DataWrapperContract $item, Fields $fields): ComponentContract
{
return TableBuilder::make($fields)
->cast($this->getResource()->getCaster())
->items([$item])
->vertical()
->simple()
->preview();
}
}

FormPage

The getFormComponent() method allows you to change the main component on the form page.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:3] use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract; use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Laravel\Collections\Fields; getFormComponent( string $action, ?DataWrapperContract $item, Fields $fields, bool $isAsync = true, ): ComponentContract
 namespaces
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Laravel\Collections\Fields;
 
getFormComponent(
string $action,
?DataWrapperContract $item,
Fields $fields,
bool $isAsync = true,
): ComponentContract
  • $action - endpoint,
  • $item - data,
  • $fields - fields,
  • $isAsync - asynchronous mode.
// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:start] use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract; use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Contracts\UI\FormBuilderContract; use MoonShine\Laravel\Collections\Fields; use MoonShine\Laravel\Pages\Crud\FormPage; use MoonShine\Support\AlpineJs; use MoonShine\Support\Enums\JsEvent; use MoonShine\UI\Components\FormBuilder; use MoonShine\UI\Fields\Hidden; // [tl! collapse:end] class ArticleFormPage extends FormPage { // ... protected function getFormComponent( string $action, ?DataWrapperContract $item, Fields $fields, bool $isAsync = true, ): ComponentContract { $resource = $this->getResource(); return FormBuilder::make($action) ->cast($this->getResource()->getCaster()) ->fill($item) ->fields([ ...$fields ->when( ! is_null($item), static fn (Fields $fields): Fields => $fields->push( Hidden::make('_method')->setValue('PUT') ) ) ->when( ! $resource->isItemExists() && ! $resource->isCreateInModal(), static fn (Fields $fields): Fields => $fields->push( Hidden::make('_force_redirect')->setValue(true) ) ) ->toArray(), ]) ->when( ! $resource->hasErrorsAbove(), fn (FormBuilderContract $form): FormBuilderContract => $form->errorsAbove($resource->hasErrorsAbove()) ) ->when( $isAsync, static fn (FormBuilderContract $formBuilder): FormBuilderContract => $formBuilder ->async(events: array_filter([ $resource->getListEventName( request()->input('_component_name', 'default'), $isAsync && $resource->isItemExists() ? array_filter([ 'page' => request()->input('page'), 'sort' => request()->input('sort'), ]) : [] ), ! $resource->isItemExists() && $resource->isCreateInModal() ? AlpineJs::event(JsEvent::FORM_RESET, $resource->getUriKey()) : null, ])) ) ->when( $resource->isPrecognitive() || (moonshineRequest()->isFragmentLoad('crud-form') && ! $isAsync), static fn (FormBuilderContract $form): FormBuilderContract => $form->precognitive() ) ->when( $resource->isSubmitShowWhen(), static fn (FormBuilderContract $form): FormBuilderContract => $form->submitShowWhenAttribute() ) ->name($resource->getUriKey()) ->submit(__('moonshine::ui.save'), ['class' => 'btn-primary btn-lg']) ->buttons($resource->getFormBuilderButtons()); } }
 namespaces
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Contracts\UI\FormBuilderContract;
use MoonShine\Laravel\Collections\Fields;
use MoonShine\Laravel\Pages\Crud\FormPage;
use MoonShine\Support\AlpineJs;
use MoonShine\Support\Enums\JsEvent;
use MoonShine\UI\Components\FormBuilder;
use MoonShine\UI\Fields\Hidden;
 
class ArticleFormPage extends FormPage
{
// ...
 
protected function getFormComponent(
string $action,
?DataWrapperContract $item,
Fields $fields,
bool $isAsync = true,
): ComponentContract {
$resource = $this->getResource();
 
return FormBuilder::make($action)
->cast($this->getResource()->getCaster())
->fill($item)
->fields([
...$fields
->when(
! is_null($item),
static fn (Fields $fields): Fields => $fields->push(
Hidden::make('_method')->setValue('PUT')
)
)
->when(
! $resource->isItemExists() && ! $resource->isCreateInModal(),
static fn (Fields $fields): Fields => $fields->push(
Hidden::make('_force_redirect')->setValue(true)
)
)
->toArray(),
])
->when(
! $resource->hasErrorsAbove(),
fn (FormBuilderContract $form): FormBuilderContract => $form->errorsAbove($resource->hasErrorsAbove())
)
->when(
$isAsync,
static fn (FormBuilderContract $formBuilder): FormBuilderContract => $formBuilder
->async(events: array_filter([
$resource->getListEventName(
request()->input('_component_name', 'default'),
$isAsync && $resource->isItemExists() ? array_filter([
'page' => request()->input('page'),
'sort' => request()->input('sort'),
]) : []
),
! $resource->isItemExists() && $resource->isCreateInModal()
? AlpineJs::event(JsEvent::FORM_RESET, $resource->getUriKey())
: null,
]))
)
->when(
$resource->isPrecognitive() || (moonshineRequest()->isFragmentLoad('crud-form') && ! $isAsync),
static fn (FormBuilderContract $form): FormBuilderContract => $form->precognitive()
)
->when(
$resource->isSubmitShowWhen(),
static fn (FormBuilderContract $form): FormBuilderContract => $form->submitShowWhenAttribute()
)
->name($resource->getUriKey())
->submit(__('moonshine::ui.save'), ['class' => 'btn-primary btn-lg'])
->buttons($resource->getFormBuilderButtons());
}
}

Layers on the Page

For convenience, all CRUD pages are divided into three layers, which are responsible for displaying a certain area on the page.

  • TopLayer - used for displaying metrics on the index page and for additional buttons on the edit page,
  • MainLayer - this layer is used for displaying main information using FormBuilder and TableBuilder,
  • BottomLayer - used for displaying additional information.

To configure the layers, the corresponding methods are used: topLayer(), mainLayer(), and bottomLayer(). The methods must return an array of Components.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:2] use MoonShine\Laravel\Pages\Crud\IndexPage; use MoonShine\UI\Components\Heading; class PostIndexPage extends IndexPage { // ... protected function topLayer(): array { return [ Heading::make('Custom top'), ...parent::topLayer() ]; } protected function mainLayer(): array { return [ Heading::make('Custom main'), ...parent::mainLayer() ]; } protected function bottomLayer(): array { return [ Heading::make('Custom bottom'), ...parent::bottomLayer() ]; } }
 namespaces
use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\UI\Components\Heading;
 
class PostIndexPage extends IndexPage
{
// ...
 
protected function topLayer(): array
{
return [
Heading::make('Custom top'),
...parent::topLayer()
];
}
 
protected function mainLayer(): array
{
return [
Heading::make('Custom main'),
...parent::mainLayer()
];
}
 
protected function bottomLayer(): array
{
return [
Heading::make('Custom bottom'),
...parent::bottomLayer()
];
}
}

If you need to access components of a specific layer from a resource or page, use the getLayerComponents method.

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:1] use MoonShine\Support\Enums\Layer; // ... // Resource $this->getFormPage()->getLayerComponents(Layer::BOTTOM); // Page $this->getLayerComponents(Layer::BOTTOM);
 namespaces
use MoonShine\Support\Enums\Layer;
 
// ...
 
// Resource
$this->getFormPage()->getLayerComponents(Layer::BOTTOM);
 
// Page
$this->getLayerComponents(Layer::BOTTOM);

If you need to add a component to a specified page in the desired layer from a resource, use the resource's onLoad() method and the page's pushToLayer().

// torchlight! {"summaryCollapsedIndicator": "namespaces"} // [tl! collapse:2] use MoonShine\Permissions\Components\Permissions; use MoonShine\Support\Enums\Layer; protected function onLoad(): void { $this->getFormPage()->pushToLayer( layer: Layer::BOTTOM, component: Permissions::make( 'Permissions', $this, ) ); }
 namespaces
use MoonShine\Permissions\Components\Permissions;
use MoonShine\Support\Enums\Layer;
 
protected function onLoad(): void
{
$this->getFormPage()->pushToLayer(
layer: Layer::BOTTOM,
component: Permissions::make(
'Permissions',
$this,
)
);
}