How to Mass Update(Edit) in Laravel Query Builder? - laravel

Hello guys I am trying to do a mass update for my application using a checkbox but when I try to update it will just update the 1st student id(I tried to select other students the update does not work). Could you give some insights or concrete examples I can follow?
This is what I have tried so far
public function changeSched()
{
DB::table('class_list')
->whereIn('id', $this->selectedStudents)
->update(['section' => $this->selectedSched]);
}
For my checkbox
#foreach($classlist as $stud)
<tr>
<th class="bg-white py-5 px-5">
<div class="items-center">
<input type="checkbox" wire:model="selectedStudents.{{ $stud->id }}"/>
</div>
</th>

I do not see any error in your update statement so it should update all the records provided by the query (mass updates are documented here).
I guess the error is before, when you set $this->selectedStudents. Are you sure that this variable contains a simple array (not an associative array) with all the selected IDs?
You should do the following:
Debug the $this->selectedStudents variable to ensure its content is correct
Temporarily replace it by a hardcoded value [1, 2, 3] to ensure that the rest of your code is appropriately working
UPDATE
To convert $this->selectedStudents in the right format, you can do the following:
$selectedIds = [];
foreach ($this->selectedStudents as $id => $isSelected){
if ($isSelected) {
$selectedIds[] = $id;
}
}
DB::table('class_list')
->whereIn('id', $selectedIds)
->update(['section' => $this->selectedSched]);

Related

Laravel 8 multi value select filtering

I'm trying to build Laravel project that will have a multi-select dropdown with list of categories. Selecting category should reload the page and apply filter.
"scopeFilter" in my model is actually doing filtering, but here i just need to find a way to properly form URL. The problem is that this code i have:
<form id="cats-form" action="#" method="GET">
<select multiple class="chosen-select" name="test[]">
#foreach($categories->get() as $cat) //this loop goes through existing categories in the system
#php
//if category is part of URL, pre-select it in the select:
$arr = request()->all();
$selected = '';
if(array_key_exists('test', $arr)) {
$testArr = $arr['test'];
$selected = in_array($cat->id, explode(',', $testArr)) ? 'selected' : '';
}
#endphp
<option {{ $selected }} value="{{ $cat->id }}">{{ $cat->name }}</option>
#endforeach
</select>
</form>
<script>
$(".chosen-select").chosen({ })
$('.chosen-select').on('change', function(evt, params) {
$('#cats-form').submit();
});
</script>
Is actually giving me this URL:
http://localhost:11089/?test%5B%5D=19&test%5B%5D=5
While i actually need this:
http://localhost:11089/?test=19,5
I guess it's a trivial problem to solve for someone with better knowledge of Laravel, can you tell me pls what i'm doing wrong here?
This is rather how the language or framework or library reads an URL query string. Assuming that your prefered way of URL string is a valid format (as I usually see the format ?arr[]=val1&arr[]=val2 and never see the second format that you preferred), both query string formats should be acceptable to PHP.
As Yarin stated that the best way PHP reads an array is the first method, you don't have to worry too much because the decoded URL query string is exactly in the first format.

Laravel Livewire wire:click creates infinite loop

