Home > Mobile >  How to dynamically add input fields with livewire
How to dynamically add input fields with livewire

Time:09-14

I want add new Inputbox to be created when the user clicks the add button. I tried the following code but it didn't work, And I get the following error:

get_class(): Argument #1 ($object) must be of type object, string given

class Test extends Component
{
    public $tests=[''];

    public function render()
    {

        return view('livewire.test');
    }

    public function mount()
    {
        $this->tests = Test::all();

    }

    public function add()
    {
        $this->tests[] = '';
    }

}

<div>
    <form wire:submit.prevent="submit">
        @foreach($tests as $index=>$test)
            <div>
                <label for="title">
                    <input wire:model="title.{{$index}}" type="text">
                </label>
            </div>
        @endforeach
        <button  type="submit">Save</button>
    </form>
    <button  wire:click="add()">ADD MORE</button>
</div>

CodePudding user response:

There are a few issues to address here, so I will provide some background information and try to be as descriptive as possible.

In your mount function you assign a collection of Test objects to your tests array property:

public function mount()
{
    $this->tests = Test::all(); // <-- returns a collection of Test models
}

This overrides the default value and type you have assigned to tests which is an array:

/*
 * This declares $tests to be an array
 * but this becomes a Collection due to your mount method
 */
public $tests = [''];

Yet in your add function you're adding a string:

public function add()
{
    $this->tests[] = '';
}

So you're getting the error because you're trying to add a string to a variable that is a collection of Test models. What you likey want to do is add a new empty Test model.

public function add()
{
    $this->tests->push(Test::make());
}

However, the above will not work as you're working with an existing database collection and so you'll get the following error:

Queueing collections with multiple model connections is not supported.

Therefore to achieve your goal of adding a new Test we need to approach this differently.

app\Http\Livewire\Test.php

<?php

namespace App\Http\Livewire;

use Illuminate\Support\Collection;
use Livewire\Component;

class Test extends Component
{
    // A collection of your models from the database
    public Collection $tests;

    // A conditional variable that we use to show/hide the add new model inputs
    public bool $isAdding = false;

    // A variable to hold our new empty model during creation
    public \App\Models\Test $toAdd;

    // Validation rules
    // These are required in order for Livewire to successfull bind to model properties using wire:model
    // Add any other validation requirements for your other model properties that you bind to
    protected $rules = [
        'tests.*.title' => ['required'],
        'toAdd.title' => ['required']
    ];

    // Livewires implementation of a constructor
    public function mount()
    {
        // Set some default values for the component
        $this->prepare();
    }

    public function render()
    {
        return view('livewire.test');
    }

    // 
    public function add()
    {
        // Set the show/hide variable to true so that the new inputs section is shown
        $this->isAdding = true;
    }

    // Save the new model data
    public function save()
    {
        // Save the model to the database
        $this->toAdd->save();
        // Clean things up
        $this->prepare();
    }

    // Just a helper method for performing repeated functionality
    public function prepare()
    {
        // Get all the required models from the database and assign our local variable
        $this->tests = \App\Models\Test::all();
        // Assign an empty model to the toAdd variable
        $this->toAdd = \App\Models\Test::make();
        // Set the isAdding show/hide property to false, which will hide the new model inputs
        $this->isAdding = false;
    }
}

resources\views\livewire\test.blade.php

<div>
    <!-- loop over all your test models -->
    @foreach ($tests as $index => $test)
        <!-- the :key attribute is essential in loops so that livewire doesnt run into DOM diffing issues -->
        <div :key="tests_{{ $index }}">
            <label for="tests_{{ $index }}_title">Title {{ $index }}
                <input type="text" id="tests_{{ $index }}_title" name="tests_{{ $index }}_title"
                    wire:model="tests.{{ $index }}.title" :key="tests_{{ $index }}_title" />
            </label>
        </div>
    @endforeach

    <!-- Only show the new model inputs if isAdding is true -->
    @if ($isAdding)
        <!-- this doesnt need a :key as its not in a loop -->
        <div>
            <label for="title">Title
                <input type="text" id="title" name="title" wire:model="toAdd.title" />
            </label>
            <!-- triggers the save function on the component -->
            <button type="button" wire:click="save">Save</button>
        </div>
    @endif

    <!-- triggers the add function on the component -->
    <button type="button" wire:click="add">Add More</button>
</div>

Hopefully, with the comments I have added the above makes sense.

