How does alpineJS re-reder a template on data update? - alpine.js

How does AlpineJS re-render a template ?
<script>let contacts = [];</script>
<div x-data="{ persons: contacts }">
<template x-for=" person in persons ">
<div x-text=" person "></div>
</template>
<button x-on:click="contacts.push('Jack'); persons = contacts; console.log(persons.length);">Click</button>
</div>
I was expecting the div to have multiple text of Jack on click.

In your case/example contacts are out of Alpine's scope, once the component is initiated it's in it's own bubble, anything outside out the component scope x-data won't trigger a re-render.
If you change contacts.push('Jack'); to persons.push('Jack');, that will trigger a re-render.

Maybe it's not the most efficient way but I've had a similar problem, to resolve it I used the get function inspired from this.
My use case is the following:
JS:
get updatedState() {
var config_state = [];
#update your data here
return config_state;
}
HTML:
<template x-for="config in updatedState">
#Do what you want here
</template>
Each time your data changes, your loop will update the data

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>

Using data on dispatched event and trigger div

I have a component that dispatches a browser event with an object
// Livewire Component Method
public function passToDashboard($dataId)
{
$data = Model::find($dataId);
$this->dispatchBrowserEvent('show-data', ['data' => $data]);
}
Now on my dashboard blade view i've got
<div class="some-classes" x-data="{dataDisplay:false}">
<div x-show="dataDisplay">
{{-- This is where i want to use the object --}}
{{ $data->title }}
</div>
</div>
<script>
window.addEventListener('show-data', data => {
console.log(data.detail.title); // outputs title just fine
})
</script>
The question is, how to 'unhide' the dataDisplay and how to show it with the passed data? Thanks!
You can listen for these events directly on the element using #event-name.window="dataDisplay = true"
To get the event data, you use the $event variable and it should be under $event.detail.data.title
Use x-text to get the text onto the element. See my full example below.
So in your case:
<div class="some-classes" x-data="{dataDisplay:false, title: ''}" #event-data.window="dataDisplay = true; title = $event.detail.data.title">
<div x-show="dataDisplay">
<h3 x-text="title"></h3>
</div>
</div>
The documentation for this can be found here: https://laravel-livewire.com/docs/2.x/events#browser
Do notice I changed the event name, because it does apparently not work if you start the event name with "show". When I changed to "event-data" or anything else it started working again.

Dynamics charts using chartist and angular with ng-repeate

I now and I can show a chart with dynamic data but a fixed number of chart. When I want to show a dynamic number of charts, something happens with ng-repeate. I said something happens, because if in mycharts[0].container.outerHTML I had the html that I need to show the graph (generated by the library), and if I copy and paste in a fixed place in my html, it will show the graph. My ng-repeate code looks as follow:
<div class="row" ng-controller="nodesDataTablesCtrl as nodeCtrl" >
<div ng-repeat="(index, node) in nodeCtrl.nodes" on-finish-render>
<div class="row">
<div class="col-lg-12">
<div id="{{ node.name }}_MEMORY" class="ct-chart snp_dynamic_chart"></div>
</div>
</div>
</div>
</div>
I found a solution, I'm not sure if is a bug of ng-repeate or Chartist.
In my ng-repeate I use nodeCtrl.nodes, which is an array that I receive from an http.get, my solution was create an range function, wich is, a function that receive a Number and return a list from 0 to n-1. Instead of passing the nodeCtrl.nodes which is updated everytime I make a request, I update a variable numberOfNodes with the range function, I mean, my new ng-repeat will be as follow:
<div ng-repeat="(index, node) in nodeCtrl.numberOfNodes" on-finish-render>
and in the success function of my request I do:
numberOfNodes = range(nodeCtrl.nodes.length)
which from my point of view make that ng-repeate don't update in some way internally.
Is important to se that programatically it shouldn't be differen but ...

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);
});

Vue.js Calling function on a rendered component

I want to create an interactive scrumboard using Laravel and Vue.js containing multiple columns and within those columns multiple tickets.
These tickets are vue components with some nice edit / delete / (un)assign developer functionality and is used on other pages as well.
I have multiple columns defined like this:
<div id="scrumboard">
<div class="scrumboard__column">
<div class="scrumboard__title">Open</div>
<div class="scrumboard__tickets_wrapper" data-status="open">
#if( $sprint->hasTicketsOfStatus("open") )
#foreach( $sprint->getTicketsByStatus("open") as $ticket )
<ticket :data="{{ $ticket->getJsonData(true) }}"></ticket>
#endforeach
#endif
</div>
</div>
<div class="scrumboard__column">
<div class="scrumboard__title">In progress</div>
<div class="scrumboard__tickets_wrapper" data-status="progress">
#if( $sprint->hasTicketsOfStatus("progress") )
#foreach( $sprint->getTicketsByStatus("progress") as $ticket )
<ticket :data="{{ $ticket->getJsonData(true) }}"></ticket>
#endforeach
#endif
</div>
</div>
<div class="scrumboard__column">
<div class="scrumboard__title">Finished</div>
<div class="scrumboard__tickets_wrapper" data-status="closed">
#if( $sprint->hasTicketsOfStatus("closed") )
#foreach( $sprint->getTicketsByStatus("closed") as $ticket )
<ticket :data="{{ $ticket->getJsonData(true) }}"></ticket>
#endforeach
#endif
</div>
</div>
</div>
And as you can see it renders a ticket component for each ticket it finds for each column.
No i have turned the scrumboard__tickets_wrapper div's into jquery ui sortable lists which allows you to swap the tickets between columns.
<script>
$(document).ready(function(){
$(".scrumboard__tickets_wrapper").sortable({
connectWith: '.scrumboard__tickets_wrapper',
receive: function(event, ui){
console.log("Switched columns");
console.log(event);
console.log(ui);
var target = $(event.target);
target.css("background-color", "#ff0000");
}
});
</script>
Everything is working so far, now my question is: how do I dynamically call the "updateStatus()" function on a ticket component once the ticket is dropped into another list?
As you can see I can get the specific element being dropped and the sortable list it's been dropped into. So I know what the new status is by grabbing the data-status property of the wrapper + I know which element was dropped.
But how do I grab the instance of the ticket component in question and call the updateStatus function to save the new status?
Thanks in advance!
Screenshot of the scrumboard
Thanks David for pointing me in the right direction. The solution to my problem was proper component nesting.
The solution was to create 3 components with proper child-component inheritence. And declaring the child-components within the template of it's parent.
This way I end up only declaring "" and the magic happens :D.
So I have made 3 components:
- scrumboard > takes scrumboardColumn as component
- scrumboardColumn > takes ticket as component
- ticket
The root vue instance also loads the ticket component since the ticket component is also used on the backlog page.
I haven't completely finished the final product but I got the sortable working by calling it from within the ready function of the scrumboardColumn component like David suggested.
Hope this helps someone in the future!

Resources