Основы

Полям отводится важнейшая роль в админ-панели MoonShine.
Они используются в FormBuilder для построения форм, в TableBuilder для создания таблиц, а также в формировании фильтра для ModelResource. Их можно использовать в ваших кастомных страницах и даже вне админ-панели.
Поля в MoonShine не привязаны к модели (за исключением поля Slug, ModelRelationFields, Json в режиме asRelation), поэтому спектр их применения ограничивается только вашей фантазией.

Для удобства у полей реализован fluent интерфейс.

# Make

Для создании экземпляра поля используется статический метод make().

Text::make(Closure|string|null $label = null, ?string $column = null, ?Closure $formatted = null)
  • $label - лейбл, заголовок поля,
  • $column - поле в базе (например name) или отношение (например countries),
  • $formatted - замыкание для форматирования значения поля при превью (везде кроме формы).

Если не указать $column, то поле в базе данных будет определено автоматически на основе $label.

# Форматирование значения

//...
 
public function fields(): array
{
return [
Text::make(
'Name',
'first_name',
fn($item) => $item->first_name . ' ' . $item->last_name
)
];
}
 
//...

Поля не поддерживающие formatted: Json, File, Range, RangeSlider, DateRange, Select, Enum, HasOne, HasMany.

# Label

Если необходимо изменить Label, можно воспользоваться методом setLabel()

setLabel(Closure|string $label)
//...
 
public function fields(): array
{
return [
Slug::make('Slug')
->setLabel(
fn(Field $field) => $field->getData()?->exists
? 'Slug (do not change)'
: 'Slug'
)
];
}
 
//...

Для перевода Label необходимо в качестве названия передать ключ перевода и добавить метод translatable()

translatable(string $key = '')
//...
 
public function fields(): array
{
return [
Text::make('Title')->translatable('ui')
];
}
 
//...

или

//...
 
public function fields(): array
{
return [
Text::make('ui.Title')->translatable()
];
}
 
//...

# Атрибуты

Основные html атрибуты, такие как required, disabled и readonly, у поля необходимо задавать через соответствующие методы.

disabled(Closure|bool|null $condition = null)
hidden(Closure|bool|null $condition = null)
required(Closure|bool|null $condition = null)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->disabled()
->hidden()
->readonly()
];
}
 
//...

Возможность указать и любые другие атрибуты используя метод customAttributes().

customAttributes(array $attributes)
//...
 
public function fields(): array
{
return [
Password::make('Title')
->customAttributes(['autocomplete' => 'off'])
];
}
 
//...

Метод customWrapperAttributes() позволяет добавить атрибуты для wrapper поля.

customWrapperAttributes(array $attributes)
//...
 
public function fields(): array
{
return [
Password::make('Title')
->customWrapperAttributes(['class' => 'mt-8'])
];
}
 
//...

# Alpine.js

Методы позволяющие удобно взаимодействовать с Alpine.js

xData(null|array|string $data = null)

Все в Alpine начинается с директивы x-data. метод xData определяет фрагмент HTML как компонент Alpine и предоставляет реактивные данные для ссылки на этот компонент.

Block::make([])->xData(['title' = 'Hello world']) // title реактивная переменная внутри
xDataMethod(string $method, ...$parameters)

x-data с указанием компонента и его параметров

Block::make([])->xDataMethod('some-component', 'var', ['foo' => 'bar'])
xModel(?string $column = null)

x-model связывание поля с реактивной переменной

Block::make([
Text::make('Title')->xModel()
])->xData(['title' = 'Hello world'])
 
// или
 
Block::make([
Text::make('Name')->xModel('title')
])->xData(['title' = 'Hello world'])
xIf(
string|Closure $variable,
?string $operator = null,
?string $value = null,
bool $wrapper = true
)

x-if скрывает поле, удаляя его из DOM

Block::make([
Select::make('Type')->native()->options([1 => 1, 2 => 2])->xModel(),
Text::make('Title')->xModel()->xIf('type', 1)
])->xData(['title' = 'Hello world', 'type' => 1])
 
// или
 
