Search code examples
laravellaravel-livewire

Laravel livewire dynamic dropdown with lots of relations on a single model


I have a fairly complex issue, I have an animal

model

class Animal extends Model
{
    use HasFactory;

    protected $fillable = [
        "breed_ID",
        "name",
        "color_ID",
        "eyes_color_ID",
        "birth_date",
        "animal_types_id",
        "born_location",
        "profile_picture_id",
        "gender_ID",
        "status",
        "image",
        "bio",
        "lenght",
        "weight",
        "passport_url",
        "chip_number",
        "breeder_ID",
    ];

    protected function genders(): BelongsTo
    {
        return $this->belongsTo(Gender::class);
    }

    public function borns(): BelongsTo
    {
        return $this->belongsTo(Born::class);
    }

    public function eyeColors(): BelongsTo
    {
        return $this->belongsTo(EyeColor::class);
    }

    public function colors(): BelongsTo
    {
        return $this->belongsTo(Color::class);
    }

    public function breeders(): BelongsTo
    {
        return $this->belongsTo(Breeder::class);
    }

    public function weights(): BelongsTo
    {
        return $this->belongsTo(Weight::class);
    }

    public function lengths(): BelongsTo
    {
        return $this->belongsTo(Length::class);
    }

    public function users(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function animalTypes(): BelongsTo
    {
        return $this->belongsTo(AnimalType::class);
    }

    public function images(): HasMany
    {
        return $this->hasMany(Image::class);
    }
}

This animal has a breed, gender, color e.t.c

When a user wants to add a new animal they are presented a form, this form is a full page livewire component.

<main class="add-animal-page">
    <section class="relative">
        <div class="container px-4 mx-auto">
            <div
                class="flex flex-col justify-center w-full min-w-0 mb-6 break-words rounded-lg shadow-xl xl:flex-row bg-gray-50">
                <form enctype="multipart/form-data" class="flex justify-center" wire:submit.prevent="upload">
                    <div class="w-full xl:w-4/6">
                        <div class="flex justify-center px-4 py-5 sm:p-6">
                            <div class="grid max-w-4xl grid-cols-6 gap-6">
                                <div class="col-span-6 sm:col-span-3 lg:col-span-2">
                                    <label for="first_name" class="block text-sm font-medium text-gray-700">Name</label>
                                    <input type="text" name="first_name" id="first_name" autocomplete="given-name"
                                        wire:model.defer="animal.name"
                                        class="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                </div>
                                <div class="col-span-6 sm:col-span-6">
                                    <label for="type" class="block text-sm font-medium text-gray-700">
                                        Type</label>
                                    <select wire:model="animal.breeds_id" name="breed" id="breed"
                                        class="block w-full px-3 py-2 mt-1 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                        <option value="">Choose a type</option>
                                        @foreach ($types as $type)
                                            <option value={{ $type->id }}>{{ $type->animal_name }}</option>
                                        @endforeach
                                    </select>
                                </div>
                                <div class="col-span-6 sm:col-span-6">
                                    <label for="breed" class="block text-sm font-medium text-gray-700">
                                        Breed</label>
                                    <select wire:model="animal.breeds_id" name="breed" id="breed"
                                        class="block w-full px-3 py-2 mt-1 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                        <option value="">Choose a breed</option>
                                        @foreach ($breeds as $breed)
                                            <option value={{ $breed->id }}>{{ $breed->breed_name }}</option>
                                        @endforeach
                                    </select>
                                </div>
                                <div class="col-span-6 sm:col-span-6">
                                    <label for="breed" class="block text-sm font-medium text-gray-700">
                                        Breed</label>
                                    <select wire:model="animal.breeds_id" name="breed" id="breed"
                                        class="block w-full px-3 py-2 mt-1 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                        <option value="">Choose a breed</option>
                                        @foreach ($breeds as $breed)
                                            <option value={{ $breed->id }}>{{ $breed->breed_name }}</option>
                                        @endforeach
                                    </select>
                                </div>
                                <div class="col-span-6 sm:col-span-6">
                                    <label for="gender" class="block text-sm font-medium text-gray-700">
                                        Gender</label>
                                    <select wire:model="animal.genders_id" name="gender" id="gender"
                                        class="block w-full px-3 py-2 mt-1 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                        <option value="">Choose a gender</option>
                                        @foreach ($genders as $gender)
                                            <option value={{ $gender->id }}>{{ $gender->type }}</option>
                                        @endforeach
                                    </select>
                                </div>
                                <div class="col-span-6 sm:col-span-6">
                                    <label for="eye_color" class="block text-sm font-medium text-gray-700">
                                        Eye color</label>
                                    <select wire:model="animal.eye_color_id" name="eye_color" id="eye_color"
                                        class="block w-full px-3 py-2 mt-1 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                        <option value="">Choose an eye color</option>
                                        @foreach ($eyeColors as $eyeColor)
                                            <option value={{ $eyeColor->id }}>{{ $eyeColor->name }}</option>
                                        @endforeach
                                    </select>
                                </div>
                                <div class="col-span-6 sm:col-span-6">
                                    <label for="Breeder" class="block text-sm font-medium text-gray-700">
                                        Breeder</label>
                                    <select wire:model="animal.breeders_id" name="Breeder" id="Breeder"
                                        class="block w-full px-3 py-2 mt-1 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                        <option value="">Choose a breeder</option>
                                        @foreach ($breeders as $breeder)
                                            <option value={{ $breeder->id }}>{{ $breeder->name }}</option>
                                        @endforeach
                                    </select>
                                </div>

