Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel Nova - Load dropdown field based on relationship with of another dropdown

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 Regionmodel, 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?

like image 712
Tudor-Radu Barbu Avatar asked Jan 29 '19 14:01

Tudor-Radu Barbu


3 Answers

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.

like image 81
Eric Bachhuber Avatar answered Sep 30 '22 07:09

Eric Bachhuber


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>
like image 34
Birdperson Avatar answered Sep 30 '22 07:09

Birdperson


There is a laravel nova package for that:

https://novapackages.com/packages/orlyapps/nova-belongsto-depend

Maybe the simplest way to do it !

like image 24
Juliatzin Avatar answered Sep 30 '22 07:09

Juliatzin