Registered Relation Field Types
Relation fields are registered types. Every relation is registered in a module service provider and referenced in YAML with an explicit type such as customerRelation, vehicleRelation, authorRelation or pageRelation. The generic type: relation is not supported — an unregistered relation type fails explicitly during rendering.
YAML Usage
- name: detailData.customer_id
label: customer_label_customer
type: customerRelation
colspan: 6The registered definition supplies the list component, detail target and title resolution — nothing else is configured in YAML.
Registering a Relation Type
Register the type in the module that owns the target model:
use Noerd\Services\RelationFieldRegistry;
use Noerd\Support\RelationFieldDefinition;
use Noerd\Customer\Models\Customer;
$relationFieldRegistry = $this->app->make(RelationFieldRegistry::class);
$relationFieldRegistry->register('customerRelation', RelationFieldDefinition::model(
listComponent: 'customers-list',
detailComponent: 'customer-detail',
modelClass: Customer::class,
titleResolver: 'name',
));Definition Parameters (RelationFieldDefinition::model())
| Parameter | Description |
|---|---|
listComponent | List opened in select mode (required) |
detailComponent | Detail modal opened for existing values |
modelClass | Model used to hydrate the saved relation value |
titleResolver | Model attribute name or callback that returns the display title (default 'name') |
selectEvent | Custom selection event name; defaults to the {entity}Selected convention derived from the list component |
detailRoute | Named Route::livewire() route opened as a modal for existing values — preferred over detailComponent when the record is addressable (see Modals); detailComponent stays as the fallback when the route is not registered |
fieldComponent | Livewire component rendering the field in the detail form; null (default) uses the generic noerd-relation-field input. See Custom Renderer Component |
Custom Title Resolver
$relationFieldRegistry->register('quoteRelation', RelationFieldDefinition::model(
listComponent: 'quotes-list',
detailComponent: 'quote-detail',
modelClass: Quote::class,
titleResolver: fn (Quote $quote): string => $quote->number . ' (' . \Number::currency($quote->total_net, in: 'EUR', locale: 'de') . ')',
));Custom Renderer Component
By default every relation type renders as the generic readonly input with a clear button and a magnifier (noerd-relation-field). A module can replace that markup for a single relation type — e.g. render an address as a clickable card — by passing its own Livewire component as fieldComponent:
$relationFieldRegistry->register('customerAddressCardRelation', RelationFieldDefinition::model(
listComponent: 'customer::customer-addresses-list',
detailComponent: 'customer::customer-address-detail',
detailRoute: 'customer.address.detail',
modelClass: CustomerAddress::class,
titleResolver: fn (CustomerAddress $address): string => $address->label ?? '',
fieldComponent: 'customer::customer-address-card-field',
));The component MUST extend Noerd\Livewire\RelationFieldComponent — it then inherits the complete behaviour (mount hydration, the noerdRelationSelected round trip, clear(), openDetail(), setFieldValue sync to the parent detail) and receives exactly the same props as the generic renderer (relationType, fieldName, label, value, modelId, theme, …). Only the Blade markup differs. The single-file component is two lines plus markup:
<?php
new class extends \Noerd\Livewire\RelationFieldComponent {}; ?>
<div>
{{-- custom markup; open the picker exactly like the generic template: --}}
{{-- @click="$modal('{{ $listComponent }}', {id: {{ $modelId ?: 'null' }}, context: '{{ $fieldName }}', listActionMethod: 'selectAction'})" --}}
</div>For markup that shows more than the title, the base class exposes the related Eloquent record via $this->relatedModel() (resolved through the definition's modelClass; null while the field is empty). Non-default themes resolve a {component}-{theme} sibling when it exists and fall back to the component itself (see Themes).
Polymorphic Relation Fields
For a field that may point at one of several relation types (e.g. an invoice source that is either an order or a quote), register a polymorphic type with the allowed relation types:
$relationFieldRegistry->registerPolymorphic('invoiceSourceRelation', [
'orderRelation',
'quoteRelation',
]);Each allowed type must itself be a registered relation type. The YAML field additionally names the column that stores the selected type:
- name: detailData.source_id
typeField: detailData.source_type
label: Source
type: invoiceSourceRelation
colspan: 6Polymorphic fields render through the shared Livewire component noerd-polymorphic-relation-field, which shows a type selector next to the relation input.
Runtime Behaviour
All registered relation types render through the shared Livewire component
noerd-relation-field(polymorphic types throughnoerd-polymorphic-relation-field), unless the definition names a customfieldComponentSelection uses the generic event
noerdRelationSelected; the{entity}Selectedevent (or the definition'sselectEvent) is dispatched as well, so detail components can listen with#[On('customerSelected')]The handler sets the foreign key in
detailDataand the display value in the genericrelationTitlesarray — never a separate display property ($this->customer); the YAML field references it withrelationField: relationTitles.customer_id:php#[On('customerSelected')] public function customerSelected(int $customerId): void { $customer = Customer::find($customerId); $this->detailData['customer_id'] = $customer->id; $this->relationTitles['customer_id'] = $customer->name; }Registering a relation type automatically registers a matching field type in the
FieldTypeRegistry— no separate field-type registration is neededUnregistered relation types fail explicitly during rendering
Theme Templates
The two Livewire components delegate their markup to the active theme's templates relation-field.blade.php / polymorphic-relation-field.blade.php (see Themes). The detail block passes the field's theme as the theme prop; a theme without an own relation template falls back to the default theme's.
The behaviour lives once in the abstract Noerd\Livewire\RelationFieldComponent / Noerd\Livewire\PolymorphicRelationFieldComponent; a copied theme folder restyles relation fields by editing the two templates — no PHP is duplicated.
The numbered templates render inside <x-noerd::detail.numbered-row> and need the row number: RelationFieldRegistry puts number into the component props whenever the detail block numbered the field (i.e. only in a theme with numbersRows), and the base class exposes it as $this->numberedRowField().