How to use interval in alpine js? - alpine.js

I am trying to add a timer to my cards. So, what I do is. I receive data from DB and pass the data to the records[] array and then I show the data on frontend.
<template x-for="record in records">
<span x-text="record.created_at">2022-10-31 18:41:20</span>
</template>
But what I want is to show the seconds between created at to now time and the seconds should be changed as time passes kind of like how much time is passed since the record is created.
Actually, in my case, I need to change the text within the loop. I tried something like this
https://cdn.jsdelivr.net/gh/kevinbatdorf/alpine-magic-helpers#latest/dist/interval.js
<template x-for="record in records">
<span x-text="$interval(getTimer(record), 1000)">2022-10-31 18:41:20</span>
</template>
<script type="text/javascript">
function dateComponent() {
return {
init: function() {
// fetch data
},
getTimer: function(record) {
console.log(record)
}
}
}
</script>
But this runs the interval only once. Is this possible in alpine js. and if yes, please guide me to where I am doing wrong.

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>

Best practice for accessing parent query object from child components

I have a "event" page that runs an Apollo query for an event with a given ID. There are multiple child components that use the event object to access various properties. I am struggling to find the best pattern for making that object available to the child components. Do I want to have additional queries in the child components that just access the cache exclusively? Or is there a way to have the components defer until the query is complete.
In general, I only run queries from "page" components and pass data down as props to lower level components:
So I would do something like
/pages/EventPage.vue
<template>
<div>
<EventDetails :event="event">
</div>
</template>
<script>
import EventDetails from '/wherever/my/component/is/EventDetails.vue'
import MY_EVENT_QUERY from '/wherever/my/query/is/EventQuery.gql'
export default {
apollo: {
event: {
query: MY_EVENT_QUERY
}
}
}
</script>
/components/EventDetails.vue
<template>
<div>
{{event.name}}
</div>
</template>
<script>
export default {
props: {
event: {
type: Object,
default: { name: '' }
}
}
}
</script>
There might be rare times where I would have a sub-component fetch its own data. I'm thinking of a form where you have select box that needs to load the list of dropdown items or something. But I would not re-query the same event item over and over again.

Laravel + Bootstrap-Vue Table not accepting data

I'm currently playing with Laravel Spark and I'm slowly learning the Vue.js system.
I have a bunch of data I want to display in a table that is pulled from AWS DynamoDB. I am successfully parsing this data in sorts of ways and can display the data in a standard static Bootstrap table. I'm now trying to use the Vue.js version and I cannot for the life of me get this data to display at all. If I insert dummy data into the Vue Component, the dummy data shows so it must be the way I'm passing the data in.
My code as follows:
TableController.php
public function show()
{
$data = $this->fetchAWSData($condition); // This is my separate AWS method
return view('table')->with('items', $data);
}
table.blade.php
#extends('spark::layouts.app')
#section('content')
<home :user="user" inline-template>
<div class="container-fluid" style="text-align: left">
<h1>Data</h1>
<MyTable items={{ $items }}></MyTable>
</div>
</home>
#endsection
MyTable.vue
<template>
<b-table striped hover :items=items></b-table>
</template>
<script>
export default {
data() {
return {
items: this.items
}
}
}
</script>
What am I doing wrong here? I've tried formatting my data all sorts of ways; JSON, manually, Arrays... nothing works. So it must be the way I'm passing it in.
Any insight would be AMAZING :)
You have to use props to be able to pass attributes to Vue's components.
MyTable.vue
...
<script>
export default {
props: ['items'],
}
</script>
Then you can pass data to component:
<MyTable :items="{{ $items }}">

Subscription reactive on Session changes causes #each to redraw every entity

