Laravel Livewire/AlpineJS: Disable a button while loading data from an API and then enabling it again - laravel

This should be really simple, but I don't get it. I want to replicate the functionality of a button that when pressed goes to an API (which could take about a minute to get the data and process it), it gets diabled, and after loading the data it gets enabled.
I'm using Laravel/Livewire/Alpine
So, in my livewire component I have:
public $loading = false;
In my blade file, I have the declaration for the div where the button is:
<div
class="grid grid-cols-3 gap-4"
x-data="{
loading: #entangle('loading')
}"
>
Then the button x-binds the disabled property to the loading value, when the button is clicked, it changes the property of the loading variable, and calls the loader function
<button
type="button"
class="btn btn-sm btn-jbn"
x-on:click="loading = true"
x-bind:disabled="loading"
wire:click="loader"
>
Load API
</button>
And it does what it is supposed to do... the button is grayed, it becomes unusable, the cursor change, etc., it executes the loader function in my livewire component, but it never return to the normal state after loading the API. In my livewiere componente I have:
public function loader() {
// API call and logic goes here, this works
$this->loading = false;
}
So I would imagine that at the end of the API process the entangled variable loading would return the button to its normal state, but it doesn't
What am I missing?

Livewire already has incorporated functionality to handle loading-states. Instead of implementing your own, you can use this.
Get rid of all your current loading-logic, and simply use wire:loading with wire:target on your button.
wire:loading can toggle the disabled attribute directly by doing wire:loading.attr="disabled", and wire:target is to set the target for that loading-state to the method you are calling, so in your case that's wire:target="loader".
This means your button looks like this,
<button
type="button"
class="btn btn-sm btn-jbn"
wire:click="loader"
wire:loading.attr="disabled"
wire:target="loader"
>
Load API
</button>

Related

Mixing Alpine.js with 'static' serverside markup, while getting the benefits of binding, etc

I'm new to Alpine and struggling to wrap my head around how to make a scenario like this work:
Let's say I have a serverside built page, that contains some buttons, that represent newsletters, the user can sign up to.
The user might have signed up to some, and we need to indicate that as well, by adding a css-class, .i.e is-signed-up.
The initial serverside markup could be something like this:
<button id='newsletter-1' class='newsletter-signup'>Newsletter 1</button>
<div>some content here...</div>
<button id='newsletter-2' class='newsletter-signup'>Newsletter 2</button>
<div>more content here...</div>
<button id='newsletter-3' class='newsletter-signup'>Newsletter 3</button>
<div>and here...</div>
<button id='newsletter-4' class='newsletter-signup'>Newsletter 4</button>
(When all has loaded, the <button>'s should later allow the user to subscribe or unsubscribe to a newsletter directly, by clicking on one of the buttons, which should toggle the is-signed-up css-class accordingly.)
Anyway, then I fetch some json from an endpoint, that could look like this:
{"newsletters":[
{"newsletter":"newsletter-1"},
{"newsletter":"newsletter-2"},
{"newsletter":"newsletter-4"}
]}
I guess it could look something like this also:
{"newsletters":["newsletter-1", "newsletter-2", "newsletter-4"]}
Or some other structure, but the situation would be, that the user have signed up to newsletter 1, 2 and 4, but not newsletter 3, and we don't know that, until we get the JSON from the endpoint.
(But maybe the first variation is easier to map to a model, I guess...)
Anyway, I would like to do three things:
Make Alpine get the relation between the model and the dom elements with the specific newsletter id (i.e. 'newsletter-2') - even if that exact id doesn't exist in the model.
If the user has signed up to a newsletter, add the is-signed-up css-class to the corresponding <button> to show its status to the user.
Bind to each newsletter-button, so all of them – not just the ones, the user has signed up to – listens for a 'click' and update the model accordingly.
I have a notion, that I might need to 'prepare' each newsletter-button beforehand with some Alpine-attributes, like 'x-model='newsletter-2', but I'm still unsure how to bind them together when Alpine has initialising, and I have the data from the endpoint,
How do I go about something like this?
Many thanks in advance! 😊
So our basic task here is to add/remove a specific item to/from a list on a button click. Here I defined two component: the newsletter component using Alpine.data() creates the data (subs array), provides the toggling method (toggle_subscription(which)) and the checking method (is_subscribed(which)) that we can use to set the correct CSS class to a button. It also handles the data fetching in the init() method that executes automatically after the component is initialized. I have also created a save method that we can use to send the subscription list back to the backend.
The second component, subButton with Alpine.bind() is just to make the HTML code more compact and readable. (We can put each attribute from this directly to the buttons.) So on click event it calls the toggle_subscription with the current newsletter's key as the argument to add/remove it. Additionally it binds the bg-red CSS class to the button if the current newsletter is in the list. For that we use the is_subscribed method defined in our main component.
.bg-red {
background-color: Tomato;
}
<script src="https://unpkg.com/alpinejs#3.x.x/dist/cdn.min.js" defer></script>
<div x-data="newsletter">
<button x-bind="subButton('newsletter-1')">Newsletter 1</button>
<button x-bind="subButton('newsletter-2')">Newsletter 2</button>
<button x-bind="subButton('newsletter-3')">Newsletter 3</button>
<button x-bind="subButton('newsletter-4')">Newsletter 4</button>
<div>
<button #click="save">Save</button>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('newsletter', () => ({
subs: [],
init() {
// Fetch list of subscribed newsletters from backend
this.subs = ['newsletter-1', 'newsletter-2', 'newsletter-4']
},
toggle_subscription(which) {
if (this.subs.includes(which)) {
this.subs = this.subs.filter(item => item !== which)
}
else {
this.subs.push(which)
}
},
is_subscribed(which) {
return this.subs.includes(which)
},
save() {
// Send this.sub to the backend to save active state.
}
}))
Alpine.bind('subButton', (key) => ({
'#click'() {
this.toggle_subscription(key)
},
':class'() {
return this.is_subscribed(key) && 'bg-red'
}
}))
})
</script>

