В данном рецепте мы покажем как кастомизировать страницу с формой (взяли пример статей из основного демо репозитория), где вынесем поля отношений в отдельные вкладки, а также этот рецепт даст понимание огромных возможностей кастомизации страниц.
Создание страницы с формой
Для начала вам необходимо добавить страницу, в нашем примере это ArticleFormPage
, которая наследует FormPage
.
После необходимо заменить FormPage
на ArticleFormPage
в ресурсе:
class ArticleResource extends ModelResource{ // ... protected function pages(): array { return [ IndexPage::class, // FormPage::class -> ArticleFormPage::class ArticleFormPage::class, DetailPage::class ]; }
Кастомизация страницы
namespace App\MoonShine\Pages\Article; use App\Models\Comment;use App\MoonShine\Resources\CommentResource;use App\MoonShine\Resources\MoonShineUserResource;use MoonShine\Laravel\Fields\Relationships\BelongsTo;use MoonShine\Laravel\Fields\Relationships\BelongsToMany;use MoonShine\Laravel\Fields\Relationships\HasMany;use MoonShine\Laravel\Fields\Relationships\HasOne;use MoonShine\Laravel\Fields\Slug;use MoonShine\Laravel\Pages\Crud\FormPage;use MoonShine\Laravel\TypeCasts\ModelCaster;use MoonShine\TinyMce\Fields\TinyMce;use MoonShine\UI\Components\ActionButton;use MoonShine\UI\Components\Collapse;use MoonShine\UI\Components\Heading;use MoonShine\UI\Components\Layout\Box;use MoonShine\UI\Components\Layout\Column;use MoonShine\UI\Components\Layout\Flex;use MoonShine\UI\Components\Layout\Grid;use MoonShine\UI\Components\Layout\LineBreak;use MoonShine\UI\Components\Table\TableBuilder;use MoonShine\UI\Components\Tabs;use MoonShine\UI\Components\Tabs\Tab;use MoonShine\UI\Fields\Color;use MoonShine\UI\Fields\ID;use MoonShine\UI\Fields\Image;use MoonShine\UI\Fields\Json;use MoonShine\UI\Fields\Number;use MoonShine\UI\Fields\Preview;use MoonShine\UI\Fields\RangeSlider;use MoonShine\UI\Fields\StackFields;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text;use MoonShine\UI\Fields\Url; final class ArticleFormPage extends FormPage{ protected function fields(): iterable { return [ ID::make(), Grid::make([ Column::make([ Box::make('Main information', [ ActionButton::make( 'Link to article', $this->getResource()->getItem()?->getKey() ? route('articles.show', $this->getResource()->getItem()) : '/', ) ->icon('paper-clip') ->blank(), LineBreak::make(), BelongsTo::make('Author', resource: MoonShineUserResource::class) ->asyncSearch() ->canSee(fn () => auth()->user()->moonshine_user_role_id === 1) ->required(), Collapse::make('Title/Slug', [ Heading::make('Title/Slug'), Flex::make([ Text::make('Title') ->withoutWrapper() ->required() , Slug::make('Slug') ->from('title') ->unique() ->separator('-') ->withoutWrapper() ->required() , ]) ->name('flex-titles') ->justifyAlign('start') ->itemsAlign('start'), ]), StackFields::make('Files')->fields([ Image::make('Thumbnail') ->removable() ->disk('public') ->dir('articles'), ]), Preview::make('No input field', 'no_input', static fn () => fake()->realText()), RangeSlider::make('Age') ->min(0) ->max(60) ->step(1) ->fromTo('age_from', 'age_to'), Number::make('Rating') ->hint('From 0 to 5') ->min(0) ->max(5) ->link('https://cutcode.dev', 'CutCode', blank: true) ->stars(), Url::make('Link') ->hint('Url') ->link('https://cutcode.dev', 'CutCode', blank: true) ->suffix('url') , Color::make('Color'), Json::make('Data')->fields([ Text::make('Title'), Text::make('Value'), ])->creatable()->removable(), Switcher::make('Active'), ]), ])->columnSpan(6), Column::make([ Box::make('Seo and categories', [ Tabs::make([ Tab::make('Seo', [ Text::make('Seo title') ->withoutWrapper(), Text::make('Seo description') ->withoutWrapper(), TinyMce::make('Description') ->addPlugins(['code', 'codesample']) ->toolbar(' | code codesample') ->required() , ]), Tab::make('Categories', [ BelongsToMany::make('Categories')->tree('category_id'), ]), ]), ]), ])->columnSpan(6), ]), $this->getCommentsField(), $this->getCommentField(), ]; } private function getCommentsField(): HasMany { return HasMany::make('Comments', resource: CommentResource::class) ->fillData($this->getResource()->getItem()) ->async() ->creatable(); } private function getCommentField(): HasOne { return HasOne::make('Comment', resource: CommentResource::class) ->fillData($this->getResource()->getItem()) ->async(); } protected function mainLayer(): array { return [ Tabs::make([ Tab::make('Basics', parent::mainLayer()), Tab::make('Comments', [ $this->getResource()->getItem() ? $this->getCommentsField() : 'To add comments, save the article', ]), Tab::make('Comment', [ $this->getResource()->getItem() ? $this->getCommentField() : 'To add comments, save the article', ]), Tab::make('Table', [ TableBuilder::make() ->fields([ ID::make(), Text::make('Text') ]) ->cast(new ModelCaster(Comment::class)) ->items($this->getResource()->getItem()?->comments ?? []) ]), ]), ]; } protected function bottomLayer(): array { return []; }}
Стоит обратить внимание на то, что мы вынесли HasOne
и HasMany
поле в отдельные методы, которые также дублируем в методе fields
.
Это необходимо для того, чтобы MoonShine при взаимодействии с полями мог найти их в системе.
final class ArticleFormPage extends FormPage{ protected function fields(): iterable { return [ ID::make(), // ... $this->getCommentsField(), $this->getCommentField(), ]; } private function getCommentsField(): HasMany { return HasMany::make('Comments', resource: CommentResource::class) ->fillData($this->getResource()->getItem()) ->async() ->creatable(); } private function getCommentField(): HasOne { return HasOne::make('Comment', resource: CommentResource::class) ->fillData($this->getResource()->getItem()) ->async(); } protected function mainLayer(): array { return [ Tabs::make([ Tab::make('Basics', parent::mainLayer()), Tab::make('Comments', [ $this->getResource()->getItem() ? $this->getCommentsField() : 'To add comments, save the article', ]), Tab::make('Comment', [ $this->getResource()->getItem() ? $this->getCommentField() : 'To add comments, save the article', ]), Tab::make('Table', [ TableBuilder::make() ->fields([ ID::make(), Text::make('Text') ]) ->cast(new ModelCaster(Comment::class)) ->items($this->getResource()->getItem()?->comments ?? []) ]), ]), ]; } protected function bottomLayer(): array { return []; }}
Метод bottomLayer
обнулен, чтобы не дублировать поля отношений под основной формой
Дополнительно в этом рецепте мы добавили TableBuilder
, где также выводим данные из отношения, тем самым мы хотим показать что вы не ограниченны только полями,
а можете выводить дополнительно собственные таблицы и формы
При более сложной кастомизации страниц необходимо соблюдать правила:
- Не создавать формы внутри форм, это приведет к конфликтам или потребует дополнительных действий (не связано с MoonShine),
- Не забывайте наполнять поля данными и приводить к нужному типу.