Laravel Livewire and select2 dropdown inside of bootstrap modal - laravel

I have a select2 blade component that I'm using inside of a bootstrap modal. Everything is working as expected, except when the modal form is submitted, the styling of the select 2 is lost, and a new empty option (must be the placeholder) is shown as an option, but only in the selects where I'm doing a yes/no option where the value is either 1 or 0 (See code below).
The styling is messed up on all, but the extra placeholder option only shows up on some.
Here's the normal way it should look, and does look when you first open the modal after a page refresh:
After submitting the modal it looks like this (notice the extra placeholder option):
Here's the code for my select2 component:
#props(['id', 'usePlaceholder' => null])
<div wire:ignore style="width:100%;" id="select2-{{ $id }}">
<select {{ $attributes }} id="{{ $id }}" name="{{ $id }}[]" class="form-select select2" data-width="100%" data-select2-id="{{ $id }}">
#if ($usePlaceholder)
<option></option>
#endif
{{ $slot }}
</select>
</div>
{{-- blade-formatter-disable --}}
#push('custom-scripts')
<script>
$(document).ready(function() {
let select2 = $('#' + #js($id));
select2.select2({
dropdownParent: $('#select2-' + #js($id)),
placeholder: 'Select an option',
minimumResultsForSearch: 15
});
***The below listeners I can comment out, they have no effect as far as I can tell, just including them for reference***
window.addEventListener('hideModal', event => {
select2.val(null).trigger('change');
});
window.addEventListener('set'+#js(ucfirst($id)), event => {
select2.val(event.detail).trigger('change');
});
});
</script>
#endpush
{{-- blade-formatter-enable --}}
And here's where I'm actually using the component:
<div class="col-md-6">
<x-input.group label="Sales Tax" for="salesTax" :error="$errors->first('salesTax')">
<x-input.select2 usePlaceholder="{{ empty($salesTax) ? 'true' : '' }}" wire:model.defer="salesTax" id="salesTax">
<option name="no" id="sales-tax-0" value="0">No</option>
<option name="yes" id="sales-tax-1" value="1">Yes</option>
</x-input.select2>
</x-input.group>
</div>
I've tried destroying and re-initializing the select2 when the modal is dismissed, but that hasn't done anything for me.

Related

Laravel Livewire: Bootstrap Select-Picker unable to render

I want to load categories and multiple sub categories according to all the categories that are selected, inside a multi-step form at step 4, but I am not able to select multiple categories using bootstrap selectpicker (multiple selects) library inside livewire component. The issue with using Bootstrap-Select with Livewire is that both of these libraries use JavaScript to update the page dynamically, which is probably causing conflicts. If I try accessing the same without livewire, it works perfectly fine, but with livewire I am unable to render it.
form.blade
#section('content')
<livewire:multi-step-form />
#endsection
#push('before-styles')
<link rel="stylesheet prefetch" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.1/css/bootstrap-select.css" />
#endpush
#push('before-scripts')
<script rel="javascript prefetch" src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.bundle.min.js"></script>
<script rel="javascript prefetch" src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.18/js/bootstrap-select.min.js" integrity="sha512-yDlE7vpGDP7o2eftkCiPZ+yuUyEcaBwoJoIhdXv71KZWugFqEphIS3PU60lEkFaz8RxaVsMpSvQxMBaKVwA5xg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
#endpush
multi-step-form.blade
<select wire:model="categories" name="categories[]" class="selectpicker form-control" multiple data-live-search="true" required id="categories" title="{{ trans('select category') }}">
#foreach($categories as $category)
<option value="{{$category->id}}" {{ (collect(old('categories'))->contains($category->id)) ? 'selected' : '' }}>
{{$category->category}}
</option>
#endforeach
</select>
Some Failed Attempts
Inside livewire component
Attempt 1
<script>
document.addEventListener('livewire:load', function () {
$('.selectpicker').selectpicker();
});
</script>
This works well but only once, i.e. at the start when livewire was loaded. This doesn't work, after the livewire component is updated by some input. So I added a code right below it:
Attempt 2
<script>
document.addEventListener('livewire:load', function () {
$('.selectpicker').selectpicker();
$('.selectpicker').selectpicker('render');
});
</script>
This didn't work too. Some few articles had render separated in updated script so I did that too.
Attempt 3
<script>
document.addEventListener('livewire:load', function () {
$('.selectpicker').selectpicker();
});
document.addEventListener('livewire:updated', function () {
$('.selectpicker').selectpicker('render');
});
</script>
Unfortunately this didn't work either.
I have tried adding the same scripts to my app.js too later, but am not sure where am I going wrong, and how can I make this working.
Attempt 4
Kept the scripts as is, and added wire:ignore in parent div
<div wire:ignore class="form-group">
<select wire:model="categories" name="categories[]" class="selectpicker form-control" multiple data-live-search="true" required id="categories" title="{{ trans('select category') }}">
#foreach($categories as $category)
<option value="{{$category->id}}" {{ (collect(old('categories'))->contains($category->id)) ? 'selected' : '' }}>
{{$category->category}}
</option>
#endforeach
</select>
</div>
As suggested in livewire/livewire/issues/45, but this didn't work as well.

Livewire uncaught (in promise) DOMException with select

I have a dropdown in my component that seems to be causing an error in Livewire.
Uncaught (in promise) DOMException: Failed to execute 'setAttribute'
on 'Element': '?' is not a valid attribute name.
I added the wire:key code based on what I read in the docs under the troubleshooting section, but it didn't seem to help. It updates the value on save just like it should, but when it refreshes the dom with the new data, it's not working.
Here's the offending element:
<div class="mb-3 col-md-4 ">
<label for="state" class="form-label">State</label>
<select wire:model.defer="location.state" class="form-select"
id="state" name="state">
#foreach ($states as $state)
<option
name="{{ $state->name }}"
id="{{ $state->code }}"
wire:key="{{ $state->id }}"
value="{{ $state->code }}"
#if (isset($location->state)
&& $location->state === $state->code) ? selected #endif
>{{ $state->name }}
</option>
#endforeach
</select>
</div>
You have a typo here,
#if (isset($location->state) && $location->state === $state->code)
? selected
#endif
Where it tries to apply ? as an attribute on the element, but ? is not a valid attribute. Just remove the ?.
That said, you cannot use the selected property on <select> elements in Livewire to set the selected value. You need to set the value of your wire:model in your component. This means that you have to remove the #if(..) selected #endif from your Blade entirely, and instead set the value in your component,
public function mount() {
$this->location['state'] = $state->code;
}

