ModelResource

Pages

Video guide

Basics

Pages are the core of the MoonShine architecture. All key functionality is defined directly in page classes, which provides flexibility and modularity.

When creating a resource, classes are also created for the index pages (IndexPage), detailed view (DetailPage) and form (FormPage). These pages will be registered with the resource in the pages() method.

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

IndexPage

IndexPage is the main section of the resource and is responsible for displaying the list of elements.

Lazy mode

Lazy mode delays loading the index table until it becomes visible on the page.

protected bool $isLazy = true;
protected bool $isLazy = true;

Metrics

The metrics() method allows you to define metrics to display on the list page (more details in the Metrics section).

Filters

In the filters() method you can specify a list of fields to form the filter form (more details in the Filters section).

Query Tags

The queryTags() method allows you to add quick filtering buttons based on preset conditions (more details in the Query Tags section).

Handlers

The handlers() method for registering event handlers (more details in the Handlers section).

Main component

To modify an existing component, use the modifyListComponent() method (more details in the Basics section).

To completely replace the main index page component, use your own class (more details in the Main components section below).

##FormPage

FormPage is responsible for creating and editing elements.

Validation

You can add validation for resource form fields using Laravel's standard validation rules.

Validation rules

The rules() method allows you to define validation rules for fields.

PostFormPage.php
protected function rules(DataWrapperContract $item): array
{
return [
'title' => ['required', 'string', 'min:5'],
'content' => ['required', 'string'],
'email' => ['sometimes', 'email'],
];
}
protected function rules(DataWrapperContract $item): array
{
return [
'title' => ['required', 'string', 'min:5'],
'content' => ['required', 'string'],
'email' => ['sometimes', 'email'],
];
}

Validation messages

The validationMessages() method allows you to override validation error messages.

PostFormPage.php
protected function rules(DataWrapperContract $item): array
{
return [
'title' => ['required', 'string', 'min:5'],
];
}
 
public function validationMessages(): array
{
return [
'title.required' => 'Title is required',
'title.min' => 'Title must contain at least :min characters',
];
}
protected function rules(DataWrapperContract $item): array
{
return [
'title' => ['required', 'string', 'min:5'],
];
}
 
public function validationMessages(): array
{
return [
'title.required' => 'Title is required',
'title.min' => 'Title must contain at least :min characters',
];
}

Preparing data for validation

The prepareForValidation() method allows you to change data before validation.

PostFormPage.php
public function prepareForValidation(): void
{
request()->merge([
'slug' => request()
->string('slug')
->lower()
->value(),
]);
}
public function prepareForValidation(): void
{
request()->merge([
'slug' => request()
->string('slug')
->lower()
->value(),
]);
}

Precognitive validation

The $isPrecognitive property allows you to enable precognitive validation for the form.

PostFormPage.php
protected bool $isPrecognitive = true;
protected bool $isPrecognitive = true;

Precognitive validation allows you to validate form fields in real time as you enter data.

Main component

To modify an existing component, use the modifyFormComponent() method (more details in the Basics section).

To completely replace the main form page component, use your own class (more details in the Main components section below).

DetailPage

DetailPage is responsible for detailed display of the element.

Main component

To modify an existing component, use the modifyDetailComponent() method (more details in the Basics section).

To completely replace the main form page component, use your own class (more details in the Main components section below).

Page types

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

 namespaces
use MoonShine\Support\Enums\PageType;
 
PageType::INDEX;
PageType::FORM;
PageType::DETAIL;
 namespaces
use MoonShine\Support\Enums\PageType;
 
PageType::INDEX;
PageType::FORM;
PageType::DETAIL;

Fields

To learn about adding fields to pages, see ModelResource > Fields.

Layers on the page

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

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

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

 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()
];
}
}
 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 the components of a specific layer, then use the getLayerComponents() method.

 namespaces
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 for a specified page to the desired layer via a resource, then use the onLoad() method of the resource and the pushToLayer() method of the page.

 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,
)
);
}
 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,
)
);
}

