Skip to main content

Introduction

Fields describe one value across forms, tables, detail pages, filters, exports and model mutation.

Add fields to a resource in the same order a developer expects to read the form. Keep validation, display values and mutation rules on the field that owns the value.

TextField::make('title', __('qore::common.title'))
->setRules(fn () => ['required', 'string', 'max:255'])
->setWidth(widthOnForm: FieldWidth::MD, widthOnColumn: 240)

Identity

  • make(string $name, ?string $label = null): static: create the field.
  • setColumn(string $column): static: use another database column than the field name.
  • fieldComponent(): string: frontend field component name. Override this in custom fields.

Validation

  • setRules(Closure(?Model): array $callback): static: Laravel rules for the field itself.
  • setExtraRules(Closure(?Model): array $callback): static: extra rules, useful when one field validates multiple payload keys.

Rules may receive the current model on edit:

EmailField::make('email')
->setRules(function (?User $model) {
$rule = Rule::unique('users', 'email');

if ($model) {
$rule->ignore($model->getKey());
}

return ['required', 'email:rfc,dns', $rule];
});

Values

  • setDefaultValue(Closure(Form): mixed $callback): static: initial value on create forms.
  • setDisplayValue(?Closure(Model): mixed $detailDisplay = null, ?Closure(Model): mixed $indexDisplay = null): static: value for detail/index.
  • setEditValue(Closure(Model): mixed $callback): static: value used when editing.
  • setExportValue(Closure(Model): mixed $callback): static: export value.
  • getExportColumns(): array: override when one field exports multiple columns.

Mutation

  • setMutator(Closure(array, Model): void $mutateCallback, ?Closure(array, Model): void $mutateLazyCallback = null): static: write payload to model.
  • isMutatable(bool $isMutatable = true): static: disable mutation for display-only fields.
  • setMutationLogger(Closure(Model, Model): void $callback): static: customize or disable logbook entries. The first model is the new model, the second is the original model.

Use mutateLazy when the model must exist first, for example file uploads or pivot syncs.

Most fields write to the model column with the same name. Use setColumn() only when the field name and database column intentionally differ.

Forms

  • setWidth(?FieldWidth $widthOnForm = null, ?int $widthOnColumn = null): static
  • setInputSize(SizeType $size): static
  • setPlaceHolder(string $placeholder): static
  • setHint(string $hint): static
  • setTooltip(string $tooltip): static
  • setIsClearable(bool $isClearable): static
  • setLabelVisibleOnForm(bool $visible = true): static
  • setDebounceValue(int $ms): static

Dynamic Forms

  • onUpdate(Closure(Form, mixed $newValue, mixed $previousValue): void $callback, bool $triggerImmediately = false): static: run when this field changes.
  • setDependency(string $fieldName, Closure(Form): void $callback): static: run when another field changes.
  • onFormRequest(Closure(Form): void $callback): static: run during the form request lifecycle before serialization.
  • onFormResponse(Closure(Form): void $callback): static: run during the form response lifecycle.
  • setFieldIsHidden(?Closure(Form): bool $callback, array $dependencies = []): static
  • setFieldIsDisabled(Closure(Form): bool $callback, array $dependencies = []): static
  • setFieldIsReadOnly(Closure(Form): bool $callback, array $dependencies = []): static

The form request lifecycle runs before the node is serialized. It has already loaded default field values and handled field updates, so it is the right place to inspect current values, hide or disable fields, and mutate values before the frontend receives the response.

The form response lifecycle runs after the request lifecycle and just before FormNode::onFormReady(). Use it when a field needs to add response-only data after request handling is complete, such as lazy options or calculated UI metadata.

SelectField::make('type')
->setOptions([...]);

TextField::make('vat_number')
->setDependency('type', function (Form $form) {
// Recalculate when type changes.
})
->setFieldIsHidden(
fn (Form $form) => $form->getFieldValue('type') !== 'company',
dependencies: ['type']
);