How to avoid livewire request for simple inputs calculations?

How would I avoid doing calculation for 3 inputs using Livewire and use JS instead, but still can bind the inputs and their new values with the component.
Example:
<input id="cash-price"
type="text"
wire:model="total_cache_price"
class="amount-credit">
<input id="deposit"
type="text"
wire:model="deposit"
class="amount-credit">
<input id="trade-in"
type="text"
wire:model="trade_in"
class="amount-credit">
I can easily do a simple calculation using JS, but the properties in Livewire component would still be empty or null after submitting the form. I am trying to avoid livewire requests for every input change.
Note: I understand the deferred updating in livewire, the problem is with the property values not changing.
I will show you an example in alpine js + livewire way.
Here I want to set value in two inputs from a selected option in select.
Please note that x-ref is used in alpine to directly access DOM elements without using getElementById.
<div x-data="{
from_date : #entangle('from_date').defer,
to_date : #entangle('to_date').defer,
dateChange(){
select = this.$refs.years;
this.from_date = select.options[select.selectedIndex].getAttribute('data-from');
this.to_date = select.options[select.selectedIndex].getAttribute('data-to');
},
resetFilters(){
this.net_balances = false;
}}">
<select x-ref="years" #change="dateChange()">
<option value="">Year</option>
#foreach ($years as $key => $year)
<option wire:key="{{ 'years'.$key }}" data-from="{{ $year->from_date }}" data-to="{{ $year->to_date }}" value="{{ $year->id }}">{{ $year->name }} </option>
#endforeach
</select>
<div>
<text type="date" x-model="from_date" />
</div>
<div >
<text type="date" x-model="to_date" />
</div>