Main Components

The main component of the page is specified by a class that implements one of the namespace interfaces MoonShine\Crud\Contracts\PageComponents. This allows you to completely replace a component, encapsulate the logic, and reuse it between pages and resources.

Available interfaces:

  • DefaultListComponentContract - the main component of the index page (list of elements),
  • DefaultDetailComponentContract - the main component of the detail page,
  • DefaultFormContract - the main form component.

The class must implement the __invoke() method, which returns a component that implements the MoonShine\Contracts\UI\ComponentContract interface.

IndexPage

To change the index page component, you need to create a class that implements the DefaultListComponentContract interface:

 namespaces
use MoonShine\Contracts\Core\DependencyInjection\CoreContract;
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Contracts\UI\TableBuilderContract;
use MoonShine\Core\Traits\WithCore;
use MoonShine\Crud\Contracts\Page\IndexPageContract;
use MoonShine\Crud\Contracts\PageComponents\DefaultListComponentContract;
use MoonShine\UI\Components\Table\TableBuilder;
 
final class ArticleListComponent implements DefaultListComponentContract
{
use WithCore;
 
public function __construct(CoreContract $core) {
$this->setCore($core);
}
 
/**
* @param iterable<array-key, mixed> $items
*/
public function __invoke(
IndexPageContract $page,
iterable $items,
FieldsContract $fields
): ComponentContract
{
$resource = $page->getResource();
 
return TableBuilder::make(items: $items)
->name($page->getListComponentName())
->fields($fields)
->cast($resource->getCaster())
->withNotFound()
->buttons($page->getButtons())
->when($page->isAsync(), function (TableBuilderContract $table) use($page): void {
$table->async(
url: fn (): string
=> $page->getRouter()->getEndpoints()->component(
name: $table->getName(),
additionally: $this->getCore()->getRequest()->getRequest()->getQueryParams(),
),
)->pushState();
})
->when($page->isLazy(), function (TableBuilderContract $table) use($resource): void {
$table->lazy()->whenAsync(
fn (TableBuilderContract $t): TableBuilderContract
=> $t->items(
$resource->getItems(),
),
);
})
->when(
! \is_null($resource->getItemsResolver()),
function (TableBuilderContract $table) use($resource): void {
$table->itemsResolver(
$resource->getItemsResolver(),
);
},
);
}
}
 namespaces
use MoonShine\Contracts\Core\DependencyInjection\CoreContract;
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Contracts\UI\TableBuilderContract;
use MoonShine\Core\Traits\WithCore;
use MoonShine\Crud\Contracts\Page\IndexPageContract;
use MoonShine\Crud\Contracts\PageComponents\DefaultListComponentContract;
use MoonShine\UI\Components\Table\TableBuilder;
 
final class ArticleListComponent implements DefaultListComponentContract
{
use WithCore;
 
public function __construct(CoreContract $core) {
$this->setCore($core);
}
 
/**
* @param iterable<array-key, mixed> $items
*/
public function __invoke(
IndexPageContract $page,
iterable $items,
FieldsContract $fields
): ComponentContract
{
$resource = $page->getResource();
 
return TableBuilder::make(items: $items)
->name($page->getListComponentName())
->fields($fields)
->cast($resource->getCaster())
->withNotFound()
->buttons($page->getButtons())
->when($page->isAsync(), function (TableBuilderContract $table) use($page): void {
$table->async(
url: fn (): string
=> $page->getRouter()->getEndpoints()->component(
name: $table->getName(),
additionally: $this->getCore()->getRequest()->getRequest()->getQueryParams(),
),
)->pushState();
})
->when($page->isLazy(), function (TableBuilderContract $table) use($resource): void {
$table->lazy()->whenAsync(
fn (TableBuilderContract $t): TableBuilderContract
=> $t->items(
$resource->getItems(),
),
);
})
->when(
! \is_null($resource->getItemsResolver()),
function (TableBuilderContract $table) use($resource): void {
$table->itemsResolver(
$resource->getItemsResolver(),
);
},
);
}
}