Field Lifecycle Examples

Use onUpdate() on the field that changed when you want to mutate another field immediately:

SelectField::make('customer_id')
->setOptions(fn (SelectMetadata $metadata) => $this->customerOptions($metadata))
->onUpdate(function (Form $form, mixed $customerId) {
if (! $customerId) {
$form->setFieldValue('invoice_email', null);

return;
}

$customer = Customer::query()->find($customerId);

$form->setFieldValue('invoice_email', $customer?->invoice_email);
});

Pass triggerImmediately: true when the field should fill dependent values as soon as the form first opens:

SelectField::make('customer_id')
->setDefaultValue(fn () => request()->integer('customer_id') ?: null)
->onUpdate(function (Form $form, mixed $customerId) {
$customer = Customer::query()->find($customerId);

$form->setFieldValue('invoice_email', $customer?->invoice_email);
}, triggerImmediately: true);

Use setDependency() on the field that depends on another field. This is useful when the dependent field owns the recalculation:

TextField::make('invoice_email')
->setDependency('customer_id', function (Form $form) {
$customer = Customer::query()->find($form->getFieldValue('customer_id'));

$form->setFieldValue('invoice_email', $customer?->invoice_email);
});

Use onFormRequest() when the field needs to inspect or mutate form state on every request:

TextField::make('reference')
->onFormRequest(function (Form $form) {
if (! $form->isInitialRequest()) {
return;
}

if ($form->getFieldValue('reference')) {
return;
}

$form->setFieldValue('reference', ReferenceService::makeNextReference());
});

Use onFormResponse() when the field needs response-time data after request handling:

SelectField::make('contact_id')
->setShouldLoadOptionsImmediately(false)
->onFormResponse(function (Form $form) {
$customerId = $form->getFieldValue('customer_id');

if (! $customerId) {
return;
}

$form->setFieldValue('contact_hint', "Showing contacts for customer #{$customerId}");
});

Use setFieldIsHidden(), setFieldIsDisabled() and setFieldIsReadOnly() for field state. Pass dependencies so Qore knows which field changes should refresh this state:

TextField::make('vat_number')
->setFieldIsHidden(
fn (Form $form) => $form->getFieldValue('type') !== 'company',
dependencies: ['type'],
);

TextField::make('invoice_email')
->setFieldIsReadOnly(
fn (Form $form) => $form->getModel()?->invoice_sent_at !== null,
dependencies: ['customer_id'],
);

Inside lifecycle callbacks, the Form object gives access to the form values, model, resource, parent relation context and node manager:

$form->getFieldValue('customer_id');
$form->setFieldValue('invoice_email', 'finance@example.test');
$form->getModel();
$form->getResource();
$form->getParentModel();
$form->getParentFieldValue('customer_id');
$form->getNodeManager();

Resource Visibility

  • setIsShownOnCreate(Closure(): bool $callback): static
  • setIsShownOnEdit(Closure(Model): bool $callback): static
  • setIsShownOnDetail(Closure(Model): bool $callback): static
  • setIsShownOnIndex(Closure(): bool $callback): static
  • setIsShownOnForms(Closure(): bool $callback): static
  • setIsOnlyShownOnCreate(): static
  • setIsOnlyShownOnEdit(): static
  • setIsOnlyShownOnDetail(): static
  • setIsOnlyShownOnIndex(): static
  • setIsOnlyShownOnForms(): static
  • setIsVisibleInTablesByDefault(bool $value): static

Tables

  • setIsFilterable(bool $value = true): static
  • setFilter(Closure(Builder, mixed): void $callback): static
  • setIsSortable(bool $value = true): static
  • setSorter(Closure(Builder, TableSortOrder): void $callback): static
  • setWidthOnColumn(?int $widthOnColumn): static
  • setMinWidthOnColumn(?int $minWidthOnColumn): static

Use custom filters/sorters when the field value is not a plain column.