How to re-init the AlpineJS component after Livewire re-renders? - laravel

I have an alert component that I hide using AlpineJS but I want it to be visible again after Livewire re-renders.
Showing the alert using Livewire component
#if(Session::has('success'))
<x-success-alert :message="Session::get('success')" />
#endif
AlpineJS component
<div x-data="{show: true}">
<div x-show="show">
<span>{{ $message }}</span>
<button #click="show = false">×</button>
</div>
</div>

The solution is to force Livewire to add that element in the dom again by adding wire:key to a random value.
<div wire:key="{{ rand() }}">
<div x-data="{show: true}">
<div x-show="show">
<span>{{ $message }}</span>
<button #click="show = false">×</button>
</div>
</div>
</div>
With that way, Livewire will destroy the old dom element and add the new one which re-init the Alpine component.

Just in case this helps anyone else, I have found that if an alpine component within a Livewire component is removed from the dom, it is not loaded correctly if reinstated *if the x-data directive is applied to the outermost element.
So, for example, just wrap the <x-success-alert> component in an additional div:
<div>
<div x-data="{show: true}">
<div x-show="show">
<span>{{ $message }}</span>
<button #click="show = false">×</button>
</div>
</div>
</div>

Related

Unable to swap Jetstream default TailwindCSS modal for Bootstrap Modal and still achieve the expected or same result as its was with TailwindCSS