__invoke() method arguments:

  • $page - object of the index page on which the component is located,
  • $items - list elements to display,
  • $fields - fields that will be displayed in the list.

Now in the page class in the $component property you need to override the component to display the list:

 namespaces
use MoonShine\Crud\Contracts\PageComponents\DefaultListComponentContract;
use MoonShine\Laravel\Pages\Crud\IndexPage;
 
class ArticleIndexPage extends IndexPage
{
/**
* @var class-string<DefaultListComponentContract>
*/
protected string $component = ArticleListComponent::class;
}
 namespaces
use MoonShine\Crud\Contracts\PageComponents\DefaultListComponentContract;
use MoonShine\Laravel\Pages\Crud\IndexPage;
 
class ArticleIndexPage extends IndexPage
{
/**
* @var class-string<DefaultListComponentContract>
*/
protected string $component = ArticleListComponent::class;
}

You can also change the list component using the getItemsComponent() method:

 namespaces
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\UI\ComponentContract;
 
getItemsComponent(iterable $items, FieldsContract $fields): ComponentContract
 namespaces
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\UI\ComponentContract;
 
getItemsComponent(iterable $items, FieldsContract $fields): ComponentContract
  • $items - field values,
  • $fields - fields.

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

DetailPage

To change the detail view page component, you need to create a class that implements the DefaultDetailComponentContract interface:

 namespaces
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Crud\Contracts\Page\DetailPageContract;
use MoonShine\Crud\Contracts\PageComponents\DefaultDetailComponentContract;
use MoonShine\UI\Components\Table\TableBuilder;
 
final class ArticleDetailComponent implements DefaultDetailComponentContract
{
public function __invoke(
DetailPageContract $page,
?DataWrapperContract $item,
FieldsContract $fields,
): ComponentContract {
$resource = $page->getResource();
 
return TableBuilder::make($fields)
->cast($resource->getCaster())
->items([$item])
->vertical(
title: $resource->isDetailInModal() ? 3 : 2,
value: $resource->isDetailInModal() ? 9 : 10,
)
->simple()
->preview()
->class('table-divider');
}
}
 namespaces
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Contracts\UI\ComponentContract;
use MoonShine\Crud\Contracts\Page\DetailPageContract;
use MoonShine\Crud\Contracts\PageComponents\DefaultDetailComponentContract;
use MoonShine\UI\Components\Table\TableBuilder;
 
final class ArticleDetailComponent implements DefaultDetailComponentContract
{
public function __invoke(
DetailPageContract $page,
?DataWrapperContract $item,
FieldsContract $fields,
): ComponentContract {
$resource = $page->getResource();
 
return TableBuilder::make($fields)
->cast($resource->getCaster())
->items([$item])
->vertical(
title: $resource->isDetailInModal() ? 3 : 2,
value: $resource->isDetailInModal() ? 9 : 10,
)
->simple()
->preview()
->class('table-divider');
}
}

__invoke() method arguments:

  • $page - object of the detailed page on which the component is located,
  • $item - object with data,
  • $fields - fields that will be displayed in the component.

Now in the page class in the $component property you need to override the component for detailed viewing:

 namespaces
use MoonShine\Crud\Contracts\PageComponents\DefaultDetailComponentContract;
use MoonShine\Laravel\Pages\Crud\DetailPage;
 
class ArticleDetailPage extends DetailPage
{
/**
* @var class-string<DefaultDetailComponentContract>
*/
protected string $component = ArticleDetailComponent::class;
}
 namespaces
use MoonShine\Crud\Contracts\PageComponents\DefaultDetailComponentContract;
use MoonShine\Laravel\Pages\Crud\DetailPage;
 