Trigger Ajax request on settings page

I'm writing a plugin that have some fields defined in backend settings.
One of that field is a partial that contain a button, that trigger an Ajax request thanks to the data-attribute API.
<button type="button" class="btn btn-danger" data-request="onReset">
My button
</button>
Where should I put the "onReset()" function ? I tried to put it in my Settings model, even tried to create a Settings controller (whereas not necessary to work with settings page), but I always get the following error :
Ajax handler 'onReset' was not found
I don't know what to do to be able to trigger that onReset() function, can somebody familiar with Laravel / OctoberCMS could point me the right direction ?
Thanks
Settings are rendered by the \System\Controllers\Settings so whatever you write will be handled by \System\Controllers\Settings Controller Class,
so we need to extend \System\Controllers\Settings and we can add dynamic
method there,
inside your plugin's boot method you need to write this extension code
I am renaming your onReset handler to onPrefixReset in this example to avoid any other conflicts.
public function boot() {
\System\Controllers\Settings::extend(function($model) {
$model->addDynamicMethod('onPrefixReset', function() {
\Flash::success('You did it!');
// OR
//return ['some data data'];
});
});
}
onPrefixReset : we are adding some Prefix here, so accidentally we are not overriding internal original methods.
now inside your _partial
<button type="button" class="btn btn-danger" data-request="onPrefixReset">
My button
</button>
so now this onPrefixReset ajax handler is defined in \System\Controllers\Settings as we did that dynamically ;) it will handle your request.
you can return any data from here and it will be received at back-end settings page.
In HTML forms if you have an onReset event, you can add your call inside there:
function updateForm()
{
$.each($('form').find(":input"), function(){
$.uniform.update($(this));
});
}
<form onReset="updateForm();">
In this link you can you can do your own ordering within a click event:
$(document).ready(function() {
$("input:reset").click(function() { /click event
this.form.reset();
window.alert($("input:text").val());
return false;
});
});

How to initiate an action to `component` from `Router`

