5
votes

Laravel Nova - Champ de liste déroulante de charge basé sur la relation avec une autre liste déroulante

J'ai cette ressource appelée Distributeur

  ID::make()->sortable(),
            Text::make('Name')
                ->creationRules('required'),
            BelongsTo::make('Region')
                ->creationRules('required')
                ->searchable(),
            BelongsTo::make('Country')
                ->creationRules('required')
                ->searchable(),

Tout est en place jusqu'à présent. Mais le modèle du Pays doit dépendre du modèle de la Région , donc lorsque je sélectionne une région, je souhaite afficher les options avec les pays liés à cette région.

Région et Country sont déjà liés dans leurs modèles basés sur des relations belongsToMany .

Y a-t-il un moyen de faire en sorte que ces champs fonctionnent ensemble?


0 commentaires

3 Réponses :


2
votes

Je me rends compte que cette question a presque un an maintenant, mais je me suis dit que je répondrais comme 1. la question continue de générer du trafic et 2. nous avons récemment rencontré un problème identique et avons été déçus par le manque de disponibilité informations.

Autant que je sache, ce problème pourrait également être résolu avec des requêtes pertinentes, mais nous avons fini par ajouter un champ personnalisé pour diverses raisons. La documentation officielle pour les champs personnalisés est assez rare, mais devrait suffit pour commencer.

Notre champ personnalisé est resté assez simple du côté de Vue. La seule vraie logique que Vue gère est d'extraire les pays / états de notre API et de les remplir dans les listes déroulantes. Du côté PHP, nous avons fini par avoir besoin de surcharger deux fonctions dans le contrôleur de notre champ: fillAttributeFromRequest () et résoudre (). Voir ci-dessous:

CountryState.php:

CountryState::make('Country and State')->rules('required')

FormField.vue

<template>
    <span>{{ field.value }}</span>
</template>

<script>
export default {
    props: ['resourceName', 'field',],
}
</script>

IndexField.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>

Enfin, dans le tableau des champs de notre ressource Nova:

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']; 
    }
}

Ces exemples auraient certainement besoin d'être peaufinés avant d'être "prêts pour la production", mais j'espère qu'ils aideront tous ceux qui osent s'aventurer dans la nature trou de lapin qui est la personnalisation Nova.


0 commentaires

2
votes

La réponse d'Erics m'a beaucoup aidé. Merci!

Mais au lieu d'écrire mes propres fonctions de remplissage et de résolution dans le champ Nova personnalisé, je viens d'hériter du champ BelongsTo:

<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>

Les données supplémentaires dans la fonction jsonSerialize peut ensuite être utilisé, pour pré-remplir votre élément de sélection frontend:

<?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());
  }
}


0 commentaires

0
votes

Il existe un package laravel nova pour cela:

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

Peut-être la façon la plus simple de le faire!


0 commentaires