class ArticleDetailPage extends DetailPage
{
/**
* @var class-string<DefaultDetailComponentContract>
*/
protected string $component = ArticleDetailComponent::class;
}

You can also change the main component of the detail view page using the getDetailComponent() method:

 namespaces
use MoonShine\Contracts\UI\ComponentContract;
 
getDetailComponent(bool $withoutFragment = false): ComponentContract
 namespaces
use MoonShine\Contracts\UI\ComponentContract;
 
getDetailComponent(bool $withoutFragment = false): ComponentContract
  • $withoutFragment - flag of whether the component should be wrapped in a Fragment.

FormPage

To change a page component with an element edit form, you need to create a class that implements the DefaultFormContract interface:

 namespaces
use MoonShine\Contracts\Core\DependencyInjection\CoreContract;
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Contracts\UI\FormBuilderContract;
use MoonShine\Core\Traits\WithCore;
use MoonShine\Crud\Collections\Fields;
use MoonShine\Crud\Contracts\Page\FormPageContract;
use MoonShine\Crud\Contracts\PageComponents\DefaultFormContract;
use MoonShine\Support\AlpineJs;
use MoonShine\Support\Enums\JsEvent;
use MoonShine\UI\Components\FormBuilder;
use MoonShine\UI\Fields\Hidden;
 
final class ArticleForm implements DefaultFormContract
{
use WithCore;
 
public function __construct(CoreContract $core) {
$this->setCore($core);
}
 
public function __invoke(
FormPageContract $page,
string $action,
?DataWrapperContract $item,
FieldsContract $fields,
bool $isAsync = true,
): FormBuilderContract
{
$resource = $page->getResource();
 
return FormBuilder::make($action)
->cast($resource->getCaster())
->fill($item)
->fields([
/** @phpstan-ignore argument.templateType */
...$fields
->when(
! \is_null($item),
static fn (Fields $fields): Fields
=> $fields->push(
Hidden::make('_method')->setValue('PUT'),
),
)
->toArray(),
])
->when(
! $page->hasErrorsAbove(),
fn (FormBuilderContract $form): FormBuilderContract => $form->errorsAbove($page->hasErrorsAbove()),
)
->when(
$isAsync,
fn (FormBuilderContract $formBuilder): FormBuilderContract
=> $formBuilder
->async(
events: array_filter([
$resource->getListEventName(
$this->getCore()->getRequest()->getScalar('_component_name', 'default'),
$isAsync && $resource->isItemExists() ? array_filter([
'page' => $this->getCore()->getRequest()->getScalar('page'),
'sort' => $this->getCore()->getRequest()->getScalar('sort'),
]) : [],
),
! $resource->isItemExists() && $resource->isCreateInModal()
? AlpineJs::event(JsEvent::FORM_RESET, $resource->getUriKey())
: null,
]),
),
)
->when(
$page->isPrecognitive() || ($this->getCore()->getCrudRequest()->isFragmentLoad('crud-form') && ! $isAsync),
static fn (FormBuilderContract $form): FormBuilderContract => $form->precognitive(),
)
->name($resource->getUriKey())
->submit(
$this->getCore()->getTranslator()->get('moonshine::ui.save'),
['class' => 'btn-primary btn-lg'],
)
->buttons($page->getFormButtons());
}
}
 namespaces
use MoonShine\Contracts\Core\DependencyInjection\CoreContract;
use MoonShine\Contracts\Core\DependencyInjection\FieldsContract;
use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;
use MoonShine\Contracts\UI\FormBuilderContract;
use MoonShine\Core\Traits\WithCore;
use MoonShine\Crud\Collections\Fields;
use MoonShine\Crud\Contracts\Page\FormPageContract;
use MoonShine\Crud\Contracts\PageComponents\DefaultFormContract;
use MoonShine\Support\AlpineJs;
use MoonShine\Support\Enums\JsEvent;
use MoonShine\UI\Components\FormBuilder;
use MoonShine\UI\Fields\Hidden;
 
