Custom Field
Create a custom field when backend behaviour differs from the built-in fields. Register frontend renderers when the field needs custom UI on forms, detail pages, indexes or filters.
Before creating a field, check whether a built-in field with setDisplayValue(), setMutator() or NodeField is enough. Custom fields are best for behaviour that will be reused.
Backend
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Qore\Next\System\Field\Field;
use Qore\Next\System\Node\Table\TableSortOrder;
class FullNameField extends Field
{
public function __construct(string $name = 'full_name', ?string $label = null)
{
parent::__construct($name, $label);
$this->setRules(fn () => ['nullable', 'string']);
/**
* @param Builder<Model> $builder
*/
$this->setFilter(function (Builder $builder, mixed $value) {
$builder->whereRaw("CONCAT(first_name, ' ', last_name) like ?", ["%{$value}%"]);
});
/**
* @param Builder<Model> $builder
*/
$this->setSorter(function (Builder $builder, TableSortOrder $sort) {
$builder
->orderBy('first_name', $sort->value)
->orderBy('last_name', $sort->value);
});
}
public function fieldComponent(): string
{
return 'FullName';
}
}
Frontend
Register the frontend field in resources/js/fields.ts.
import {
FieldDetailProps,
FieldFilterProps,
FieldFormProps,
FieldIndexProps,
registerField,
useFieldForm
} from 'qore-next'
import { Input, Space, Typography } from 'antd'
type FullNameValue = {
first_name?: string
last_name?: string
}
function FullNameForm(field: FieldFormProps<FullNameValue>) {
const { inputAttributes, setValue } = useFieldForm(field)
const value = field.value ?? {}
return (
<Space.Compact className="w-full">
<Input
{...inputAttributes}
value={value.first_name ?? ''}
onChange={(event) => {
setValue({ ...value, first_name: event.target.value })
}}
/>
<Input
value={value.last_name ?? ''}
onChange={(event) => {
setValue({ ...value, last_name: event.target.value })
}}
/>
</Space.Compact>
)
}
function FullNameDetail(field: FieldDetailProps<string>) {
return <Typography.Text>{field.value}</Typography.Text>
}
function FullNameIndex(field: FieldIndexProps<string>) {
return field.value
}
function FullNameFilter(field: FieldFilterProps) {
return (
<Input
value={field.value.length > 0 ? field.value[0] : ''}
placeholder={field.label}
onChange={(event) => {
field.onChange([event.target.value])
}}
onPressEnter={() => {
field.confirm()
}}
/>
)
}
registerField({
name: 'FullName',
form: FullNameForm,
detail: FullNameDetail,
index: FullNameIndex,
filter: FullNameFilter
})
registerField() accepts separate renderers for every place the field can appear. Reuse built-in renderers from fieldComponents when only one surface is custom.