Laravel Livewire wire:click creates infinite loop - laravel

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.

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.

Livewire ignore does not receive correct data

I'm using selectize.js for dropdown styling with livewire. I'm using livewire for a data table with sortable columns and pagination. The issue is, every time I make pagination or a sort by column, the javascript goes missing thus, there's no styling for the dropdown. I've solved the styling issue using wire:ignore. Now the new problem that I have is that the data passed to the dropdown is not accurate.
#foreach($applications as $application)
<p>{{$application->status}}</p>
<div wire:ignore>
<select class="selectize" name="status" data-width="200px">
#foreach(['Pending',
'Hired',
'Under consideration'] as $status)
<option
#if($application->status === $status) selected #endif>{{ $status }}</option>
#endforeach
</select>
</div>
#endforeach
Inside the <p>{{$application->status}}</p> tag, I get the status 'Pending' but on the dropdown, it shows 'Hired'. The correct status is 'Pending'.
(from comment) #stalwart1014 for example, when I use "select2" I do this
in content section
<select id="select2" wire:model="some">
//.....
</select>
in script section
$(document).ready(function() {
window.initSelectDrop=()=>{
$('#select2').select2({
placeholder: '{{ __('Select') }}',
allowClear: true});
}
initSelectDrop();
window.livewire.on('select2',()=>{
initSelectDrop();
});
});
and in component
public function hydrate()
{
$this->emit('select2');
}
This not always function properly with JS element...but hope this can help you. Greetings
it's maybe a bit late, but worth to mention for the future, I will do not point directly to your issue but explain the workaround of wire:ignore, the wire:ignore directive ignores the updates or changes for the further requests and updates, this attributes is defined for playing with JS third party library, the wire:ignore directive prevent all it's nested elements from updating if you wish to except Childs from updating you can use wire:ignore.self directive

Laravel input checkbox if selected on edit

I have an array that I am setting up as checkboxes
<?php $buslist = array('Brooklyn','Lakewood'); ?>
#foreach ($buslist as $buses)
{{Form::label('brooklyn_1',$buses)}}
{{Form::checkbox('BusList2[]', $buses,false, ['id'=> $buses]) }}
#endforeach
Then I switch it to an string with a , using implode
But when I try to edit my information, none of the checkboxes are selected and the information gets lost on update.
What code can i put into the blade checkboxes, if that bus is in my string list?
Checkboxes are finicky creatures. In your form you actually have to do something like this
{!! Form::hidden('new-group-user-member', 0) !!}
{!! Form::checkbox('new-group-user-member', true, NULL) !!} Member
When a checkbox is not checked nothing gets sent to the server so you never know when to change the value in the backend. If you add a hidden form field with the same name and a value of 0 it will get sent with the form even though the checkbox is not checked.
In third parameter use in_array($buses, $buslist) instead of false. Your blade code will look like this:
{{Form::checkbox('BusList2[]', $buses,in_array($buses, $buslist), ['id'=> $buses]) }}

Laravel 4 - Showing edit form with OLD data input as well as DB information

Im making a edit form for my app and i was wondering if someone could tell me how to get the data from the database into my text field.
I can locate the record i need to edit based on the users click, and i can display the information if i do the following:
value="{{ $letter->subject }}"
BUT, the problem im having is that when i run it through the validation and there is an error, it comes back with the database information instead of the OLD data.
So my questions is. Is there a way to serve up the database information first and then when it goes through the validatior, validate the information the user has edited?
Currently to validate the text field and bring the data back incase of error, im using
Input::old('subject')
Is there a parameter for that old bit that allows me to put in the DB data?
Cheers,
Hey you could validate and return ->withInput() and then in your actual form, check if there is Input::old() and display it, otherwise display from the db.
example:
<input type="text" name="subject"
value="{{ (Input::old('subject')) ? Input::old('subject') : $letter->subject }}">
Or you could go the other way and define the variable and do a regular if statement, instead of the ternary one! Up to you to decide what you want to use!
All you need is form model binding http://laravel.com/docs/html#form-model-binding:
{{ Form::model($letter, ['route' => ['letters.update', $letter->id], 'method' => 'put']) }}
// your fields like:
{{ Form::text('someName', null, ['class' => 'someHTMLclass' ...]) }}
// no default values like Input::old or $letter->something!
{{ Form::close() }}
This way you form will be populated by the $letter data (passed from the controller for example).
Now, if you have on your countroller:
// in case of invalid data
return Redirect::back()->withInput();
then on the redirect your form will be repopulated with input values first, not the original model data.
Make it more simple and clean
<input type="text" name="subject" value="{{ (Input::old('subject')) ?: $letter->subject }}">
I'm not sure for Laravel 4 but in Laravel 5, function old takes second param, default value if no old data in session.
Please check this answer Best practice to show old value

Laravel 4 - Update Div Using Ajax