final class ArticleForm implements DefaultFormContract
{
use WithCore;
 
public function __construct(CoreContract $core) {
$this->setCore($core);
}
 
public function __invoke(
FormPageContract $page,
string $action,
?DataWrapperContract $item,
FieldsContract $fields,
bool $isAsync = true,
): FormBuilderContract
{
$resource = $page->getResource();
 
return FormBuilder::make($action)
->cast($resource->getCaster())
->fill($item)
->fields([
/** @phpstan-ignore argument.templateType */
...$fields
->when(
! \is_null($item),
static fn (Fields $fields): Fields
=> $fields->push(
Hidden::make('_method')->setValue('PUT'),
),
)
->toArray(),
])
->when(
! $page->hasErrorsAbove(),
fn (FormBuilderContract $form): FormBuilderContract => $form->errorsAbove($page->hasErrorsAbove()),
)
->when(
$isAsync,
fn (FormBuilderContract $formBuilder): FormBuilderContract
=> $formBuilder
->async(
events: array_filter([
$resource->getListEventName(
$this->getCore()->getRequest()->getScalar('_component_name', 'default'),
$isAsync && $resource->isItemExists() ? array_filter([
'page' => $this->getCore()->getRequest()->getScalar('page'),
'sort' => $this->getCore()->getRequest()->getScalar('sort'),
]) : [],
),
! $resource->isItemExists() && $resource->isCreateInModal()
? AlpineJs::event(JsEvent::FORM_RESET, $resource->getUriKey())
: null,
]),
),
)
->when(
$page->isPrecognitive() || ($this->getCore()->getCrudRequest()->isFragmentLoad('crud-form') && ! $isAsync),
static fn (FormBuilderContract $form): FormBuilderContract => $form->precognitive(),
)
->name($resource->getUriKey())
->submit(
$this->getCore()->getTranslator()->get('moonshine::ui.save'),
['class' => 'btn-primary btn-lg'],
)
->buttons($page->getFormButtons());
}
}

__invoke() method arguments:

  • $page - object of the page on which the component is located,
  • $action - form handler,
  • $item - object with data,
  • $fields - fields that will be displayed in the component.

Now in the page class in the $component property you need to override the form component:

 namespaces
use MoonShine\Crud\Contracts\PageComponents\DefaultFormContract;
use MoonShine\Laravel\Pages\Crud\FormPage;
 
class ArticleFormPage extends FormPage
{
/**
* @var class-string<DefaultFormContract>
*/
protected string $component = ArticleForm::class;
}
 namespaces
use MoonShine\Crud\Contracts\PageComponents\DefaultFormContract;
use MoonShine\Laravel\Pages\Crud\FormPage;
 
class ArticleFormPage extends FormPage
{
/**
* @var class-string<DefaultFormContract>
*/
protected string $component = ArticleForm::class;
}

You can also use the getFormComponent() method to change the main component on the form page.

 namespaces
use MoonShine\Contracts\UI\ComponentContract;
 
getFormComponent(bool $withoutFragment = false): ComponentContract
 namespaces
use MoonShine\Contracts\UI\ComponentContract;
 
getFormComponent(bool $withoutFragment = false): ComponentContract
  • $withoutFragment - flag of whether the component should be wrapped in a Fragment.

Simulate Route

We do not recommend using CRUD pages to arbitrary URL. However, if you understand their logic well, you can use CRUD pages on non-standard routes, emulating the necessary URL.

class HomeController extends Controller
{
public function __invoke(FormArticlePage $page, ArticleResource $resource)
{
return $page->simulateRoute($page, $resource);
}
}
class HomeController extends Controller
{
public function __invoke(FormArticlePage $page, ArticleResource $resource)
{
return $page->simulateRoute($page, $resource);
}
}