                                <div class="col-span-6 sm:col-span-6">
                                    <label for="passport" class="block text-sm font-medium text-gray-700">
                                        Passport URL</label>
                                    <input type="text" wire:model.defer="animal.passport_url" name="passport"
                                        id="passport" autocomplete="text"
                                        class="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                </div>

                                <div class="col-span-6 sm:col-span-6">
                                    <label for="chip_number" class="block text-sm font-medium text-gray-700">
                                        Chip number</label>
                                    <input type="text" wire:model.defer="animal.chip_number" name="chip_number"
                                        id="chip_number" autocomplete="chip_number"
                                        class="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                </div>

                                <div class="col-span-6">
                                    <label for="bio" class="block text-sm font-medium text-gray-700">
                                        Bio
                                    </label>
                                    <div class="mt-1">
                                        <textarea wire:model.defer="animal.bio" id="bio" name="bio" rows="3"
                                            class="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
                                </textarea>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="max-w-md mx-auto overflow-hidden rounded-lg md:max-w-xl">
                            <div class="md:flex">
                                <div class="w-full p-3 ">
                                    <div
                                        class="relative flex items-center justify-center h-48 bg-gray-100 border-2 border-dotted rounded-lg border-primary-light">
                                        <div class="absolute">
                                            <div class="flex flex-col items-center"><i
                                                    class="fa fa-folder-open fa-4x text-primary"></i> <span
                                                    class="block font-normal text-gray-400">Upload your image
                                                    here</span>
                                            </div>
                                        </div>
                                        <input wire:model.defer="image" type="file"
                                            class="w-full h-full opacity-0 cursor-pointer">
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="absolute bottom-0 right-0 px-6 py-3 mx-4 my-6 text-right bg-gray-50 sm:px-6">
                        <button type="submit"
                            class="inline-flex justify-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-primary hover:bg-primary-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                            Save
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </section>
</main>

The form is populated via this livewire class.

class AddAnimal extends Component
{
    use WithFileUploads;

    public User $user;
    public Animal $animal;
    public Collection $genders;
    public Collection $eyeColors;
    public Collection $breeds;
    public Collection $colors;
    public Collection $breeders;
    public Collection $types;
    public $image;

    protected array $rules = [
        'animal.name' => 'required|min:2',
        'animal.eye_color_id' => 'nullable',
        'animal.bio' => 'nullable',
        'animal.breeds_id' => 'nullable',
        'animal.genders_id' => 'nullable',
        'animal.breeders_id' => 'nullable',
        'animal.chip_number' => 'nullable',
        'animal.passport_url' => 'nullable',
        'image' => 'nullable',
    ];

