I have this resource called Distributor
ID::make()->sortable(),
Text::make('Name')
->creationRules('required'),
BelongsTo::make('Region')
->creationRules('required')
->searchable(),
BelongsTo::make('Country')
->creationRules('required')
->searchable(),
Everything is on place till now. But the Country
model should depend on Region
model, so when I select a region I want to display options with countries related to that Region.
Region and Country are already related in their models based on belongsToMany
relationships.
Is there a way to do this fields work together?
I realize this question is almost a year old now, but I figured I would answer as 1. the question is still getting traffic and 2. we recently ran into an identical issue and were disappointed with the lack of available information.
As far as I know, this problem could also be solved with relatable queries, but we ended up adding a custom field for various reasons. The official documentation for custom fields is quite sparse, but should be enough to get started.
Our custom field remained quite simple on the Vue side. The only real logic that Vue handles is to pull countries/states from our API, and to fill them into the dropdowns. On the PHP side, we ended up needing to override two functions in our field's controller: fillAttributeFromRequest() and resolve(). See below:
CountryState.php :
namespace Gamefor\CountryState;
use Laravel\Nova\Fields\Field;
class CountryState extends Field
{
public $component = 'country-state';
/**
* Hydrate the given attribute on the model based on the incoming request.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param string $requestAttribute
* @param object $model
* @param string $attribute
* @return void
*/
protected function fillAttributeFromRequest($request, $requestAttribute, $model, $attribute)
{
parent::fillAttributeFromRequest($request, $requestAttribute, $model, $attribute);
if ($request->exists('state_id')) {
$model->state_id = $request['state_id'];
}
if ($request->exists('country_id')) {
$model->country_id = $request['country_id'];
}
}
/**
* Resolve the field's value for display.
*
* @param mixed $resource
* @param string|null $attribute
* @return void
*/
public function resolve($resource, $attribute = null)
{
// Model has both country_id and state_id foreign keys
// In the model, we have
//
// public function country(){
// return $this->belongsTo('App\Country', 'country_id', 'id');
// }
//
// public function state(){
// return $this->belongsTo('App\State', 'state_id', 'id');
// }
$this->value = $resource->country['name'] . ', ' . $resource->state['name'];
}
}
FormField.vue
<template>
<default-field :field="field" :errors="errors">
<template slot="field">
<select
name="country"
ref="menu"
id="country"
class="form-control form-select mb-3 w-full"
v-model="selectedCountryId"
@change="updateStateDropdown"
>
<option
:key="country.id"
:value="country.id"
v-for="country in countries"
>
{{ country.name }}
</option>
</select>
<select
v-if="states.length > 0"
name="state"
ref="menu"
id="state"
class="form-control form-select mb-3 w-full"
v-model="selectedStateId"
>
<option :value="state.id" :key="state" v-for="state in states">
{{ state.name }}
</option>
</select>
</template>
</default-field>
</template>
<script>
import { FormField, HandlesValidationErrors } from "laravel-nova";
export default {
mixins: [FormField, HandlesValidationErrors],
props: {
name: String
},
data() {
return {
countries: [],
states: [],
allStates: [],
selectedCountryId: null,
selectedStateId: null
};
},
created: function() {
this.fetchCountriesWithStates();
},
methods: {
updateStateDropdown() {
this.states = this.allStates.filter(
item => item.country_id === this.selectedCountryId
);
this.selectedStateId = this.states.length > 0 ? this.states[0].id : null;
},
async fetchCountriesWithStates() {
const countryResponse = await Nova.request().get("/api/v1/countries");
const stateResponse = await Nova.request().get("/api/v1/states");
this.countries = countryResponse.data;
this.allStates = stateResponse.data;
this.updateStateDropdown();
},
fill(formData){
formData.append('country_id', this.selectedCountryId);
formData.append('state_id', this.selectedStateId);
},
},
};
</script>
IndexField.vue
<template>
<span>{{ field.value }}</span>
</template>
<script>
export default {
props: ['resourceName', 'field',],
}
</script>
Lastly, in our Nova resource's fields array:
CountryState::make('Country and State')->rules('required')
These samples would definitely need tweaking before they are "production ready", but hopefully they help anyone who dares venture into the wild rabbit hole that is Nova customization.
Erics answer helped me a lot. Thanks!
But instead of writing my own fill and resolve functions in the custom Nova field, I just inherited the BelongsTo Field:
<?php
namespace Travelguide\DestinationSelect;
use App\Models\Destination;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Http\Requests\NovaRequest;
class DestinationSelect extends \Laravel\Nova\Fields\BelongsTo
{
/**
* The field's component.
*
* @var string
*/
public $component = 'destination-select';
/**
* Prepare the field for JSON serialization.
*
* @return array
*/
public function jsonSerialize()
{
$parentId = null;
$parentName = null;
if (isset($this->belongsToId)) {
$destination = Destination::where('id', $this->belongsToId)->first();
if (isset($destination) && isset($destination->parent)) {
$parentId = $destination->parent->id;
$parentName = $destination->parent->name;
}
}
return array_merge([
'parent_id' => $parentId,
'parent_name' => $parentName,
], parent::jsonSerialize());
}
}
The additional data in the jsonSerialize function can then be used, to pre-fill your frontend select element:
<template>
<default-field :field="field" :errors="errors">
<template slot="field">
<select
name="country"
ref="menu"
id="country"
class="form-control form-select mb-3 w-full"
v-model="selectedCountryId"
@change="onCountryChange"
>
<option
:key="country.id"
:value="country.id"
v-for="country in countries"
>
{{ country.name }}
</option>
</select>
<select
v-if="regions.length > 0"
name="region"
ref="menu"
id="region"
class="form-control form-select mb-3 w-full"
v-model="selectedRegionId"
>
<option :value="region.id" :key="region" v-for="region in regions">
{{ region.name }}
</option>
</select>
</template>
</default-field>
</template>
<script>
import { FormField, HandlesValidationErrors } from "laravel-nova";
export default {
mixins: [FormField, HandlesValidationErrors],
props: ['resourceName', 'field'],
data() {
return {
countries: [],
regions: [],
selectedCountryId: null,
selectedRegionId: null
};
},
created: function() {
this.fetchCountries();
},
methods: {
async fetchCountries() {
const countryResponse = await Nova.request().get("/api/destinations");
this.countries = countryResponse.data;
// Add 'null' option to countries
this.countries.unshift({
name: '-',
id: null
});
if (this.field.parent_id) {
this.selectedCountryId = this.field.parent_id;
this.selectedRegionId = this.field.belongsToId || null;
} else {
this.selectedCountryId = this.field.belongsToId || null;
}
this.updateRegionDropdown();
},
async updateRegionDropdown() {
if (!this.selectedCountryId) {
return;
}
// Get all regions of the selected country
const regionResponse = await Nova.request().get("/api/destinations/" + this.selectedCountryId);
this.regions = regionResponse.data;
// Add 'null' option to regions
this.regions.unshift({
name: '-',
id: null
});
},
onCountryChange() {
// De-select current region and load all regions of new country
this.selectedRegionId = null;
this.updateRegionDropdown();
},
fill(formData) {
if (this.selectedRegionId) {
formData.append('destination', this.selectedRegionId);
} else if (this.selectedCountryId) {
formData.append('destination', this.selectedCountryId);
}
},
},
};
</script>
There is a laravel nova package for that:
https://novapackages.com/packages/orlyapps/nova-belongsto-depend
Maybe the simplest way to do it !
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With