In my page, the router taking care of page transition. but before i go for next page, I would like to validate the form fields. for that I use the cp-validation addon.
But when the next button clicked, I would like to ask my child component to do the validation and send back the result to router. how to do this?
I tried to validate the model within router but not works. if my try is not correct way please let me know the correct approach to handle this scenario.
my router bhs:
<div class="balanceEmi rdc-scroll-content has-subheader rdc-view">
{{cs2i-select-tenure selectedCreditCard=model}}//my component
//which requie to know the form validation
</div>
<div class="rdc-view___footer rdc-view___footer---stickey">
{{rdc-button default="CANCEL" type="secondary" action="redirect"}}
{{#if enableNext}}
{{rdc-button default="NEXT" type="primary" action="goToNext"}}
{{else}}
{{rdc-button default="NEXT" type="primary" disabled="true"}}
{{/if}}
<div class="formValidateBeforeNextBtn" {{action 'formValidateBeforeNext'}}>//let this actin call my component to do the validaiton else le tme know the correct way
<!-- empty for validation button-->
</div>
</div>
I recommend registering the validate component to a factory, then you can call this from wherever you are in your application like a router.
Read the Ember.js docs here

Scripts not working on partial view after Ajax call

I have called scripts on _Layout.cshtml page and my Index.cshtml page has partial view into it. So on page load, SignalR scripts working perfect on partial view, on page end I make another ajax request and load the partial view with another data filled in that and embed under already displayed data, and then the SignalR does not work on the newly embedded record.
This is my index page code:
<div class="col-md-6">
<div class="profile-body">
<div class="row infinite-scroll">
#Html.Partial("_AlbumRow", Model)
</div>
</div>
</div>
This is my partial View Code:
#model IEnumerable<SmartKids.Lib.Core.ViewModels.FileMediaAlbumsVM>
#foreach (var item in Model)
{
<div class="widget">
<div class="block rounded">
<img src="#Url.Content(item.ImageUrl)" alt="#item.Title">
<input type="button" data-image-id="#item.imageId" class="btn btn-sm btn-default">Like</input>
</div>
</div>
}
Kindly help me how to resolve this issue that after making an ajax request I am not able to get those SignalR working. Here is more to say when I put the SignalR scripts on PartialView that works but it also sucks that on each ajax request there is again SignalR loaded on the page and when I click on LIke button it makes many calls to the function behind it.
Kindly help me to resolve this issue, I am stuck at this point since 1 week.
Here is signalR Code:
$(".btn.btn-sm.btn-default").on("click", function () {
var imageId = $(this).attr("data-image-id");
albumClient.server.like(imageId);
});
Problem: You are binding event to elements directly, So when you remove this element and replace it with a different one the events are also removed along with that element, This is something like strongly coupled.
Solution: Use Jquery event delegation. This will make sure the events will be triggered on the current elements and also all the elements that can come in future.
syntax is as below.
$(document).on("click", ".btn.btn-sm.btn-default",function () {
var imageId = $(this).attr("data-image-id");
albumClient.server.like(iamgeId);
});
NOTE: This was never a singlaR issue, it was Jquery issue.
Efficient Way: The problem in using $(document).on("click"... is that when ever there is a click happening on the entire page the Jquery framework will bubble the events from the clicked element upwards(its parent, and its parent and so on..) unless the element specified in the selector arrives, So its kind of performance hit as we don't want this check's to run if we are clicking outside the required area ( button .btn.btn-sm.btn-default in this example).
So best practice is to bind this event delegation to the closest parent possible which will not be removed, <div class="row infinite-scroll"> in this question. So that only when the click happens within this element the event bubbling will happen and also will be stopped once it reaches the parent element,it acts kind of a boundary for event bubbling.
$('.row.infinite-scroll').on("click", ".btn.btn-sm.btn-default",function () {
var imageId = $(this).attr("data-image-id");
albumClient.server.like(iamgeId);
});

Calling ajax function from another js script

I have a ajax function that is called by button tag
<button id="of_save" type="button" class="button-primary">
<?php echo __('Save All Changes', 'optionsframework');?>
</button>
$('#of_save').live('click',function() {
...
...
...
}};
The problem is that theme author put a single button on top of settings page and a million of settings, so every time I want to hit save, I must scroll whole page.
I found a little JS script that intercept CTRL+S or another browser combination and is working wonderful
http://www.openjs.com/scripts/events/keyboard_shortcuts
The problem is I don't know how to call the ajax function from this JS
shortcut.add("Ctrl+Shift+X",function() {
alert("Hi there!");
// not working...
live('click',function());
});

Resources