Am working on a Laravel 8 package that swaps the #TALL stack for what I call the #BALL stack, and its basically a Bootstrap, AlpineJs Laravel, Livewire stack. Bootstrap 5 is leaning towards utility classes and no longer makes use of JQuery which gives room for AlpineJs so I really don't see a lot of upsides with Tailwind, not to mention I've gotten really comfortable with Bootstrap which should be all that matters at the end right?
Now the problem is this, I've been able to make changes to a lot of the JetStream blade files and if youve install Jetstream then your familiar with this image:
(Yes!!!! Its the exact same thing but with Bootstrap!)
but one particular component has kept me up for days and its the Bootstrap Modal.
Whenever I call the Bootstrap modal Livewire requests seem to make the Modal itself disappear leaving just the backdrop, forcing a page request before anything else can be clicked.
The final request is never completed
I have no way to hold the modal until a confirmation request is loaded and then programmatically disable the modal when finished.
The exact place I have this problem is in the views/api/api-token-manager.blade which is added by Jetstream when you install the livewire stack. My files looks like so:
My views/api/api-token-manager.blade
<!-- API Token List -->
// Inside a x-jet-action-section component
<x-slot name="content">
<div class="space-y-6">
#foreach ($this->user->tokens->sortBy('name') as $token)
<div class="d-flex justify-content-between">
<div>
{{ $token->name }}
</div>
<div class="d-flex">
#if ($token->last_used_at)
<div class="text-sm text-muted">
Last used {{ $token->last_used_at->diffForHumans() }}
</div>
#endif
#if (Laravel\Jetstream\Jetstream::hasPermissions())
<button class="btn btn-link text-secondary" data-toggle="modal"
wire:click="manageApiTokenPermissions({{ $token->id }})"
data-target="#managingApiTokenPermissions-{{ $token->id }}">
Permissions
</button>
#endif
<button class="btn btn-link text-danger text-decoration-none"
data-toggle="modal"
data-target="#confirmApiTokenDeletion-{{ $token->id }}">
Delete
</button>
</div>
</div>
<!-- API Token Permissions Modal -->
<x-jet-dialog-modal id="managingApiTokenPermissions-{{ $token->id }}">
<x-slot name="title">
API Token Permissions
</x-slot>
<x-slot name="content">
<div class="mt-2 row">
#foreach (Laravel\Jetstream\Jetstream::$permissions as $permission)
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ $permission }}" wire:model.defer="updateApiTokenForm.permissions">
<label class="form-check-label">
{{ $permission }}
</label>
</div>
</div>
#endforeach
</div>
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button data-dismiss="modal">
{{ __('Nevermind') }}
</x-jet-secondary-button>
<x-jet-button class="ml-2" wire:click="updateApiToken"
wire:click="$emit('updateApiToken', {{ $token->id }})"
data-dismiss="modal">
{{ __('Save') }}
</x-jet-button>
</x-slot>
</x-jet-dialog-modal>
#push('scripts')
<script>
Livewire.on('updateApiToken', id => {
#this.manageApiTokenPermissions(id)
#this.updateApiToken()
})
</script>
#endpush
<!-- Delete Token Confirmation Modal -->
<x-jet-confirmation-modal id="confirmApiTokenDeletion-{{ $token->id }}">
<x-slot name="title">
Delete API Token
</x-slot>
<x-slot name="content">
Are you sure you would like to delete this API token?
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button data-dismiss="modal">
Nevermind
</x-jet-secondary-button>
<x-jet-danger-button class="ml-2"
wire:click="$emit('deleteApiToken', {{ $token->id }})"
data-dismiss="modal">
Delete
</x-jet-danger-button>
</x-slot>
</x-jet-confirmation-modal>
#push('scripts')
<script>
Livewire.on('deleteApiToken', id => {
#this.confirmApiTokenDeletion(id)
#this.deleteApiToken()
})
</script>
#endpush
#endforeach
</div>
</x-slot>
My x-jet-dialog-modal component
#props(['id' => null, 'maxWidth' => null])
<x-jet-modal :id="$id" :maxWidth="$maxWidth" {{ $attributes }}>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
{{ $content }}
</div>
<div class="modal-footer">
{{ $footer }}
</div>
</div>
</x-jet-modal>
My x-jet-modal component
#props(['id', 'maxWidth', 'modal' => false])
#php
$id = $id ?? md5($attributes->wire('model'));
switch ($maxWidth ?? '') {
case 'sm':
$maxWidth = ' modal-sm';
break;
case 'md':
$maxWidth = '';
break;
case 'lg':
$maxWidth = ' modal-lg';
break;
case 'xl':
$maxWidth = ' modal-xl';
break;
case '2xl':
default:
$maxWidth = '';
break;
}
#endphp
<!-- Modal -->
<div class="modal fade" tabindex="-1" id="{{ $id }}" aria-labelledby="{{ $id }}" aria-hidden="true">
<div class="modal-dialog{{ $maxWidth }}">
{{ $slot }}
</div>
</div>
I really don't want to make the change to Tailwind regardless of the hype so I would love any assistance the community can render.
To make a direct contribution to the project click here.
Thanks!!!
I was able to solve the above problem with Livewire Event. Its important to note that the reason for all this hassle was to create assets to replace Jetstream assets without affecting the business logic i.e. MODEL and CONTROLLER.
Here is what my solution looks like:
<div class="mt-3">
<x-jet-danger-button wire:click="$emit('confirmingUserDeletion')"
wire:loading.attr="disabled">
Delete Account
</x-jet-danger-button>
</div>
<!-- Delete User Confirmation Modal -->
<x-jet-dialog-modal id="confirmingUserDeletionModal">
<x-slot name="title">
Delete Account
</x-slot>
<x-slot name="content">
Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.
<div class="mt-4 w-75" x-data="{}" x-on:confirming-delete-user.window="setTimeout(() => $refs.password.focus(), 250)">
<x-jet-input type="password" class="{{ $errors->has('password') ? 'is-invalid' : '' }}" placeholder="Password"
x-ref="password"
wire:model.defer="password"
wire:keydown.enter="deleteUser" />
<x-jet-input-error for="password" />
</div>
</x-slot>
<x-slot name="footer">
<x-jet-secondary-button wire:click="$toggle('confirmingUserDeletion')"
wire:loading.attr="disabled"
data-dismiss="modal">
Nevermind
</x-jet-secondary-button>
<x-jet-danger-button wire:click="deleteUser" wire:loading.attr="disabled">
Delete Account
</x-jet-danger-button>
</x-slot>
</x-jet-dialog-modal>
#push('scripts')
<script>
Livewire.on('confirmingUserDeletion', () => {
#this.confirmUserDeletion()
new Bootstrap.Modal(document.getElementById('confirmingUserDeletionModal')).toggle()
})
</script>
#endpush

