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.

 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,
];
}
}
 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.

 namespaces
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.

 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'),
];
}
}
 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.
 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();
});
}
}
 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.
 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();
}
}
 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.

 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
 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.
 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());
}
}
 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.

 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 components of a specific layer from a resource or page, 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 to a specified page in the desired layer from a resource, use the resource's onLoad() method and the page's pushToLayer().

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

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)->loaded();
}
}
class HomeController extends Controller
{
public function __invoke(FormArticlePage $page, ArticleResource $resource)
{
return $page->simulateRoute($page, $resource)->loaded();
}
}