Block::make([
Select::make('Type')->options([1 => 1, 2 => 2])->xModel(),
Text::make('Title')->xModel()->xIf(fn() => 'type==2||type.value==2')
])->xData(['title' = 'Hello world', 'type' => 1])
 
// если нужно скрыть поле без контейнера
 
Block::make([
Select::make('Type')->native()->options([1 => 1, 2 => 2])->xModel(),
Text::make('Title')->xModel()->xIf('type', '=', 2, wrapper: false)
])->xData(['title' = 'Hello world', 'type' => 1])
xShow(
string|Closure $variable,
?string $operator = null,
?string $value = null,
bool $wrapper = true
)

x-show тоже самое что и x-if, но не удаляет элемент из DOM, а только скрывает

xDisplay(string $value, bool $html = true)

x-html вывод значения

Block::make([
Select::make('Type')
->native()
->options([
1 => 'Платно',
2 => 'Бесплатно',
])
->xModel(),
 
Number::make('Стоимость', 'price')
->xModel()
->xIf('type', '1'),
 
Number::make('Ставка', 'rate')
->xModel()
->xIf('type', '1')
->setValue(90),
 
LineBreak::make(),
 
Div::make()
->xShow('type', '1')
->xDisplay('"Result:" + (price * rate)')
,
])->xData([
'price' => 0,
'rate' => 90,
'type' => '2',
]),

# Подсказка

Полю можно добавить подсказку с описанием вызвав метод hint()

hint(string $hint)
//...
 
public function fields(): array
{
return [
Number::make('Rating')
->hint('From 0 to 5')
->min(0)
->max(5)
->stars()
];
}
 
//...

Полю можно добавить ссылку (например с инструкциями) link().

link(
string|Closure $link,
string|Closure $name = '',
?string $icon = null,
bool $withoutIcon = false,
bool $blank = false
)
//...
 
public function fields(): array
{
return [
Text::make('Link')
->link('https://cutcode.dev', 'CutCode', blank: true)
];
}
 
//...

# Nullable

Если необходимо у поля по умолчанию сохранять NULL, то необходимо использовать метод nullable().

nullable(Closure|bool|null $condition = null)
//...
 
public function fields(): array
{
return [
Password::make('Title')
->nullable()
];
}
 
//...

# Сортировка

Для возможности сортировки поля на главной странице ресурса необходимо добавить метод sortable().

sortable(Closure|string|null $callback = null)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->sortable()
];
}
 
//...

Метод sortable() в качестве параметра может принимать название поля в базе данных или замыкание.

//...
 
public function fields(): array
{
return [
BelongsTo::make('Author')->sortable('author_id'),
 
Text::make('Title')->sortable(function (Builder $query, string $column, string $direction) {
$query->orderBy($column, $direction);
})
];
}
 
//...

# Badge

Для отображения поля в режиме preview в виде badge, необходимо воспользоваться методом badge().

badge(string|Closure|null $color = null)

Доступные цвета:

primary secondary success warning error info

purple pink blue green yellow red gray

//...
 
public function fields(): array
{
return [
Text::make('Title')
->badge(fn($status, Field $field) => 'green')
];
}
 
//...

# Горизонтально отображение

Метод horizontal() позволяет отображать название и поле горизонтально.

horizontal()
//...
 
public function fields(): array
{
return [
Text::make('Title')
->horizontal(),
];
}
 
//...

# Отображение

В ресурсе модели поля отображаются на странице со списком (главная страница) и на страницах создания/редактирования/просмотра.

Чтобы исключить вывод поля на какой-либо странице, можно воспользоваться соответствующими методами hideOnIndex(), hideOnForm(), hideOnDetail() или обратные методы showOnIndex(), showOnForm(), showOnDetail().

Чтобы исключить только со страницы редактирования или добавления - hideOnCreate(), hideOnUpdate(), а также обратные showOnCreate(), showOnUpdate.

Для того чтобы исключить поле на всех страниц можно воспользоваться методом hideOnAll().

hideOnIndex(Closure|bool|null $condition = null)
showOnIndex(Closure|bool|null $condition = null)
hideOnForm(Closure|bool|null $condition = null)
showOnForm(Closure|bool|null $condition = null)
 
hideOnCreate(Closure|bool|null $condition = null)
showOnCreate(Closure|bool|null $condition = null)
 