Update

Is it possible to create several input boxes and finally save all the data at once?

That is entirely possible yes. Due to some limitations with Livewire, specifically it not knowing how to hydrate Eloquent Models in a Collection correctly, we need to use a little wizardry to make it work.

app\Http\Livewire\Test.php

<?php

namespace App\Http\Livewire;

use Illuminate\Support\Collection;
use Livewire\Component;

class Test extends Component
{
    // A collection of your models from the database
    public Collection $tests;

    // A conditional variable that we use to show/hide the add new model inputs
    public bool $isAdding = false;

    // A variable to hold our new empty models during creation
    public Collection $toAdd;

    // Validation rules
    // These are required in order for Livewire to successfull bind to model properties using wire:model
    // Add any other validation requirements for your other model properties that you bind to
    protected $rules = [
        'tests.*.title' => ['required'],
        'toAdd.*.title' => ['required']
    ];

    // Livewires implementation of a constructor
    public function mount()
    {
        $this->cleanUp(true);
    }

    public function hydrate()
    {
        // Get our stored Test model data from the session
        // The reason for this is explained further down
        $this->toAdd = session()->get('toAdd', collect());
    }

    public function render()
    {
        return view('livewire.test');
    }

    public function add()
    {
        $this->isAdding = true;

        // Push (add) a new empty Test model to the collection
        $this->toAdd->push(\App\Models\Test::make());

        // Save the value of the toAdd collection to a session
        // This is required because Livewire doesnt understand how to hydrate an empty model
        // So simply adding a model results in the previous elements being converted to an array
        session()->put('toAdd', $this->toAdd);
    }

    public function save($key)
    {
        // Save the model to the database
        $this->toAdd->first(function ($item, $k) use ($key) {
            return $key == $k;
        })->save();

        // Remove it from the toAdd collection so that it is removed from the Add More list
        $this->toAdd->forget($key);

        // Clean things up
        $this->cleanUp(!$this->toAdd->count());
    }

    public function saveAll()
    {
        $this->toAdd->each(function ($item) {
            $item->save();
        });

        $this->cleanUp(true);
    }

    // Just a helper method for performing repeated functionality
    public function cleanUp(bool $reset = false)
    {
        // Get all the required models from the database and assign our local variable
        $this->tests = \App\Models\Test::all();

        // If there are no items in the toAdd collection, do some cleanup
        // This will reset things on page refresh, although you might not want that to happen
        // If not, consider what you want to happen and change accordingly
        if ($reset) {
            $this->toAdd = collect();
            $this->isAdding = false;
            session()->forget('toAdd');
        }
    }
}

resources\views\livewire\test.blade.php

<div>
    <!-- loop over all your test models -->
    @foreach ($tests as $index => $test)
        <!-- the :key attribute is essential in loops so that livewire doesnt run into DOM diffing issues -->
        <div :key="tests_{{ $index }}">
            <label for="tests_{{ $index }}_title">Title {{ $index }}
                <input type="text" id="tests_{{ $index }}_title" name="tests_{{ $index }}_title"
                    wire:model="tests.{{ $index }}.title" :key="tests_{{ $index }}_title" />
            </label>
        </div>
    @endforeach
    <!-- Only show the new model inputs if isAdding is true -->
    @if ($isAdding)
        <div>
            @foreach ($toAdd as $index => $value)
                <div :key="toAdd_{{ $index }}">
                    <label for="toAdd_{{ $index }}_title">New Title {{ $index }}
                        <input type="text" id="toAdd_{{ $index }}_title"
                            name="toAdd_{{ $index }}_title" wire:model="toAdd.{{ $index }}.title"
                            :key="toAdd_{{ $index }}_title" />
                    </label>
                    <!-- triggers the save function on the component -->
                    <button type="button" wire:click="save({{ $index }})" :key="toAdd_{{ $index }}_save">Save</button>
                </div>
            @endforeach
            <button type="button" wire:click="saveAll">Save All</button>
        </div>
    @endif
    <!-- triggers the add function on the component -->
    <button type="button" wire:click="add">Add More</button>
</div>

I've not removed some of the previous comments and not commented on the code that should be pretty obvious what it is doing.

I did rename the prepare function to cleanUp and it seemed more appropriate.

I also provided functions for saving an individual Test model, or all at the same time. You might want to be able to do one or the other or both at some point so seemed useful.

  • Related