Svelte {#if variable} block does not react to variable updates within the block - html-table

I would like to populate a table with visible rows in Svelte.
My current attempt relies on a {#if variable} test, where the rendered row updates the variable. Unfortunately, the test does not appear to react to changes to the variable. Perhaps this is as designed but the documentation does not appear to address this. Essentially:
<table>
<tbody>
{#each rows as row}
{#if renderIt==true}
<tr use:updateRenderIt>
<td>cell</td>
</tr>
{/if}
{/each}
</tbody>
</table>
I think my understanding of the timing is lacking :(. Perhaps the {#if} block cannot react to each renderIt change. There are quite a few examples of {#if} blocks, but none appear to rely on a variable which is changed within the block.
There is a running example in the Svelte playground. The console divider can be moved vertically to change the viewport dimensions.
If someone knows of a way to achieve this it would be appreciated! I can do it in traditional Javascript, but my Svelte expertise is limited :).

What I'm assuming you want is to have a state on each row when it is visible.
To do so you will need to store some data with your row, so instead of your row being a list of numbers and a single boolean to say if all rows are visible or not, it will be a list of objets that have a property visible:
let rows = [];
for (let i = 0; i < 100; i++) {
rows.push({
index: i,
visible: false,
});
};
Next, to capture when visibility changes on those rows, use Intersection Observer API:
let observer = new IntersectionObserver(
(entries, observer) => {
console.log(entries);
}
);
And use a svelte action to add that observer to elements:
<script>
...
let intersect = (element) => {
observer.observe(element);
};
</script>
<table>
<tbody>
{#each rows as row (row.index)}
<tr
use:intersect>
<td>{row.visible}</td>
</tr>
{/each}
</tbody>
</table>
To pass the intersecting state back to the element throw a custom event on it:
let observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
entry.target.dispatchEvent(
new CustomEvent("intersect", { detail: entry.isIntersecting })
);
});
}
);
And finally capture that event and modify the state:
<tr use:intersect
on:intersect={(event) => (row.visible = event.detail)} >
<td>{row.visible}</td>
</tr>
To render rows up to how many can fit on screen you could make the defaut state visible: true, and then wrap the element with an {#if row.visible}<tr .... </tr>{/if}. After the first event you would then remove the observer from the element using observer.unobserve by either updating the svelte action or in the observer hook.

Related

Vuetify: How to show overlay with spinner while v-data-table renders (how to wait for a component to finish rendering)

I have a Vuetify v-data-table with about a 1000 rows in it. Rendering it for the first time and when text searching on it, the render takes a few seconds. During this time I want to overlay it with a scrim + spinner to indicate things are happening.
The overlay:
<v-overlay
:value="loading"
opacity="0.7"
absolute
v-mutate="onMutate"
>
<v-progress-circular indeterminate size="32" color="blue"></v-progress-circular>
</v-overlay>
The v-data-table:
<v-data-table
:headers="cHeaders"
:items="cLimitedData"
disable-pagination
hide-default-footer
:search="searchIntercepted"
#current-items="tstCurrentItems"
>
The computed variable cLimitedData:
cLimitedData() {
if (this.indNoLimit) {
return this.data
} else {
return this.data.slice(0,this.dtRowTo)
}
},
I watch the search variable, and when it changes, I set loading to true to activate the overlay.
watch: {
search() {
if (!this.indNoLimit) {
// remove limit, this will cause cLimitedData to return all rows
this.loading = true
// -> moved to onMutate
//this.$nextTick(function () {
// this.indNoLimit = true
//})
} else {
this.searchIntercepted = this.search
}
},
However, the overlay doesn't activate until after the v-data-table had finished rendering. I've tried a million things, one of them is to put a v-mutate="onMutate" on the overlay, and only when it fired, would I this.indNoLimit = true to set things in motion, but that is still not good enough to have the scrim start before the v-data-table begins reloading itself.
onMutate(thing1) {
console.log('#onMutate',thing1)
this.$nextTick(function () {
this.indNoLimit = true
this.searchIntercepted = this.search
})
},
I also found that the next tick in #current-items fairly reliably marked the end of the render of the v-data-table, thus the deactivation of the scrim is probably going to be ok:
tstCurrentItems(thing1) {
console.log('#current-items',thing1)
this.$nextTick(function () {
console.log('#current-items NEXT')
this.loading = false
})
I believe my question should be: how can I detect/wait for components to have rendered (the v-overlay+v-progress-circular) before making changes to other components (the v-data-table).
Note: To solve the initial wait time of loading of the table, I found a way to progressively load it by inserting div-markers that trigger a v-intersect. However, this does not solve the situation when a user searches the whole data set when only the first 50 rows are loaded.
EDIT: Tried to start the update of the table after the overlay has been activated using https://github.com/twickstrom/vue-force-next-tick, but still no luck. It almost looks like vue tries to aggregate changes to the DOM instead of executing them one by one.
I have not figured out why the v-data-table seems to block/freeze, but here is a solution that can streamline any large v-data-table:
The v-data-table:
<v-data-table
:headers="cHeaders"
:items="data"
:items-per-page="dtRowTo"
hide-default-footer
hide-default-header
:search="search"
item-key="id"
>
<template
v-slot:item="{item, index, isSelected, select, headers}"
>
<tr>
<td
:colspan="headers.length"
>
Stuff
<div v-if="(index+1)%25==0" v-intersect.quiet.once="onIntersect">{{index+1}}</div>
</td>
</tr>
</template>
</v-data-table>
Add a div, or any other element, to intersect with at given intervals. Every time we intersect with it, we're going to increase the page size.
Variables:
dtRowTo: 50, // initial nr of rows
dtRowIncr: 50, // increments
The onIntersect:
onIntersect (entries, observer) {
let index = entries[0].target.textContent
if (index == this.dtRowTo) {
this.dtRowTo += this.dtRowIncr
}
},
Unfortunately you cannot add the index as an argument like v-intersect.quiet.once="onIntersect(index)", as this executes the function before you intersect with it (not sure why), so we will have to take the index out of the textContext.
Basically what you do is you increase the page size every time you're at the bottom. Search will still operate on the whole dataset.
What does not work (which I found out the hard way), is incrementally increasing the items, like the computed function below. As search needs the entire dataset to be present at :items, this won't work:
<v-data-table
:items="cLimitedData"
computed: {
cLimitedData() {
return this.data.slice(0,this.dtRowTo)
},
}
Doing so is fine (I guess?) as long as you don't need search or anything that operates on the entire data set.

Changing complex computed object in vue.js

I have complicated object for a table. Looks like this:
{
1510002000: {
date: "07.11.17"
hours: {
1510002000:{
activity:{
activity: "Тест",
color: "#00ff00",
end_at: 1510005600,
start_at: 1510002000,
type_id: 1
}
},
1510005600: {
...
},
...
}
},
....
}
This is a code from template that uses this object:
<tr v-for="date in this.tds">
<td>{{ date.date }}</td>
<td is="hour-td"
v-for="hour in date.hours"
:color="hour.activity.color"
:start_at="hour.activity.start_at"
:end_at="hour.activity.end_at"
:activity="hour.activity.activity"
:type_id="hour.activity.type_id"
>
</td>
</tr>
I evaluated it as a computed property, but I need to rerender table when parent component provides data assync, so I have a watcher for prop (prop called "activities"):
watch: {
activities: function(){
var vm = this;
let dth = new DateTimeHelper;
if (this.activities.length > 0){
this.activities.forEach(function(activity){
let dateTimestamp = dth.getDateTimestampFromTimestamp(activity.start_at); // just getting the key
if (vm.tds[dateTimestamp]){
if (vm.tds[dateTimestamp].hours[activity.start_at]){
vm.tds[dateTimestamp].hours[activity.start_at].activity.activity = activity.activity;
vm.tds[dateTimestamp].hours[activity.start_at].activity.color = activity.color;
vm.tds[dateTimestamp].hours[activity.start_at].activity.type_id = activity.type_id;
}
}
});
}
console.log(vm.tds) // here I can see that the object is filled with new data
}
},
The problem is that the table doesn't rerender. More precisely, the component "hour-td" does not contain the new data.
Also I've tried to use Vue.set, but no success with that
Can you help me with the updating table? I've spent like 5 hours for refactoring and attempts.
Thanks in advance
SOLUTION
In my case there can be two states: there are activities, there are no activities. So I made two computed props for each case and render them separately and switch by v-if="activities.length"
I think that your problem is with Vue known issue for Change Detection Caveats (you can read here) with array direct assignation, that don't detect changes.
You should change this part of code (with direct array assignation):
if (vm.tds[dateTimestamp]){
if (vm.tds[dateTimestamp].hours[activity.start_at]){
vm.tds[dateTimestamp].hours[activity.start_at].activity.activity = activity.activity;
vm.tds[dateTimestamp].hours[activity.start_at].activity.color = activity.color;
vm.tds[dateTimestamp].hours[activity.start_at].activity.type_id = activity.type_id;
}
}
With the Vue.set() option for arrays in order to detect the change and re-renders the component. It worked for me in differents occassions:
// Vue.set(object, key, value)
// something like this:
Vue.set(vm.tds[dateTimestamp].hours[activity.start_at].activity.activity,1,activity.activity);
More info here: https://codingexplained.com/coding/front-end/vue-js/array-change-detection
Edit:
I see now that you said:
Also I've tried to use Vue.set, but no success with that
What you mean with: "no success" ? Can you share the code? I have the same issue and I resolved with Vue.set..
You can also take a look to vm.$forceUpdate(), try to execute after the last console.log or grab all the code inside a vm.$nextTick( [callback] ) in order to execute all the actions (load data in the table) and then, re-render the component on next tick.
More info here: https://v2.vuejs.org/v2/api/#vm-forceUpdate && https://v2.vuejs.org/v2/api/#Vue-nextTick
Edit 2:
I think that your problem is with the index of the array, you should take a look here: https://v2.vuejs.org/v2/guide/list.html#Array-Change-Detection .
Try changing the:
if (vm.tds[dateTimestamp]){
if (vm.tds[dateTimestamp].hours[activity.start_at]){
vm.tds[dateTimestamp].hours[activity.start_at].activity.activity = activity.activity;
vm.tds[dateTimestamp].hours[activity.start_at].activity.color = activity.color;
vm.tds[dateTimestamp].hours[activity.start_at].activity.type_id = activity.type_id;
}
}
and simplify with:
if (vm.tds[dateTimestamp] && vm.tds[dateTimestamp].hours[activity.start_at]){
Vue.set( vm.tds, vm.tds.indexOf(vm.tds[dateTimestamp].hours[activity.start_at]), activity);
}
Hope it helps!

Aurelia if.bind inside repeat.for is not updating

I have a strange case in an Aurelia template of elements with if.bind inside a repeat.for not being shown/hidden when their underlying property is changed. With the following code, the edit fields should be shown and the edit button should be hidden as soon as the edit button is clicked. Subsequently, both the save and undo buttons should hide the edit fields and show the edit buttons again.
MyList.ts:
import { computedFrom } from "aurelia-binding";
export class MyList
{
items: any[] = [{
"firstName": "Joe",
"lastName" : "Blow",
"id": 1
},
{
"firstName": "Jane",
"lastName" : "Doe",
"id": 2
}
]
editingItem: any = null
isEditing: boolean;
edit(item){
this.editingItem = item;
this.isEditing = true;
}
editFirst(item){
this.editingItem = this.items[0];
this.isEditing = true;
}
undo(){
// undo logic here
this.editingItem = null;
this.isEditing = false;
}
save(){
// Save logic here
this.editingItem = null;
this.isEditing = false;
}
}
MyList.html:
<template>
<table>
<tbody>
<tr repeat.for="item of items">
<td if.bind="!isEditing">
<button click.delegate="edit(item)">Edit</button>
</td>
<td>${item.firstName}</td>
<td>${item.lastName}</td>
<td if.bind="isEditing && editingItem.id == item.id">
<button click.delegate="save()">Save</button>
<button click.delegate="undo()">Undo</button>
<input value.bind="editingItem.firstName" />
<input value.bind="editingItem.lastName" />
</td>
</tr>
</tbody>
</table>
</template>
Clicking the edit button does nothing. Interestingly, if I add
${isEditing}
Anywhere in the template outside the repeat.for, the code works as expected. It's as if the rendering engine doesn't know to re-render elements inside the repeat loop.
Is this a bug? Or am I doing something silly?
This is weird. I created a gist from your code and as you said it wasn't working. No errors in the console either.
But when I initialized isEditing it started working
isEditing: boolean = false;
Updated gist
From #Balázs in the comments: By not initializing the isEditing, that property virtually does not exist -- it can be used for autocompletion/IntelliSense, but it is not actually present on the object. You can verify that by replacing the console.log call in the edit method with console.log(this.editing);, you will see that it is undefined. By it being undefined, Aurelia cannot subscribe to it (because it is as though it wasn't even there - no getter/setter exists), and therefore has no way of knowing when, if ever, it comes to life. However, that even explicitly setting it to undefined is different from this, because that does actually create the property, whose value happens to be set to undefined.
Also note:
Since you are assigning editingItem directly with item here:
edit(item){
this.editingItem = item;
this.isEditing = true;
}
Whatever you change in editingItem will affect item as well. So even if you undo, the change will persist to item. So you should do a clone before assigning to editingItem.

Aurelia ui-virtualization infinite-scroll-next function not being hit after data refresh

I'm using aurelia's ui-virtualization library to create a table using virtual-repeat and infinite-scroll-next. Html looks something like this:
<table>
<thead>
...
</thead>
<tbody>
<tr virtual-repeat.for="item of items" infinite-scroll-next="getMore">
...
</tr>
</tbody>
</table>
This works great, except I have certain ui components and interactions that update what is in my list array. Once that has be updated, the infinite-scroll-next function (getMore()) is never called. I update something like this:
update() {
let vm = this;
vm.apiService.getData(vm.filterOption)
.then(response => {
vm.items = response.content.items;
});
}
Where filterOptions are changed through the ui and apiService.getData() returns a promise from an http call. The data in the table updates correctly, but the infinite scroll is then broken.
Am I doing something wrong, or is this a bug in ui-virtualization?
Edit:
It appears there are some properties added to the array __array_observer__ and __observers__. Thinking overwriting the whole array and thus removing these properties might be causing the problem, I tried an approach where I just add or remove elements appropriately. This does not work either.
Edit:
It seems to fail if I leave fewer than 7 of the original elements in the array.

Using jQuery DataTables with Aurelia

I need some advice regarding the usage of jQuery DataTables with Aurelia. Basically I'm running into two problems.
I can't determine the best way to initialize it AFTER the repeat.for binding loop has completed. Apparently that loop is still working even after the attached() lifecycle is fired.
If I use $(myRef).DataTables(data: myArray) method to populate the table, and insert links (<a href-route=... click.delegate=...>) into that table, Aurelia doesn't seem to recognize the links or activate the router.
Problem 1: Here's my attempt to populate the table using Aurelia's binding. Aurelia correctly makes the table, and I can just wait 2-3 seconds and then load DataTables, but that's not the right way. I don't have a definitive event to trigger the loading of the DataTables class because I don't know when repeat.for is completed.
<div class="table-responsive">
<table ref="tblUserList" class="table table-condensed table-hover table-striped" width="100%">
<thead>
<tr>
<th><span t="Username"></span></th>
<th><span t="First_name"></span></th>
<th><span t="Last_name"></span></th>
</tr>
</thead>
<tbody>
<tr repeat.for="record of records">
<td><a route-href="route: user; params.bind: {id: record.user_id}" click.delegate="$parent.select(record)">${record.user_username}</a></td>
<td>${record.p_fname}</td>
<td>${record.p_lname}</td>
</tr>
</tbody>
</table>
</div>
Problem 2: Here's my attempt to populate the table using the jQuery method. DataTables successfully loads the table, but Aurelia doesn't recognize the links or trigger action.
$(this.tblUserList).dataTable({
"paginate": true,
"pageLength": 10,
data: this.records,
columns: [
{ data: 'user_username',
fnCreatedCell: function (nTd, sData, oData, iRow, iCol) {
$(nTd).html("<a route-href='route: user; params.bind: {id:" + oData.user_id + "}' click.delegate='$parent.select(" + oData.user_id + ")'>" + oData.user_username + "</a>");
}
},
{ data: 'p_fname' },
{ data: 'p_lname' }
]
});
Can anyone help me solve any one of the above problems? Or... am I approaching this whole issue the wrong way? Is it better to use the jQuery method to populate, or the Aurelia repeat.for binding loop?
I know this is old, but just in case, if it can help.
When you add DOM elements after binding, they are not aureliazed. You should use the enhance method of TemplatingEngine:
import the TemplatingEngine and inject it:
import {inject, TemplatingEngine} from 'aurelia-framework';
#inject(TemplatingEngine)
In the constructor, initialize the template engine:
constructor(templatingEngine) {
this.templatingEngine = templatingEngine;
}
in Aurelia's attached() method, do your datatable init stuff, and add a class to be able to retrieve your newly created DOM elements:
$(nTd).html("<a class='myclass' ...
Enhance your elements:
$('.myclass').each(function (index, value) {
let el = $(this);
if (!el.hasClass('au-target')) { //can't enhance already aureliazed elm
this.templatingEngine.enhance({element: el[0], bindingContext: this});
//el[0] is important to have DOM and not jQuery object
}
});
Then your binding should work.
Using the first approach (aurelia binding), remove data from the config object and load your data in the activate lifecycle hook:
import 'datatables';
export class UserList {
activate() {
this.records = [...];
}
attached() {
$(this.tblUserList).dataTable();
}
}