hideOnUpdate(Closure|bool|null $condition = null)
showOnUpdate(Closure|bool|null $condition = null)
hideOnDetail(Closure|bool|null $condition = null)
showOnDetail(Closure|bool|null $condition = null)
hideOnAll(Closure|bool|null $condition = null)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->hideOnIndex()
->hideOnForm(),
 
Switcher::make('Active')
->hideOnAll()
->showOnIndex(static fn() => true)
];
}
 
//...

Если вам необходимо просто указать какие поля отображать на страницах или изменить очередность вывода, то можно воспользоваться удобным способом переопределения полей .

# Динамическое отображение

Может возникнуть потребность отображать поле только в том случае, если значение у другого поля в форме имеет определенное значение (Например: отображать телефон, только если стоит галочка, что телефон есть).
Для этих целей используется метод showWhen().

showWhen(
string $column,
mixed $operator = null,
mixed $value = null
)

Доступные операторы:

= < > <= >= != in not in

Если оператор не указан, то будет использоваться =

//...
 
public function fields(): array
{
return [
Checkbox::make('Has phone', 'has_phone'),
Phone::make('Phone')
->showWhen('has_phone','=', 1)
];
}
 
//...

Если оператор имеет значение in или not in, то в $value необходимо передать массив, а значения в виде строки.

//...
 
public function fields(): array
{
return [
Select::make('List', 'list')->multiple()->options([
'value 1' => 'Option Label 1',
'value 2' => 'Option Label 2',
'value 3' => 'Option Label 3',
]),
 
Text::make('Name')
->showWhen('list', 'not in', ['value 1', 'value 3']),
 
Textarea::make('Content')
->showWhen('list', 'in', ['value 2', 'value 3'])
];
}
 
//...

В методе showWhen() для полей Json и BelongsToMany получить доступ к вложенным значениям можно через .:

->showWhen('data.content.active', '=', 1)

# Изменение отображения

Когда необходимо изменить view с помощью fluent interface можно воспользоваться методом customView().

customView(string $customView)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->customView('fields.my-custom-input')
];
}
 
//...

Метод changePreview() позволяет переопределить view для превью (везде кроме формы).

changePreview(Closure $closure)
//...
 
public function fields(): array
{
return [
Text::make('Thumbnail')
->changePreview(function ($value, Field $field) {
return view('moonshine::ui.image', [
'value' => Storage::url($value)
]);
})
];
}
 
//...

Метод forcePreview() укажет что поле должно быть всегда в режиме preview

Text::make('Label')->forcePreview()

Метод requestValueResolver() позволяет переопределить логику получения значения из Request

requestValueResolver(Closure $closure)
//...
 
public function fields(): array
{
return [
Text::make('Thumbnail')
->requestValueResolver(function (string $nameDot, mixed $default, Field $field) {
return request($nameDot, $default);
})
];
}
 
//...

Методы beforeRender() и afterRender() позволяют вывести какую-то информацию перед и после поля соответственно.

beforeRender(Closure $closure)
afterRender(Closure $closure)
//...
 
public function fields(): array
{
return [
Image::make('Thumbnail')
->beforeRender(function (Field $field) {
return $field->preview();
})
];
}
 
//...

# Методы по условию

Метод when() реализует fluent interface и выполнит callback, когда первый аргумент, переданный методу, имеет значение true.

when($value = null, callable $callback = null)
//...
 
public function fields(): array
{
return [
Text::make('Slug')
->when(fn() => true, fn(Field $field) => $field->locked())
];
}
 
//...

Экземпляр поля, будет передан в функции callback.

Методу when() может быть передан второй callback, он будет выполнен, когда первый аргумент, переданный методу, имеет значение false.

when($value = null, callable $callback = null, callable $default = null)
//...
 
public function fields(): array
{
return [
Text::make('Slug')
->when(
auth('moonshine')->user()->moonshine_user_role_id === 1,
fn(Field $field) => $field->locked(),
fn(Field $field) => $field->readonly()
)
];
}
 
//...

Метод unless() обратный методу when() и выполнит первый callback, когда первый аргумент имеет значение false, иначе будет выполнен второй callback, если он передан методу.

