Is it possible to share state between multiple form components?

Hey, I have multiple form components that all form one big form in the end, e.g. text inputs, some dropdowns and a image upload. when I fill in my text inputs and hit the save button, the text inputs will be uploaded and the model will be saved to the database.

But if I fill in the dropdowns and then hit save, a new animal model will be added instead of updating the newly created one.

I know why it’s not working (I think), every component has a new animal being created in it’s mount function like so.

    public function mount()
    {
        $this->animal = new Animal();
    }

This means that every component has a different animal, instead of a shared animal which they will update.

Is it possible to somehow create a shared animal model that can be used by my components?

This is the main page, where all my components are called

<x-slot name="title">
    Add animal
</x-slot>

<div>
    <section>
        <div
            class="py-10 mx-auto max-w-7xl sm:px-6 lg:px-8">
            <div class="mt-10 sm:mt-0">
                @livewire('animal.general-information-form')

                <x-jet-section-border/>

            </div>
            <div class="mt-10 sm:mt-0">
                @livewire('animal.selected-breed-form')

                <x-jet-section-border/>
            </div>

            <div class="mt-10 sm:mt-0">
                @livewire('animal.details-form')

                <x-jet-section-border/>
            </div>

            <div class="mt-10 sm:mt-0">
                @livewire('utilities.image-uploader')

                <x-jet-section-border/>
            </div>
        </div>
    </section>
</div>

In e.g. the general-information-form I have this

<x-jet-form-section submit="updateAnimalProfile">
    <x-slot name="title">
        {{ __('Profile Information') }}
    </x-slot>

    <x-slot name="description">
        {{ __('Lets start with some basic information about your animal.') }}
    </x-slot>

    <x-slot name="form">
        <div class="col-span-6 sm:col-span-4">
            <x-jet-label for="name" value="{{ __('Name') }}"/>
            (required)
            <x-jet-input id="name" type="text" class="block w-full mt-1" wire:model.defer="animal.name"
                         autocomplete="name"/>
            <x-jet-input-error for="name" class="mt-2"/>
        </div>
        <div class="col-span-6 sm:col-span-4">
            <x-jet-label for="chip_number" value="{{ __('Chip number') }} "/>
            (optional)
            <x-jet-input id="chip_number" type="text" class="block w-full mt-1" wire:model.defer="animal.chip_number"
                         autocomplete="chip_number"/>
            <x-jet-input-error for="chip_number" class="mt-2"/>
        </div>
        <div class="col-span-6 sm:col-span-4">
            <x-jet-label for="passport_url" value="{{ __('Passport url') }}"/>
            (optional)
            <x-jet-input id="passport_url" type="text" class="block w-full mt-1" wire:model.defer="animal.passport_url"
                         autocomplete="passport_url"/>
            <x-jet-input-error for="passport_url" class="mt-2"/>
        </div>
        <div class="col-span-6 sm:col-span-4">
            <x-jet-label for="birth_date" value="{{ __('Birth date') }}"/>
            (optional)
            <x-jet-input id="birth_date" type="date" class="block w-full mt-1" wire:model.defer="animal.birth_date"/>
            <x-jet-input-error for="birth_date" class="mt-2"/>
        </div>
        <div class="col-span-6 sm:col-span-4">
            <label for="bio">{{__('Bio')}} (optional)</label>
            <textarea
                class="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                wire:model.defer="animal.bio" name="bio" id="bio" cols="30" rows="10">
            </textarea>
        </div>


    </x-slot>

    <x-slot name="actions">
        <x-jet-action-message class="mr-3" on="saved">
            {{ __('Saved.') }}
        </x-jet-action-message>

        <x-jet-button wire:loading.attr="disabled" wire:target="photo">
            {{ __('Save') }}
        </x-jet-button>
    </x-slot>
</x-jet-form-section>

And the backend side

<?php

namespace App\Http\Livewire\Animal;

use App\Actions\Utilities\createNewAnimal;
use App\Models\Animal;
use Livewire\Component;

class GeneralInformationForm extends Component
{
    /**
     * The animal to be created.
     *
     * @var Animal
     */
    public Animal $animal;


    protected array $rules = [
        'animal.name' => ['nullable', 'string', 'max:12'],
        'animal.chip_number' => ['nullable', 'string', 'max:28'],
        'animal.passport_url' => ['nullable', 'string', 'max:28'],
        'animal.birth_date' => ['nullable', 'date'],
        'animal.bio' => ['nullable', 'string', 'max:256'],
    ];

    public function mount()
    {
        $this->animal = new Animal();
    }

    public function render()
    {
        return view('livewire.animal.general-information-form');
    }

    public function updateAnimalProfile(createNewAnimal $updater)
    {
        $this->validate();

        $animalArray = $this->animal->toArray();

        $updater->create(input: $animalArray);
    }
}

The rest of the components are almost identical.

You could share state between your Parent component and its Children.

Provide your Parent component with an Animal:

@livewire('parent-form', ['animal' => App\Models\Animal::find(1)])

app/Http/Livewire/ParentForm.php

This is your parent/wrapper component.

<?php

namespace App\Http\Livewire;

use App\Models\Animal;
use Livewire\Component;

class ParentForm extends Component
{
    /**
     * The mode you're working with
     */
    public Animal $animal;

    /**
     * Holds updates to values of the model you are working with
     * this is not required working with this 'dirty' state allows you to 
     * easily reset the form
     */
    public array $state = [];

    /**
     * Listen for events being sent from your child components
     *
     * @var array
     */
    protected $listeners = [
        'mergeSate' // listen for an event that says a child components state has changed
    ];

