How to dynamically add input fields with livewire - laravel

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>

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

Livewire component not updating

I have a simple component:
<input type="text" wire:model="search">
{{ $search }}
I load this like this:
#livewire('searchbar')
And this is my class:
class Searchbar extends Component
{
public $search;
public $results = [];
public function updatedSearch()
{
info($this->search);
$this->results = Client::search($this->search)->get();
}
public function render()
{
return view('livewire.searchbar', [
'results' => $this->results,
]);
}
}
When I type into the input field, the info() logs the correct value but in the view, the the {{ $search }} gets not updated. When I use protected $queryString = ['search']; the URL get's updated correctly as well and if I would use dd() instead of info() I'd see the view updating with the dd.
Why is the $search not updating in the view?
Wrap your component. Make sure your Blade view only has ONE root element.
<div>
<input type="text" wire:model="search">
{{ $search }}
</div>
First thing first, inside livewire render, you do not need to send variables.
This will be sufficient.
public function render()
{
return view('livewire.searchbar');
}
You already declared $results as public variable, just like $search.
Livewire will know when the content of those variables are updated.
And now please try again.
$search should be automatically updated based on any text you inserted in input text with wire:model attribute.

nouislider with livewire how to get values

How can i get the values from nouislider in livewire? So i can compare the min and max price. I don't see how i can get the values. It has 2 handles a min and max price.
<div wire:ignore id="slider-margin"></div>
<div class="d-flex">
<div wire:model="min_price" class="me-5" id="slider-margin-value-min" aria-valuenow="min_price"></div>
<div wire:model="max_price" id="slider-margin-value-max" aria-valuemax="max_price"></div>
</div>
class Shop extends Component
{
use WithPagination;
protected $paginationTheme = 'bootstrap';
public $min_price;
public $max_price;
public function render()
{
$this->products = Product::query()
->wherebetween('price',[$this->min_price,$this->max_price])->paginate(9);
return view('livewire.shop',[
'products'=>$this->products
]);
}
}
try this code:
<div wire:ignore id="slider-margin"></div>
<div class="d-flex">
<div wire:click="setMin({{ $min_price }})" class="me-5" id="slider-margin-value-min" aria-valuenow="min_price"></div>
<div wire:click="setMax({{ $max_price }})" id="slider-margin-value-max" aria-valuemax="max_price"></div>
</div>
and:
class Shop extends Component
{
use WithPagination;
protected $paginationTheme = 'bootstrap';
public $min_price;
public $max_price;
public function setMax($max_price)
{
$this->max_price= $max_price;
}
public function setMin($min_price)
{
$this->min_price = $min_price;
}
public function mount()
{
$this->products = Product::query()
->wherebetween('price',[$this->min_price,$this->max_price])->paginate(9);
}
}
A div is no input, so a wire:model won't do anything on it. noUiSlider builds HTML after page load, so to be able to use it for variables in Livewire, you will have to use JavaScript. You can listen for the events and use the global Livewire JS object to emit changes to your component
So, an example of how this might work:
let slider = document.getElementById('slider-margin');
slider.noUiSlider.on('update', function (values) {
Livewire.emitTo('shop', 'sliderUpdated', values);
});
Then in your component, you can do something with the passed values:
function sliderUpdated(array $values)
{
$this->min_price = \Illuminate\Support\Arr::first($values);
}
Ensure to take a good read on the event docs, as I am not 100% sure above code will function straight away, but I think this will at least get you in the right direction!

Undefined variable in laravel 8 - Trying to fetch <select> list from DataBase

I am trying to fetch a list from the database into a <select> for the form.
I have a controller with classes (index, getLocations, create, store, edit, update, destroy)
public function getLocations($locationList)
{
$locationList = Locations::select('id', 'locationName')->get();
return view('pages.dataEntry.reports.reports', compact('locationList'));
}
getLocation has its own Model:
class Locations extends Model
{
use HasFactory;
protected $table = 'kaec_locations';
}
and I get undefined variable $locationList is undefined
<select class="form-select" name="caseLocation">
<option value="none" selected disabled>Select Location</option>
#foreach ($locationList as $item)
<option value="{{ $item->id }}"> {{ $item->locationName }} </option>
#endforeach
</select>
Is this a good practice? to have a separate Model for locations?
How come the variable is undefined? Should it be included in the route?
Route::controller(ReportController::class)->group(function () {
Route::get('reports', 'getLocations');
Route::get('reports', 'index')->name('reports.index'); // Index page (DataTable)
Route::get('reports/create', 'create')->name('reports.create'); // The form for adding new records
Route::post('reports/create', 'store')->name('reports.store'); // Add new to DB
Route::get('reports/edit/{report}', 'edit')->name('reports.edit'); // The form for editing records
Route::put('reports/edit/{report}', 'update')->name('reports.update'); // Update record to DB
Route::get('reports/{report}', 'destroy')->name('reports.destroy'); // Delete from DB
});
Got it. I do not need to make a function for this. I already have function for adding new data create(). The Create refers to the blade file of the form I want to display that <select>
So I just removed the getLocations() and added the code to create()
/**
** Add new Record Page.
**/
public function create()
{
$locationList = Locations::all();
return view('pages.dataEntry.reports._addForm', compact('locationList'));
}