    public function mount(User $user)
    {
        $this->user = $user;
        $this->animal = new Animal();
        $this->genders = Gender::all();
        $this->eyeColors = EyeColor::all();
        $this->breeds = Breed::all();
        $this->colors = Color::all();
        $this->breeders = Breeder::all();
        $this->types = AnimalType::all();
    }

    public function render()
    {
        return view('livewire.add-animal')
            ->layout('components.layouts.dashboard', ['title' => 'Add-animal'])
            ->with(['user' => $this->user, 'genders' => $this->genders, 'eyeColors' => $this->eyeColors, 'breeds' => $this->breeds, 'colors' => $this->colors, 'breeders' => $this->breeders, 'types' => $this->types]);
    }

How can I make it so that my dropdowns become dynamic? e.g if a user selects dog as animal type, in the breed dropdown only relevant dog breeds should be shown, and not cats or horses. I tried using some of the online available tutorials to get me started, but could not figure it out with all the relationships going on in my models.


Solution

  • Since the Breed Model has an animal_type id, we can use Livewire's updated hook to check for changes on the animal type and render only breeds related to the animal type.

    so in the livewire component,

    class AddAnimal extends Component
    {
        public User $user;
        public Animal $animal;
        public Collection $genders;
        public Collection $eyeColors;
    
        // public Collection $breeds; we will use a computed property
    
        public Collection $colors;
        public Collection $breeders;
        public Collection $types;
        public $image;
    
        // newly added variable to keep track of animal type changed
        public $filters = [
            'animal_type_id' => ''
        ];
    
        protected array $rules = [
            'animal.name' => 'required|min:2',
            'animal.eye_color_id' => 'nullable',
            'animal.bio' => 'nullable',
            'animal.breeds_id' => 'nullable',
            'animal.genders_id' => 'nullable',
            'animal.breeders_id' => 'nullable',
            'animal.chip_number' => 'nullable',
            'animal.passport_url' => 'nullable',
            'image' => 'nullable',
            'animal.animal_type_id' => '', // make sure you have the rule can be left empty if its not required
        ];
    
        public function mount(User $user)
        {
            $this->user = $user;
            $this->animal = new Animal();
            $this->genders = Gender::all();
            $this->eyeColors = EyeColor::all();
            $this->colors = Color::all();
            $this->breeders = Breeder::all();
            $this->types = AnimalType::all();
        }
    
    
        public function updatedAnimalAnimalTypeId($value)
        {
            $this->filters['animal_type_id'] = $value;
        }
    
        public function getBreedsProperty()
        {
            return Breed::when($this->filters['animal_type_id'], function($query, $animal_type_id){
                return $query->where('animal_type_id', $animal_type_id);
            })->get();
        }
    
        public function render()
        {
            return view('livewire.add-animal')
                ->layout('components.layouts.dashboard', ['title' => 'Add-animal'])
                ->with(['user' => $this->user, 'genders' => $this->genders, 'eyeColors' => $this->eyeColors, 'breeds' => $this->breeds, 'colors' => $this->colors, 'breeders' => $this->breeders, 'types' => $this->types]);
        }
    
    
    }
    

    Note that I have used computed property to get the breeds. I also have made use of when clause to avoid the null check.

    so in the blade file, we just have to wire:model the animal_type_id.

    ....
    
    <div class="col-span-6 sm:col-span-6">
        <label for="type" class="block text-sm font-medium text-gray-700">
            Type</label>
        <select wire:model="animal.animal_type_id" name="animal_type_id" id="animal_type_id"
            class="block w-full px-3 py-2 mt-1 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
            <option value="">Choose a type</option>
            @foreach ($types as $type)
            <option value={{ $type->id }}>{{ $type->animal_name }}</option>
            @endforeach
        </select>
    </div>
    ....
    
    

    Now the breeds will be rendered based on the animal type selected.

    I have assumed animal_type_id is the correct column name in Animal model. if its not, please change the column name respectively.