Alpine JS - Creating a menu with active states

I am trying to create a sidebar menu with Alpine JS
I am not even sure if this is possible to do with Alpine JS.
#foreach($kanbans as $kanban)
<div x-data="activeKanban : ????">
<div #click="activeKanban = {{$kanban->id}}">
<div x-show="activeKanban !== {{$kanban->id}}">
// Design for collapsed kanban
</div>
<div>
<div x-show="activeKanban === {{$kanban->id}}">
// Design for active kanban
</div>
</div>
#endforeach
As I switch trough the pages, the $kanban->id changes, and I was wondering instead of manually setting activeKanban : 1 is there a way to pass this information to AlpineJS?
So that by default if I load the other page, the default menu that would be open would be based on the ID instead of them all being collapsed or just 1 that is specified being open?
If you're aiming for an accordion menu of sorts here's how you might achieve it with AlpineJs based on the code you shared:
// Set x-data on a div outside the loop and add your active kanban as a property
<div x-data="{
activeKanban: {{ $activeKanbanId ?? null }}
}">
#foreach($kanbans as $kanban)
<div #click="activeKanban = {{ $kanban->id }}">
<div x-show="activeKanban !== {{ $kanban->id }}">
// Collapsed view
</div>
<div x-show="activeKanban === {{ $kanban->id }}">
// Expanded view
</div>
</div>
#endforeach
</div>
Here each kanban menu item will have access to the activeKanban property in AlpineJs component instance and can set it reactively.
i.e. if activeKanban is set to a new id the current open item will close and the new one will open
Adding flexibility
What if you want to open and close them all independently though? There's more than one way to achieve this but in this case we can modify the code above to allow for it:
// Here we add an array of openItems and two helper functions:
// isOpen() - to check if the id is either the activeKanban or in the openItems array
// toggle() - to add/remove the item from the openItems array
<div x-data="{
activeKanban: {{ $activeKanbanId ?? null }},
openItems: [],
isOpen(id){
return this.activeKanban == id || openItems.includes(id)
},
toggle(id){
if(this.openItems.includes(id)){
this.openItems = this.openItems.filter(item => {
return item !== id
});
}else{
this.openItems.push(id)
}
}
}">
#foreach($kanbans as $kanban)
<div #click="toggle({{ $kanban->id }})">
<div x-show="!isOpen({{$kanban->id}})">
// Collapsed view
</div>
<div x-show="isOpen({{$kanban->id}})">
// Expanded view
</div>
</div>
#endforeach
</div>
This allows us to set an active item and also optionally open/close other menu items.

Vue component select some options by default

UPDATE: the issues does not reproduce with "CDN" style. Here's the perfectly working JSBin: https://jsbin.com/ziqasifoli/edit?html,js,output
It looks that it is connected to something amongst webpack/gulf/elixir/vuefy.. I.e., all that Laravel 5.3 infrastructure. Not sure even where to start debugging.
...
I'm struggling with a simple (as I thought) component, that gets name, options and selected as input and displays multiple selectbox:
<!-- TagSelector.vue: -->
<template>
<select :name="name" v-model="selected" multiple>
<option v-for="option in options" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</template>
<script>
export default {
props: ['name', 'options', 'selected'],
}
</script>
.
// app.js
Vue.component('tags-selector', require('./components/form_fields/TagsSelector.vue'));
const app = new Vue({
el: '#app'
});
.
<!-- some.form.blade.php -->
<tags-selector
class="form-control"
name="posted_data[tags]"
:selected="['opt1', 'opt2']"
:options='{!! json_encode($options) !!}'
></tags-selector>
It displays selector as expected, but no options pre-selected there.
How do I get it working?
To set some options as selected by default you will want to bind them to data, to do this simply make a copy of your selected prop and use v-model to bind it, which you can do with the created hook:
created: function() {
this.selectedCopy = this.selected;
},
data: function(){
return {
selectedCopy: []
}
}
Then simply bind them by using v-model:
<select :name="name" v-model="selectedCopy" multiple>
<option v-for="option in options" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
Here's the JSFiddle: https://jsfiddle.net/3cq1jjL8/

Resources