Laravel Livewire - How to force child component refresh

I'd like to refresh the child component when the parent is updated and only the child component of this component should update, because I'd like to reuse the child component somewhere else in the page. The components structure is the following.
UserShow.php
class UserShow extends Component
{
public User $user;
public int $userId;
public function mount() {
$this->user = User::first();
}
public function updatedUserId() {
$this->user = User::find($this->userId);
}
public function render() {
return view('livewire.user-show');
}
}
<div>
<select wire:model="userId">
<option value="1">Matteo</option>
<option value="2">Giulia</option>
<option value="3">Mattia</option>
</select>
<div>
Name: {{ $user->name }}
#livewire('user-show-email', ['user' => $user])
</div>
</div>
UserShowEmail.php
class UserShowEmail extends Component
{
/** #var User */
public $user;
public function render() {
return view('livewire.user-show-email');
}
}
<div>
Email: {{ $user->email }}
</div>
When the user in UserShow is changed via updatedUserId() method, I'd like that $user prop in UserShowEmail will change accordingly. By reading the docs it seems that Livewire is not able to automatically refresh the child component.
I wonder if there's a workaround, or something that allow me to refresh the child component when parent changed.
Thanks!
If you don't mind updating the whole child component, this would be one way of doing it:
There's a trick that always work for me when I want to make a child
Livewire component "reactive". In your parent livewire component you
use a timestamp for the key attribute:
<livewire:child-component key="{{ now() }}" :my-prop="$myModel" />
The tricky part is the now() function in key prop. This component now
will always be updated when the parent is updated.
In your parent component:
<div>
...
<div>
Name: {{ $user->name }}
<livewire:user-show-email key="{{ now() }}" :user="$user" />
</div>
</div>
You can read more about it here and here.

how do i pass data value to another page via link in laravel?

i am trying to make a list of locations that you can rent. but to rent the place you need to fill in some information. to fill in this information you excess another page. how do i make it so laravel knows the page belongs to a certain location
this is what ive done now but i keep getting the error:
Call to undefined method App\Reservation::location()
as soon as i have filled in the fields of information
this is the blade file that links to the the create reservation file
#foreach
($locations as $location => $data)
<tr>
<th>{{$data->id}}</th>
<th>{{$data->name}}</th>
<th>{{$data->type}}</th>
<th><a class="btn" href="{{route('Reservation.index', $data->id)}}">rent</a></th>
</tr>
#endforeach
this is the create reservations blade
<form action="{{ route('location.store') }}" method="post">
#csrf
<label>title</label>
<input type="text" class="form-control" name="name"/>
<label>type</label>
<select>
<option value="0">klein</option>
<option value="1">groot</option>
</select>
<button type="submit" class="btn">inschrijven</button>
</form>
this is what the location controller looks like
public function store(Request $request)
{
$location = new Reservation;
$location->name = $request->get('name');
$location->type = $request->get('type');
$location->location()->associate($request->location());
$location->save();
return redirect('/location');
}
and the relationships in my models should also work
class Reservation extends Model
{
public function locations()
{
return $this->belongsTo('Location::class');
}
}
class Location extends Model
{
public function reservations()
{
return $this->hasMany('Registration::class');
}
}
ive been stuck at this all day and i really dont know where to look anymore
The error you are getting is because of the wrong function name, you are calling location, while it is locations.
public function locations(){}
&
$location->location()->associate($request->location());
and you can pass the variable as a query parameter, you'll need to pass this data as an array in your blade file.
Web.php
Route::get('/somewhere/{id?}, function(){
//do something
})->name('test');
Blade
route('test', ['id' => $id]);
Controller Method
public function store(Request $request, $id) //Adding the query parameter for id passed in Route.
{
$location = new Reservation;
$location->name = $request->get('name');
$location->type = $request->get('type');
$location->location()->associate($id);
$location->save();
return redirect('/location');
}

Resources