    /**
     * Merge the state of the data from the child component into this components state array
     *
     * @param $state
     * @return void
     */
    public function mergeSate($state)
    {
        $this->state = array_merge($this->state, $state);
    }

    /**
     * Update your model with the values from the state array
     *
     * @return void
     */
    public function save()
    {
        $this->animal->update($this->state);
    }

    public function mount(Animal $animal)
    {
        // prefile the state array 
        $this->state = $animal->toArray();
    }

    public function render()
    {
        return view('livewire.parent-form');
    }
}

resources/views/livewire/parent-form.blade.php

Then when you are rendering your child/nested components, you pass through the $state to each child.

<div class="max-w-7xl mx-auto mt-12 space-y-6">
    <pre class="text-xs">@json($animal)</pre>

    @livewire('child-form', ['state' => $state])

    <button wire:click="save" class="px-4 py-2 bg-gray-800 rounded text-sm text-white">Save</button>
</div>

app/Http/Livewire/ChildForm.php

This is your child/nested component.

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class ChildForm extends Component
{
    public array $state = [];

    /**
     * Define validation rules
     *
     * @var array
     */
    protected $rules = [
        'state.name' => 'required'
    ];

    /**
     * Override the default error messages 
     *
     * @var array
     */
    protected $messages = [
        'state.name.required' => 'The name is required'
    ];

    /**
     * When the value of the local state array changes
     * Validate the data and emit an event on success
     *
     * @return void
     */
    public function updatedState()
    {
        // validate the local state
        $this->validate();

        // data is valid, fire an event to have the state merged into the parents state
        $this->emitUp('mergeSate', $this->state);
    }

    public function render()
    {
        return view('livewire.child-form');
    }
}

resources/views/livewire/child-form.blade.php

<div>
    <input type="text" wire:model="state.name">
    @error('state.name')
        {{ $message }}
    @enderror
</div>

Clicking the save button, generates the error Typed property App\Http\Livewire\Animal\AnimalAdd::$animal must not be accessed before initialization

I see you mention a parent-form, but in my case the parent form is where all the sub-forms are initialized

This is the main-form

<x-slot name="title">
    Add animal
</x-slot>

<div>
    <section>
        <div
            class="py-10 mx-auto max-w-7xl sm:px-6 lg:px-8">
            <pre class="text-xs">@json($animal,JSON_THROW_ON_ERROR)</pre>
            <div class="mt-10 sm:mt-0">
                @livewire('animal.general-information-form',['state' => $state])

                <x-jet-section-border/>

            </div>
            <div class="mt-10 sm:mt-0">
                @livewire('animal.selected-breed-form',['state' => $state])

                <x-jet-section-border/>
            </div>

            <div class="mt-10 sm:mt-0">
                @livewire('animal.details-form',['state' => $state])

                <x-jet-section-border/>
            </div>

            <div class="mt-10 sm:mt-0">
                @livewire('utilities.image-uploader',['state' => $state])

                <x-jet-section-border/>
            </div>
            <div class="flex items-end justify-end">
                <x-jet-action-message class="mr-3" on="saved">
                    {{ __('Saved.') }}
                </x-jet-action-message>

                <x-jet-button wire:click="save" wire:loading.attr="disabled">
                    {{ __('Save') }}
                </x-jet-button>
            </div>
        </div>
    </section>
</div>

Which has this as backend

<?php

namespace App\Http\Livewire\Animal;

use App\Models\Animal;
use Livewire\Component;

class AnimalAdd extends Component

{
    public Animal $animal;
    public array $state = [];

    protected $listeners = ['mergeState'];

    public function mergeState($state)
    {
        $this->state = array_merge($this->state, $state);
    }

    public function save()
    {
        $this->animal->update($this->state);
    }

    public function mount(Animal $animal)
    {
        $this->state = $animal->toArray();
    }

    public function render()
    {
        return view('livewire.animal.animal-add');
    }
}

Also another thing, I see you are passing a animal in the parent-form Animal::find(1) but if there is no animal already in the database this would fail no?

Edit :
I changed the mount function in the parent form from

    public function mount(Animal $animal)
    {
        $this->animal = new Animal();
        $this->state = $animal->toArray();
    }

to this

    public function mount()
    {
        $this->animal = new Animal();
    }

Filing in all the inputs seems to work fine, data gets added to the components local state, then send to the parent form where it’s get merged. but hitting the save button gives no errors but uploads nothing to the database.

Edit 2:
this was a bit of an obvious one, I changed the

  public function save()
    {
        $this->animal->update($this->state);
    }

to

  public function save()
    {
        $this->animal->save($this->state);
    }

A new animal is being saved but all of it’s values are NULL, dumping $this->state in the function does confirm that data is inside of the state array. I have no idea why it’s uploading as NULL tho?

I see you mention a parent-form , but in my case the parent form is where all the sub-forms are initialized

Yes, your AnimalAdd component is the same as my ParentForm.

Also another thing, I see you are passing a animal in the parent-form Animal::find(1) but if there is no animal already in the database this would fail no?

Correct, if there’s not an animal in the database where id equals 1 it would fail. It’s just intended as an example of how to pass an Animal to the component.

You could do this, but what if you want to edit an existing Animal?

In your AnimalAdd component, have the following mount function:

public function mount(Animal $animal = null)
{
    if (is_null($animal->id)) {
        $this->animal = new Animal();
    }
    
    $this->state = $animal->toArray();
}

That checks to see if you’re passing an Animal to the component, if not it creates a new empty Animal.

Then change the save function to:

public function save()
{
    $this->animal = $this->animal->updateOrCreate($this->state);
}

That will either update the Animal if it exists, or create a new one.

You can then reuse that component to either add or update an Animal rather than creating another component for updating.

1 Like

This fixed all of the issues I had, Thank you !