I'm using Laravel 4 and am trying to update a (#articles) div with the new articles that are retrieved from an ajax request. When I inspect the page and view the Network section, I can see the POST requests being fired off and it's not showing any errors (eg, articles appear to be returned). However, unfortunately, the #articles div is not being updated with the new information. Yet, if I do a browser refresh, the new articles are displayed.
Routes.php
Route::any("/dashboard/latest_sa", [
"as" => "dashboard/latest_sa",
"uses" => "DashboardController#latest_sa"
]);
controllers/DashboardController.php
Class DashboardController extends \BaseController
{
...
protected function latest_sa()
{
if( Request::ajax() )
{
// called via ajax
$articles = Articles::orderBy('published_at', 'desc')->paginate(20);
return json_decode($articles);
}
else
{
// fresh page load
$articles = Articles::orderBy('published_at', 'desc')->paginate(20);
return $articles;
}
}
...
}
app/views/dashboard/default.blade.php
...
#section("content")
// defined in /public/js/main.js
<script type="text/javascript">
callServer();
</script>
<div class="col-xs-4 col-sm-4 col-md-4 col-lg-4">
<h4>Latest Articles</h4>
<div class="articles">
<ul>
#foreach ($articles as $article)
<li>
<img src="{{ $article->user_image }}" alt="{{ $article->article_title }}" />
{{ $article->article_title }}
<div class="details">
<span class="author">{{ $article->author_name }}</span>
<span class="created">{{ Helpers::time_ago($article->published_at) }}</span>
<span class="symbol">{{ $article->symbol_title }}</span>
</div>
</li>
#endforeach
</ul>
</div>
{{ $articles->links() }}
</div>
...
/public/js/main.js
function callServer()
{
setInterval(function(){
$.ajax({
type: "POST",
url: "dashboard/latest_sa",
success:function(articles)
{
$(".articles").html(articles);
}
});
},5000);
}
JS is hardly my strong suit, so I'm not sure what I'm doing wrong here.
And, for clarity sake, the reason why I'm trying to update all of the articles in the div is so that the Helpers::time_ago method also gets called, instead of just fetching the new articles. This way, it properly shows how long ago the article was published (eg, less than a minute ago, a minute ago, a hour ago, a day ago, etc) without refreshing the page. Essentially, I'm trying to kill two birds with one stone; update the div with the most recent articles, and update the remaining article's published_at attribute using my Helpers::time_ago method. If there is a more effective / efficient way of doing this, feel free to correct me. This seems rather crude, but since it's only for personal use and will never be used for commercial purposes, it suits my needs (not that that excuses bad code).
Nonetheless, from my fairly basic understanding, the JS should be doing the following steps:
1) Fire a POST request off to the /dashboard/latest_sa route
2) Execute the DashboardController#latest_sa action
3) Return a DB collection of all $articles ordered by the latest published date, and paginated
4) Pass the $articles collection back to the JS success attribute (as articles)
5) Fire the anonymous function, with the articles collection as an argument
6) Update the corresponding inner HTML with the results from the articles collection
The logic sounds right, so I'm pretty sure this is going to be a human error (98% of the time it is, after all. lol). Hopefully, someone here will be able to see the (probably glaring) problem in the logic and point me in the right direction.
In the meantime, I'm going to keep toying around with it.
I look forward to your thoughts, ideas, and suggestions. TIA.
EDIT:
Well, I found one of the problems; the articles div is a class, and in the JS I'm referring to it as an id. I fixed that, and now after the timeInterval, the article's div is "updated" but no results are being displayed (none, zippo, nadda).
Yet, if I directly access the /dashboard/latest_sa URI I get the valid JSON response that I'm expecting. So, albeit I am closer, I am still missing something.
EDIT 2:
Okay, in the controller, I made some changes which can be seen above, where I am now doing a json_decode on the $articles, before returning them to be passed into the view. With that in place, the articles are showing back up again after the timeInterval has elapsed, however, the new articles and the published_at for the existing articles are not being updated. After reviewing Inspect -> Network, it shows that the server is responding with a 500 Internal Server Error from the ajax POST request.
Hrm... Seems like I'm going in circles. Sounds like a good time to take a break and go for a walk. ;)
EDIT 3:
Well, I modified my Helpers class and added in the following method to check if the $article is a json object.
public static function isJson($string)
{
json_decode($string);
return (json_last_error() == JSON_ERROR_NONE);
}
app/views/dashboard/index.blade.php
#foreach ($articles as $article)
<?php
if( Helpers::isJson($article) )
{
$article = json_decode($article);
// dd($article) // when uncommented it returns a valid PHP object
}
?>
<!-- Iterate over the article object and output the data as shown above... -->
#endforeach
As you can see, (for the time being) inside of my view's foreach($articles as $article), I run Helpers::isJson($article) as a test and decode the object if it is json. This has enabled me to get passed the 500 Internal Server Error message, populate the articles div with the results on the initial load, and after the ajax POST request is fired off, I'm getting back a server response of 200 OK according to Inspect -> Network. However, after it updates the div, it doesn't show any articles.
Around, and around I go... I think it's time I take that break I keep murmuring about. ;)
Any thoughts, suggestions and / or ideas are greatly welcomed and appreciated.
At first, you should know that, when you return a collection from the controller/route, the response automatically turns in to a json response so, you don't need to use json_decode() and it won't work, instead, you may try something like this (from your controller for ajax):
$articles = Articles::orderBy('published_at', 'desc')->paginate(20);
return View::make('defaultAjax')->with('articles', $articles);
Since building the HTML in the client side using the json data received from server side would be tough for you so, you may return HTML from the server with the generated view instead of json, so you may try something like this in your success handler:
success:function(articles) {
$(".articles").html(articles);
}
Now create a view for ajax response without extending the template like this:
//defaultAjax.blade.php used in the controller for ajax response
<ul>
#foreach ($articles as $article)
<li>
<img src="{{ $article->user_image }}" alt="{{ $article->article_title }}" />
{{ $article->article_title }}
<div class="details">
<span class="author">{{ $article->author_name }}</span>
<span class="created">{{ Helpers::time_ago($article->published_at) }}</span>
<span class="symbol">{{ $article->symbol_title }}</span>
</div>
</li>
#endforeach
</ul>
{{ $articles->links() }}
Notice, there is no #extendds() or #section(), just plain partial view, so it'll be rendered without the template and you can insert the ul inside the .articles div. That's it.
$("#articles").html(articles); ->> $(".articles").html(articles);

Resources