unless($value = null, callable $callback = null, callable $default = null)
//...
 
public function fields(): array
{
return [
Text::make('Slug')
->unless(
auth('moonshine')->user()->moonshine_user_role_id === 1,
fn(Field $field) => $field->readonly()->hideOnCreate(),
fn(Field $field) => $field->locked()
)
];
}
 
//...

# Заполнение

Поля можно заполнить значениями использую метод fill().

fill(mixed $value, mixed $casted = null)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->fill('Some title')
];
}
 
//...

Метод changeFill() позволяет изменить логику наполнения поля значениями.

fill(mixed $value, mixed $casted = null)
//...
 
public function fields(): array
{
return [
Text::make('Categories')
->changeFill(
fn(Article $data, Field $field) => $data->categories->implode('title', ',')
)
];
}
 
//...

Поля отношений не поддерживают метод changeFill

# Apply

У каждого поля реализован метод apply(), который трансформирует данные с учетом request и resolve методов. Например, трансформирует данные модели для сохранения в базе данных или формирует запрос для фильтрации.

Существует возможность переопределить действия при выполнении метода apply(), для этого необходимо воспользоваться методом onApply() который принимает замыкание.

onApply(Closure $onApply)
//...
 
public function fields(): array
{
return [
Text::make('Thumbnail by link', 'thumbnail')
->onApply(function(Model $item, $value, Field $field) {
$path = 'thumbnail.jpg';
 
if ($value) {
$item->thumbnail = Storage::put($path, file_get_contents($value));
}
 
return $item;
})
];
}
 
//...

Если поле используется для построения фильтра, то в замыкание будет передан Query Builder.

use Illuminate\Contracts\Database\Eloquent\Builder;
 
//...
 
public function filters(): array
{
return [
Switcher::make('Active')
->onApply(fn(Builder $query, $value, Field $field) => $query->where('active', $value))
];
}
 
//...

Если вы не хотите чтобы поле выполняло какие-то действия, то можно воспользоваться методом canApply().

canApply(Closure|bool|null $condition = null)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->canApply()
];
}
 
//...

# События

Иногда может возникнуть потребность переопределить resolve методы, которые выполняются до и после apply(), для этого необходимо воспользоваться соответствующими методами.

onBeforeApply(Closure $onBeforeApply)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->onBeforeApply(function(Model $item, $value, Field $field) {
//
return $item;
})
];
}
onAfterApply(Closure $onAfterApply)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->onAfterApply(function(Model $item, $value, Field $field) {
//
return $item;
})
];
}
onAfterDestroy(Closure $onAfterDestroy)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->onAfterDestroy(function(Model $item, $value, Field $field) {
//
return $item;
})
];
}

# Assets

Для поля есть возможность загрузить дополнительные css стили и js скрипты, используя метод addAssets().

addAssets(array $assets)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->addAssets(['custom.css', 'custom.js'])
];
}

# Wrapper

Поля при отображении в формах используют специальную обертку wrapper для заголовков, подсказок, ссылок и тд. Иногда может возникнуть ситуация, когда требуется отобразить поле без дополнительных элементов.
Метод withoutWrapper() позволяет отключить создание wrapper.

withoutWrapper(mixed $condition = null)
//...
 
public function fields(): array
{
return [
Text::make('Title')
->withoutWrapper()
];
}

# Реактивность

Метод reactive() позволяет реактивно изменять поля.

reactive(
?Closure $callback = null,
bool $lazy = false,
int $debounce = 0,
int $throttle = 0,
)
  • $callback - callback функция,
  • $lazy - отложенный вызов функции,
  • $debounce - время между вызовами функций (ms.),
  • $throttle - интервал вызова функций (ms.).
Callback

Callback функция в методе reactive() принимает параметры, которыми вы можете воспользоваться для построения своей логики.

function(Fields $fields, ?string $value, Field $field, array $values)
  • $fields - реактивные поля,
  • $value - значение поля которе инициирует реактивность,
  • $field - поле инициирующее реактивность,
  • $values - значения реактивных полей.

Поля поддерживающие реактивность: Text, Number, Checkbox, Select и их наследующие.