How to separate components in Vue?

I have Laravel app, and create search box with algolia everything is good and the search is find but my question now for this code of Vue :
<template>
<ais-index app-id="myid" api-key="mykey" index-name="users">
<ais-input class="search form-control shadow-sm rounded" placeholder="Search"></ais-input>
<ais-results>
<template slot-scope="{result}">
<div>
<h2>{{ result.name }}</h2>
<h4>{{ result.email }}</h4>
</div>
</template>
</ais-results>
</ais-index>
</template>
<script>
export default {};
</script>
I need spare the form ( ais-input ) in a place and put the result ( ais-results ) in another place of course same page
Something like that make two component one for input and second for result ( blew it will be in blade file ) :
<div class="col-md-2">
<search-box/> (<ais-input/>)
</div>
<div class="col-md-2">
<result-box/> (<ais-results/>)
</div>
Considering they seem to already be components, you can try to use them directly.
You can create two new Vue components, where you add the existing components:
<template>
<div class="col-md-2">
<ais-input class="search form-control shadow-sm rounded" placeholder="Search"></ais-input>
</div>
</template>
And the you can use them in your template as you wish.

Laravel + Vue : method written in vue modal template not defined

I am working on the CRUD part of a Laravel app.
It is supposed that when the delete button of an entry is clicked, a modal show up and ask user to confirm delete the corresponding entry or not.
I tried to do this but it said the method written in vue modal template is not defined in the JS console of chrome browser when I clicked the button.
Sure I have defined it. Why does this happen and how to fix it?
If there is any similar example that demonstrate how to do it in vue,
please provide the link. Thanks!
This is the structure of my frontend code.
The blade.php
<button id="show-modal" class="btn btn-danger"
#click="triggerDeleteModal($project)">
delete</button>
<modal-delete></modal-delete>
/resources/js/app.js
Vue.component('modal-delete', require('./components/ModalDelete.vue'));
/resources/js/components/ModalDelete.vue
<template>
<div class="modal fade" tabindex="-1"
role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×
</button>
<h4 class="modal-title">Modal Header</h4>
</div>
<div class="modal-body">
<p>Some text in the modal.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Close</button>
<button type="button" class="btn btn-primary">Save</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['project'],
methods: {
triggerDeleteModal(project) {
alert('Did something!' + project + " - project : " + this.project);
}
}
}
</script>
You got two options here. First the one LakiGeri mentioned you put the button inside the vue-template.
The second way is you just calling the the blade.php and you call two templates. First the template with the button and the modal-delete call. And after this you $emit the function from the inside of the modal-delete template.
Vue.js emit events
I would suggest the second way because so you can reuse the modal-delete Template.
I show you one of my examples: create.blade.php
...
#section('content')
<p>Neues Raster anlegen</p>
<div class="col-md-12">
<gridunits></gridunits>
</div>
#endsection
Now calling the first template gridunits. Inside this i forward the methods for the emit-events remove and add to the second template.
<template>
...
<div class="form-group">
<gridunititems
v-for="gridunit in request.gridunits"
:key="gridunit.id"
:gridunit="gridunit"
#remove="removeGridUnit"
#add="addGridUnit"
>
</gridunititems>
</div>
...
</template>
<script>
...
methods: {
addGridUnit(id) {
//doing things
},
removeGridUnit(idToRemove) {
//doing things
},
...
</script>
Second template gridunititems
<template>
...
<div class="input-group-append">
<button type="button" class="btn btn-outline-success" #click.prevent="$emit('add',gridunit.id)">+</button>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-outline-danger" #click.prevent="$emit('remove',gridunit.id)">-</button>
</div>
...
</template>
create.blade.php calling first template
gridunits.vue calling second template and methods in script
gridunitsitems.vue button who emitting event

Collapse using Transition Vue

I would like to use Vue's collapse in my code, but I have an error.
[Vue warn]: <transition-group> children must be keyed: <p>
My component:
<template xmlns:v-model="http://www.w3.org/1999/xhtml" xmlns:v-on="http://www.w3.org/1999/xhtml">
<section style="background-color: #dedede;">
<div class="container-fluid">
<div class="Consult-faq container">
<div class="row">
<div class="col-sm-12">
<h2>Cursos</h2>
<a v-for="(course,id) in courses" v-on:click="course.show = !course.show">
<a v-on:click="show = !show">
<div class="col-xs-12" style="border-bottom: solid;border-bottom-color: #999999;border-bottom-width:1px ">
<div class="col-xs-12">
<h4>
<i v-if="course.show" class="fa fa-plus-square text-right" aria-hidden="true"/>
<i v-else class="fa fa-minus-square text-right" aria-hidden="true"/>
{{course.text}}
</h4>
</div>
</div>
<transition-group name="fade">
<p v-if="show">
<div class="col-xs-12">
<article v-for="n in 2" class="Module-content">
<div class=" col-sm-12 col-md-6" style="position: relative;">
<div v-for="(course, index) in course.courses">
<course-card v-if="index % 2 == n - 1" :course="course"></course-card>
</div>
</div>
</article>
</div>
</p>
</transition-group>
</a>
</a>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default{
props : [
'courses'
],
data(){
return {
show: false
}
},
mounted() {
console.log(this.courses)
}
}
</script>
So, I'd like to know to collapse item per item. Like this in image.
When I click to expand, all courses expand or close all courses close.
Transition is irrelevant here (though you can get rid of that warning by using transition instead of transition-group, because the transition is only acting on a single node, not a group.)
Right now you're depending on a single variable show to control all of the elements' visibility, so they will all respond to clicks on any of them:
<a v-on:click="show = !show">
<p v-if="show" >
You need individual variables for each element if you want them to expand/collapse separately. You partially did this already, just change the remaining instances of show with course.show and you should be good to go.
(Probably want to clean up that nested <a> within <a> while you're at it; you can just remove the inner one.)
I solved this using vue-resource, I was using Guzzle in Laravel and require data in Controller make this not reactive. And I solved this problem using vue-resource in component.

Validation of invisible fields on submit

(Symfony 2.8)
I want to insert data into a form as steps in a process.
The field to validate is in "Descripción" tab and the submit buttons are in the "Guardar/Revisión" tab.
When I click the submit button, it displays the error "An invalid form control with name='form[proDescripcion]' is not focusable" in the Chrome debug console.
Is it possible to correct the error without changing the design?
<div id="tabs-1">
<p>Descripción</p>
<div class="inputText">
<div class="label">
<label for='proDescripcion' >{{ form_label(form.proDescripcion) }}</label>
</div>
<div class="input">
{{ form_widget(form.proDescripcion) }}
</div>
</div>
</div>
<div id="tabs-2">
<p>Imagen/Documento</p>
</div>
<div id="tabs-3">
<p>Datos</p>
</div>
<div id="tabs-4">
<p>Guardar/Revisión</p>
{{ form_row(form.save) }}
{{ form_row(form.finish) }}
</div>
You can add a "novalidate" attribute to your form
{{ form_start(contact, {attr: {novalidate: 'novalidate'}}) }}
With this attribute, HTML5 will not try to validate your form on submit. Don't forget to do it on the server-side with assert annotation.

Resources