So here's the catch: I store the user's coordinates using this neat solution. Here is my implementation:
updateLoc = function () {
var position = Geolocation.latLng() || {lat:0,lng:0};
Session.set('lat', position.lat);
Session.set('lon', position.lng);
};
Meteor.startup(function() {
updateLoc(); // set at 0, 0 to begin with
Meteor.setTimeout(updateLoc, 1000); // get first coordinates 1 second in
Meteor.setInterval(updateLoc, 5000); // then, every 5 seconds
});
I have an entitiesList route waiting on entities to be subscribed, according to those two session variables:
this.route('entitiesList', {
path: '/',
waitOn: function() {
if (Meteor.userId())
return Meteor.subscribe('entities', {lat: Session.get('lat'),lon: Session.get('lon')});
},
data: function() {
return {entities: Entities.find()};
}
});
Here is the publication:
Meteor.publish('entities', function (position) {
if (position.lon !== null && position.lat !== null) {
return Entities.find({location: {
$near: {$geometry:{type: "Point", coordinates: [position.lon, position.lat]},$maxDistance:500}}
}});
}
this.ready();
});
Finally, the entitiesList template :
<template name="entitiesList">
<div class="entities">
<h1>Entities list</h1>
{{#each entities}}
{{> entityItem}}
{{else}}
<p>No entity found. Looking up...</p>
{{>spinner}}
{{/each}}
</div>
</template>
Now! This solution works. Entities are listed correctly, updated every 5 seconds according to the user's location.
The only issue lies in rendering: when the reactivity is due to an update of Session variables, the entire set of entities is deleted and redrawn. But when a change occurs in the Entity Collection (say, an entity gets deleted / created) only this change is re-rendered accordingly in the template.
What this produces is a list that flashes very annoyingly every 5 seconds. I thought of removing the #each block and sort of write it myself using this.autorun() in the rendered function of the template, and to redraw the list in a more optimized fashion using jQuery, but it would be an obnoxious hack, with HTML chunks of code outside of the template files... Surely there's gotta be another way!
Each time you change your session variables, your subscription is loading and Iron Router sets his loading template and that's why it's flickering.
Instead of using iron-router you could do:
Template.entitiesList.created=function()
{
var self=this
this.isLoading=new ReactiveVar(false)
this.isFirstLoading=new ReactiveVar(true)
this.autorun(function(){
self.isLoading.set(true)
Meteor.subscribe('entities', {lat: Session.get('lat'),lon: Session.get('lon')},function(err){
self.isLoading.set(false)
self.isFirstLoading.set(false)
});
})
}
Template.entitiesList.helpers({
entities:function(){return Entities.find()}
isLoading:function(){Template.instance().isLoading.get()
isFirstLoading:function(){Template.instance().isFirstLoading.get()
})
<template name="entitiesList">
<div class="entities">
<h1>Entities list</h1>
{{#if isFirstLoading}}
<p>Looking up...<p/>
{{>spinner}}
{{else}}
{{#each entities}}
{{> entityItem}}
{{else}}
<p>No entity found</p>
{{/each}}
{{#if isLoading}}
{{>spinner}}
{{/if}}
{{/if}}
</div>
</template>
Fiddling through with iron-router, I found that there actually is an option to not render the loading template on every new subscription triggered: the subscriptions option. Just had to replace waitOn by subscriptions and I get the desired result.

Calling a function on a table of async populted data in Meteor

I am using Meteor and the jquery data table library.
I have a table that I am loading meteor/handlebars using something like this:
<table>
{{each x}}
// code to insert rows of data
{{/each}}
</table>
I need to call this on the table once it has been fully populated with data to turn it into a sortable table:
$('#tableID').dataTable();
It works when I call it from the console once the DOM is fully loaded and the data is in there, but using Template.rendered() doesn't work, nor does listening for changes with .observe since the data is loaded before that particular view is rendered.
Where can I run this code to ensure that the data is fully loaded, and if there are updates to the data in the table that it will run again?
I found a way of doing it which seems to work after I split the individual rows into templates - will update as I continue to debug it (and this is certainly not ideal).
Template.individualRow.rendered = function() {
if (!$('#tableID').hasClass('initialized')) {
$('#tableID').not('.initialized').addClass('initialized').dataTable();
};
};
It beats me, but I can suggest a way to work around:
Do not use handlbars' helper, and observe the data, when the data changed, re-render the template manually, like this:
<table id="tableID"></table>
<template name="yourTemplate">
{{#each this}}
<tr>....</tr>
{{/each}}
</template>
var query = ...
query.observe({
added: function () {
manuallyRender();
},
changed: function () {
manuallyRender();
},
...
});
function manuallyRender() {
// don't need Meteor.render here, because you don't need it to be reactive.
var frag = $(Template.yourTemplate(query.fetch()));
$('#tableID').empty().append(frag).dataTable();
}
It should work, but maybe not the best way, any thoughts?
Actually, I think meteor has a long way to go...

Resources