FormBuilder::make()
->name('my-form')
->fields([
Text::make('Title')
->reactive(function(Fields $fields, ?string $value): Fields {
return tap($fields, static fn ($fields) => $fields
->findByColumn('slug')
?->setValue(str($value ?? '')->slug()->value())
);
}),
 
Text::make('Slug')
->reactive()
])

В данном пример реализовано формирование slug-поля на основе заголовка.
Slug будет генерироваться в процессе ввода текста.

Реактивное поле может менять состояние других полей, но не изменяет свое состояние!

Для изменения состояния поля инициирующего реактивность удобно воспользоваться параметрами callback функции.

Select::make('Category', 'category_id')
->reactive(function(Fields $fields, ?string $value, Field $field, array $values): Fields {
$field->setValue($value);
 
return tap($fields, static fn ($fields) =>
$fields
->findByColumn('article_id')
?->options(
Article::where('category_id', $value)
->get()
->pluck('title', 'id')
->toArray()
);
);
})

# Методы onChange

C помощью методов onChangeMethod() и onChangeUrl() можно добавить логику при изменении значений полей.

Методы onChangeUrl() или onChangeMethod() присутствуют у всех полей, кроме полей отношений HasOne и HasMany.

onChangeUrl()

Метод onChangeUrl() позволяет асинхронно отправить запрос при изменении поля.

onChangeUrl(
Closure $url,
string $method = 'PUT',
array $events = [],
?string $selector = null,
?string $callback = null,
)
  • $url - url запроса,
  • $method - метод асинхронного запроса,
  • $events - вызываемые события после успешного запроса,
  • $selector - selector элемента у которого будет изменяться контент,
  • $callback - js callback функция после получения ответа.
//...
 
public function fields(): array
{
return [
Switcher::make('Active')
->onChangeUrl(fn() => '/endpoint')
];
}

Если требуется заменить область с html после успешного запроса, вы можете в ответе вернуть HTML контент или json с ключом html.

//...
 
public function fields(): array
{
return [
Switcher::make('Active')
->onChangeUrl(fn() => '/endpoint', selector: '#my-selector')
];
}
onChangeMethod()

Метод onChangeMethod() позволяет асинхронно вызывать метод ресурса или страницы при изменении поля без необходимости создавать дополнительные контроллеры.

onChangeMethod(
string $method,
array|Closure $params = [],
?string $message = null,
?string $selector = null,
array $events = [],
?string $callback = null,
?Page $page = null,
?ResourceContract $resource = null,
)
  • $method - наименование метода,
  • $params - параметры для запроса,
  • $message - сообщения,
  • $selector - selector элемента у которого будет изменяться контент,
  • $events - вызываемые события после успешного запроса,
  • $callback - js callback функция после получения ответа,
  • $page - страница содержащая метод,
  • $resource - ресурс содержащий метод.
//...
 
public function fields(): array
{
return [
Switcher::make('Active')
->onChangeMethod('someMethod')
];
}
public function someMethod(MoonShineRequest $request): void
{
// Logic
}

Пример сортировки компонента CardsBuilder в разделе Recipes

# Методы для значений

Получение значение из исходного

Метод fromRaw() позволяет добавить замыкание для получения итоговое значение из исходного.
Данное замыкание используется при импорте данных.

/**
* @param Closure(mixed $raw, static): mixed $callback
* @return $this
*/
modifyRawValue(Closure $callback)
use App\Enums\StatusEnum;
use MoonShine\Fields\Enum;
 
Enum::make('Status')
->attach(StatusEnum::class)
->fromRaw(fn(string $raw, Enum $ctx) => StatusEnum::tryFrom($raw))
Получения необработанного значения

Метод modifyRawValue() позволяет добавить замыкание для получения необработанного значения.
Данное замыкание используется при экспорте данных.

/**
* @param Closure(mixed $raw, static): mixed $callback
* @return $this
*/
modifyRawValue(Closure $callback)
use App\Enums\StatusEnum;
use MoonShine\Fields\Enum;
 
Enum::make('Status')
->attach(StatusEnum::class)
->modifyRawValue(fn(StatusEnum $raw, Enum $ctx) => $raw->value))

# Схема работы поля