I have a Livewire component that's a product filter. The queries all work fine, but sometimes it creates an infinite loop of requests.
You can see that happening in the GIF below which is a capture of the Laravel Debugbar. I'm clicking some filters and then suddenly it goes into this request loop.
I specifically use wire:loading.attr="disabled" on the filters in the view so someone can not select a filter while a request is still processing.
My code and some background:
Livewire Component
use App\Models\Product;
use App\Models\Brand;
use App\Models\Color;
class SearchProducts extends Component
{
public ?array $brand = [];
public ?array $color = [];
protected $queryString = ['brand', 'color'];
public function render()
{
$products = Product::query();
$products = $products->with('brand');
$products = $products->with('colors');
$products = $this->filterBrands($products);
$products = $this->filterColors($products);
$products = $products->paginate(24);
return view('livewire.search-products', [
'all_brands' => Brand::where('status', 'active')->get(),
'all_colors' => Color::where('status', 'active')->get(),
])->extends('app');
}
public function filterBrands($query)
{
$queryFilterBrand = array_filter($this->brand);
return empty($queryFilterBrand) ? $query : $query->whereIn('brand_id', $queryFilterBrand);
}
public function filterColors($query)
{
$queryFilterColor = array_filter($this->color);
return empty($queryFilterColor) ? $query : $query->whereHas('colors', function ($q) use ($queryFilterColor) {
$q->whereIn('color_id', $queryFilterColor);
});
}
}
The reason that I use array_filter is because if I unselect a color value and use a character in the key (wire:model="brand.b{{ $brand->id }}"), instead of removing that from the array Livewire will set that key value to false. So then this false value will be put into the query which will give inaccurate results.
Livewire views and the issue
This works fine:
#foreach($all_brands as $brand)
<input type="checkbox" value="{{ $brand->id }}" id="brand.{{ $brand->id }}" wire:model="brand.{{ $brand->id }}" wire:loading.attr="disabled">
<label class="search-label search-wide-label mb-2" for="brand.{{ $brand->id }}">{{ $brand->title }} <i class="fal fa-times float-right selected-icon"></i></label>
#endforeach
But this creates an infinite loop when I select 2 or more colors after each other, or if I select 1 color and then deselect it. So it seems that issue occurs after the 2nd interaction:
#foreach($all_colors as $color)
<input type="checkbox" value="{{ $color->id }}" id="color.{{ $color->id }}" wire:model="color.{{ $color->id }}" wire:loading.attr="disabled">
<label class="search-label search-wide-label mb-2" for="color.{{ $color->id }}">{{ $color->title }} <i class="fal fa-times float-right selected-icon"></i></label>
#endforeach
This is weird because this blade snippet is exactly the same as for $brands as shown above:
The only thing that different is that the colors relationship is a hasMany vs a belongsTo for brand.
I'm now thinking that this is where the problem is...
The things I've tried and didn't work
Remove the #foreach loop for $all_colors and just put the filters in plain HTML (to check if the issue is related to the loop)
Adding wire:key="brand.{{ $brand->id }}" to the input element
Adding wire:key="brand.{{ $brand->id }}" to a div around the input element
Using wire:model="brand.{{ $brand->id }}" or wire:model="brand.{{ $loop->id }}" as was suggested in the comments (and what I thought solved the problem)
Using wire:model="brand.b{{ $brand->id }}" so there's a unique key name
Removing the array_filter approach (seems unlikely that this is the problem but just to test)
Using buttons instead of checkboxes
Using defer, lazy and/or debounce
Paying an expert to try and fix it...
Console error
Last piece, I get this error in my console only when the infinite loop happens so it's very likely either a cause or effect.
TypeError: null is not an object (evaluating 'directive.value.split')
Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'directive.value.split')
Both in LoadingStates.js which I think is a Livewire Javascript file.
The error there seems to be happening here:
function startLoading(els) {
els.forEach(({ el, directive }) => {
if (directive.modifiers.includes('class')) {
let classes = directive.value.split(' ').filter(Boolean)
Answered on GitHub issue, copied here for others to be able to find.
The problem is a morphdom issue.
The code that is triggering the errors and the loop is the wire:loading on your heading row and products row.
The reason is, when you select two or more colours, there are no results shown. What happens then is you're swapping from showing heading/products/total to showing an empty state.
But morphdom doesn't know by default that it should delete the old divs and add the new one. Instead it is trying to "morph" the old first div into the new one. That means that the wire:loading listeners are still registered when they shouldn't be. Hence why the error and the loop.
It's a simple fix though. You need to add wire keys to the divs defining what they are, so morphdom knows that they have actually changed completely, and to delete the old ones and add new ones.
Have a look at this screenshot below of the diff for what I did to get it working. I added a wire key for all the top level divs inside this file.
It's recommended whenever using conditionals like that to add wire:keys to any elements that are first level inside the conditional, so morphdom knows there has been a change. It's the same problem VueJS has, where keys are required inside loops.
Screenshot of diff
So it turns out that if you're using wire:model in a foreach loop like this, you have to write it like this: wire:model="brand.{{ $brand->id }}". Couldn't find it in the docs so hopefully it helps others here.
Update
The infinite loop is solved by this, but what's happening now is that the array values are set to zero instead of removed from the array once you select a checkbox and then click it again to unselect. So then the whereIn is going to look for brand IDs with value 0 which will not return results.
Update 2
The loop is actually not solved... See original question. Slapping a bounty on this $%$£# because too many hours and coffee were wasted.
A couple issues I see:
You're using the same wire:key for your <div> element and your <input> element. I would keep those unique.
Also, you're using the same name on the input element. This will cause issues with checkboxes as the value passed to the backend has the same name and I believe livewire will attempt to sync based on that. Changing the name field in your input to be unique is likely the answer.

Laravel Livewire key() expects parameter 1 to be array, integer given | nested components | loading a component inside a loop

I've been working with Laravel livewire for a while now, I've a nested components, which is product list for my site and inside that list I've another component for adding product to wishlist. As per documentation stated here , it says
"Similar to VueJs, if you render a component inside a loop, Livewire has no way of keeping track of which one is which. To remedy this, livewire offers a special "key" syntax:"
Like this:
<div>
#foreach ($users as $user)
#livewire('user-profile', $user, key($user->id))
#endforeach
</div>
Here is my code snippets from my project.
<div>
#foreach($products as $product)
<div class="product-box white-bg mb-8" data-dusk="product">
{{-- here im passing product id as param in key(), 'productList' is a static value for a variable of mount(). --}}
#livewire('desktop.wish-list-add', $product, key($product->id), 'productList')
<div class="product-content d-flex justify-content-between align-items-center p-5">
...............
#endforeach
{{ $products->links() }}
</div>
The issue is when I try to pass $product->id as param for key(), it gives error
key() expects parameter 1 to be array, integer given
But the doc clearly shows that we have to pass id as param. Has anyone faced this issue so far?
#livewire('photos.photo-wire', ['photo' => $photo], key($photo->id))
instead of
#livewire('photos.photo-wire', ['photo' => $photo, key($photo->id)])
because that will generate the error:
key() expects parameter 1 to be array, integer given
okay, I found the solution ( however it doesn't make sense to me , but it works :/ )
You have to pass other parameters for mount() like this:
#livewire('desktop.wish-list-add', 'productList', $product->id, key($product->id))
Instead of this:
#livewire('desktop.wish-list-add', $product, key($product->id), 'productList')

displaying an array but it return empty

Good day, i have a laravel 5.8 project and i'm having a little problem
i have this code
public function doctor_details($doctor_id){
$doctor = DB::table('doctors')->select('specification')->where('doctor_id', $doctor_id)->get>first();
$specs_data = explode(',', $doctor_specs);
$specs_array = [];
foreach ($specs_data as $doctor_spec) {
$doctor_spec_result = DB::table('specializations')->where { return explode('speciality_id'',', $doctor_spec)->get();
foreach ($doctor_spec_result as $doctor_spec_res) {
$specs_array[] = $doctor_spec_res->speciality_name;
}
}
return view ('doctor_details', compact('doctor', 'specs_array'));
}
now if i do dd($doctor_spec_result); the result is
as you can see i'm getting an empty array but if i do dd($specs_data); the result is
as you can see there's definitely a data but i can't make it work
this is my blade
<div class="row">
<div class="col-lg-12">
<h3>{{ $doctor->doctor_name }}</h3>
</div>
<div class="col-lg-12">
#foreach( $specs_array as $spec_output )
<p>{!! $spec_output !!}</p>
#endforeach
</div>
</div>
I think you are trying to get an list only containing the values of the specification field for the particular rows queried, so that you can then get the specialty_names. You can use the pluck method on the builder to do this for you. If a search by doctor_id could return more than one result:
$doctor_specs = DB::table('doctors')
->where('doctor_id', $doctor_id)
->pluck('specification')
->transform(function ($item) { return explode(',', $item); })
->flatten();
$specs_array = DB::table('specializations')
// use whereIn to find all the rows by 'specialty_id'
->whereIn('speciality_id', $doctor_specs)
->pluck('specialty_name')
->toArray();
return view ('doctor_details', compact('doctor', 'specs_array'));
Laravel 6.x Docs - Database - Query Builder - Retrieving Results pluck
Laravel 6.x Docs - Collections - Methods - transform
Laravel 6.x Docs - Collections - Methods - flatten
Update:
Though since doctor_id is a key there will only be one, we can remove the collection methods and deal with this simpler:
$doctor_specs = explode(
',',
DB::table('doctors')->where('doctor_id', $doctor_id)
->value('specification')
);
Ideally:
Or if $doctor was retrieved with all columns from the doctors table, including specification:
$doctor_specs = explode(',', $doctor->specification);
You can simplify your code using join
$doctor_spec_result = DB::table('doctors')
->select('specializations.speciality_name')
->join('specializations','specializations.speciality_id','doctors.specification')
->where('doctors.doctor_id', $doctor_id)
->get();
return view ('doctor_details', compact('doctor', 'doctor_spec_result '));
Change in this line.
$doctor_spec_result = DB::table('specializations')->where('speciality_id', $doctor_spec)->get();
you are checking the worng attribute. As you are getting specification in 0 index.
speciality_id to specification

Keeping select values across pages

I have a couple of routes
Route::get('/route_one', 'IndexController#index');
Route::get('/route_two', 'IndexController#index');
They call the same controller function because these pages need the same array of data. This function is as follows
public function index()
{
$user = Auth::user();
if( $user ) {
$fileData = $this->fillArrayWithFileNodes(new DirectoryIterator( public_path(). '/images'));
$currentPath= Route::getFacadeRoot()->current()->uri();
if(!empty($fileData)) {
return view('index', compact('fileData', 'currentPath'));
}
} else {
return view('auth.login');
}
}
Now the index view is pretty straight forward, but it does has this part
#if($currentPath == 'route_one')
#include('layouts.searchbarRouteOne')
#endif
#if($currentPath == 'route_two')
#include('layouts.searchbarRouteTwo')
#endif
So depending on what route is called, a different sidebar is displayed. Now the sidebars essentially contain a load of select inputs like the following
<div class="col-lg-3">
<div class="form-group">
<label>Year</label>
<select id="year" class="form-control">
<option value=""></option>
#foreach($fileData["route_one"] as $year => $products)
<option value="{{ $year }}">{{ $year }}</option>
#endforeach
</select>
</div>
</div>
Both sidebars have different selects. When select options are selected, an ajax call is made to display an image. This all works fine.
This is my problem. I have a link to get to route_one or route_two. As the page refreshes when the link is clicked, the selects are at their default state. What I would like to do somehow is keep the last state of the select inputs. I am not storing this data within a database which may be an issue?
Furthermore, route_two relies on the select options from route_one. So when route_two is selected, I need to pass it route_ones options.
What would be the best way to achieve what I am after?
Thanks
Think what you are trying to accomplish here: remember the old input values.
You could send the form when clicking the link and flash the data in your controller or use JavaScript saving input values to the browser's storage.
Simple example using plain JavaScript
// Get all select-elements
var inputs = document.querySelectorAll('select');
// Loop through them
for (var i = 0; i < inputs.length; i++) {
// Set the old input value
inputs[i].value = localStorage.getItem(inputs[i].name);
// Start listening changes
inputs[i].addEventListener('change', store);
}
// The function to call when the value has changed
function store(event) {
// Set the new value to the browser's storage
localStorage.setItem(event.target.name, event.target.value);
}
In that example your form elements are required to have unique name attributes. Of course it can be switched out using e.g. id attribute.

Resources