I'm trying to throttle the execution of a function in Svelte. However, using throttle within an auto-subscription seems to break it. Consider the following example (REPL):
<script>
import throttle from 'lodash.throttle';
import { writable } from 'svelte/store';
const arr = writable(['A', 'B', 'C']);
function foo(val) {
console.log('foo: ', val);
}
</script>
{#each $arr as a}
<button on:click={throttle(() => { foo(a); }, 1000)}>
Button {a}
</button>
{/each}
The execution of foo(a) is not throttled at all. If you remove the {#each $arr as a} block and just pass a string to foo, you'll see that it works as intended.
I'm assuming this has to do with the event loop and how Svelte auto-subscriptions work but don't know the exact reason. Wondering if anyone knows a) why this is happening and b) what a possible solution could look like?
If you look at the code Svelte generates for this, you can see that it is regenerating the throttled function on every click when you pass a store value. This resets the throttle timer on every click.
dispose = listen(button, "click", function () {
if (is_function(throttle(click_handler, 1000)))
throttle(click_handler, 1000).apply(this, arguments);
});
For whatever reason, this does not happen when you pass a regular string.
dispose = listen(button, "click", throttle(click_handler, 1000));
This may be a bug in Svelte, but I'm not sure. It might be worth opening an issue on the GitHub repo.
I was able to work around it by generating the throttled functions ahead of time:
<script>
import throttle from 'lodash.throttle';
import { writable } from 'svelte/store';
const arr = writable(['A', 'B', 'C']);
function foo(val) {
console.log('foo: ', val);
}
$: throttledFns = $arr.map(val => getThrottled(val));
function getThrottled(val) {
console.log('getting throttled');
return throttle(() => { foo(val); }, 1000);
}
</script>
{#each $arr as a, idx}
<button on:click={throttledFns[idx]}>
Button {a}
</button>
{/each}
This will regenerate the throttled functions when the store array changes, but not on every click.
You can also generate a throttled version of foo once and use that, but that will throttle all clicks to the buttons (e.g. if you click A and then click B, the click to B will be throttled).
<script>
// as before
const throttledFoo = throttle(foo, 1000);
</script>
{#each $arr as a, idx}
<button on:click={() => throttledFoo(a)}, 1000)}>
Button {a}
</button>
{/each}
Related
I'm learning Svelte after having used Vue for a while, but am a bit confused by some strange reactivity issues with Svelte.
I have this simple code:
Svelte Snippet
<script>
let count = 0
$: tripleCount = count * 3
const increaseCount = () => count++
$: if (tripleCount > 6) count = 0
</script>
<button on:click={increaseCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>
Triple count is: {tripleCount}
</p>
But, when I try to run it on Svelte's playground, I get an error message alerting me that there is a Cyclical dependency detected: tripleCount → count → tripleCount.
I've found a few ways to fix that issue and get the Svelte component to work as intended. But, I'm curious about why I get that issue, given that there isn't any logical loop that'd be impossible to close. The equivalent code in Vue works perfectly fine.
Vue Equivalent Snippet
<script setup>
import { computed, ref, watchEffect } from 'vue'
const count = ref(0)
const tripleCount = computed(() => count.value * 3)
const incrementCount = () => count.value++
watchEffect(() => {
if (tripleCount.value > 6) count.value = 0
})
</script>
<template>
<button #click="incrementCount">
Clicked {{ count }} {{ count === 1 ? 'time' : 'times' }}
</button>
<p>Triple count is: {{ tripleCount }}</p>
</template>
Live Demo of Expected Behavior
You can ignore the code in the snippet below since it's not too relevant to my question and is already described more simply above.
Just run the snippet below to see what I'm trying to get my Svelte component to do.
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="app"></div>
<script>
const { createApp, ref, computed, watchEffect } = Vue
createApp({
setup() {
const count = ref(0)
const tripleCount = computed(() => count.value * 3)
const incrementCount = () => count.value++
watchEffect(() => {
if (tripleCount.value > 6) count.value = 0
})
return {
count,
tripleCount,
incrementCount,
}
},
template: `
<button #click="incrementCount">
Clicked {{ count }} {{ count === 1 ? 'time' : 'times' }}
</button>
<p>Triple count is: {{ tripleCount }}</p>
`,
}).mount('#app')
</script>
Possible Solutions and New Issues
Approach A: Indirect Update
An interesting workaround to avoid getting the Cyclical dependency error is to change count indirectly, through an intermediary resetCount function.
<script>
// $: if (tripleCount > 6) count = 0
const resetCount = () => count = 0
$: if ($tripleCount > 6) resetCount()
</script>
Logically, this implementation is no different from the original, but somehow the Cyclical dependency error goes away.
However, some new unexpected behavior arises. When tripleCount is greater than 6 and count is therefore reset to 0, tripleCount does not update. It retains its last value (9 in this case), and its reactivity doesn't reactivate until the next click of the button.
Why does tripleCount not react to the change of count when count is reset?
Approach B: Indirect Update + Delay
If I add a delay before the reset helper sets count to 0, the code will work as its Vue equivalent, with the correct behavior.
<script>
const resetCount = () => {
setTimeout(() => count = 0, 0) // zero ms delay
}
$: if ($tripleCount > 6) resetCount()
</script>
Why does this delay help tripleCount react to the change in count? I guess using setTimeout is helping Svelte exit that event loop and only then handle the reactivity of the change. But, it seems quite error-prone to me that I have to be mindful of not forgetting to add these delays for a supposedly computed value to be able to pick up on all the changes of the value it's dependent on.
Is there a better way of making sure tripleCount reacts to the resetting of count?
As can be seen in the Vue implementation, I didn't need to use any indirect resetCount auxiliary function nor setTimeout hack to get the expected behavior.
Approach C: Stores
After further experimentation, I managed to get my Svelte component to work as intended by using stores like this:
<script>
import { writable, derived } from 'svelte/store';
const count = writable(0);
const tripleCount = derived(count, $count => $count * 3);
function incrementCount() {
count.update(n => n + 1);
}
$: if ($tripleCount > 6) {
count.set(0);
}
</script>
<button on:click={incrementCount}>
Clicked {$count} {$count === 1 ? 'time' : 'times'}
</button>
<p>Triple count is: {$tripleCount}</p>
Would this be the recommended way to work with reactivity in Svelte? I am a bit sad because it takes away much of the simplicity that made me want to learn Svelte. I know all frameworks rely on stores for state management at some point, but it seems overkill that I need to use them to implement logic as basic as the one I was trying to implement.
I'd be extremely grateful for any guidance or feedback on the issues I presented here. Thank you so much for reading and for any help <3! Hoping I can understand how reactivity in Svelte works better ^^
PS: Summary of Questions
Why does the intuitive way ($: if (tripleCount > 6) count = 0) not work?
Why does the indirect reset trick ($: if (tripleCount > 6) resetCount() avoid the Cyclical dependency error?
Why does the immediate delay trick (setTimeout(() => count = 0, 0)) ensure tripleCount does update after count is reset?
Is there a way to get the expected behavior in Svelte without an auxiliary resetCount function, nor a setTimeout hack, nor by resorting to using Svelte Stores yet?
If at all possible write it like this 4️⃣:
<script>
let count = 0
$: tripleCount = count * 3
const increaseCount = () => {
count++;
if (count * 3 > 6) count = 0;
}
</script>
<button on:click={increaseCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>
Triple count is: {tripleCount}
</p>
The preferred and most performant way is to make all state changes synchronously and then let Svelte update the view in one go (not relying on derived data and calculating the tripleCount inline).
You don't have use an event and can react to a change in data (like in your workarounds) but using an event prevents the chance of creating an infinite loop.
If want to use a value based on a calculation that happens after a reactivity update, you can use the tick()
( Use sparingly because that might cause a trickle effect, where you'll need more and more ticks )
import { tick } from "svelte";
const increaseCount = async () => {
count++;
await tick();
if (tripleCount > 6) count = 0
}
or when reacting to data:
const resetCount = () => tick().then(() => count = 0)
How svelte works 1️⃣
Every assignment in a reactive block is augmented by the compiler with an invalide() call.
This invalidate marks the variable as dirty and schedules an update unless there is already an update planned.
The value of the variables are updated instantly, they are regular javascript variables.
After your code ran, then the update starts.
explained in pseudo code
$: tripleCount = count * 3
<div>{count === 1 ? 'time' : 'times'}</div>
becomes
function updateData(changes) {
if (hasCountChanged(changes)) {
tripleCount = count * 3
}
}
function updateView(changes) {
if (hasTripleCountChanged(changes)) {
updateTheText(count === 1 ? 'time' : 'times')
}
}
This updateData code only runs once per component.
Svelte looks at all the reactive blocks and sorts then based on their dependencies.
It a A changes B and B changes A you've got a cycle and Svelte can't sort them in the correct order. Using the resetCount() the compiler was unable to detect this cycle 2️⃣ and generated:
function resetCount() {
count = 0
invalidate('count')
}
function updateData(changes) {
if (hasCountChanged(changes)) {
tripleCount = count * 3
}
if (hasTripleCountChanged(changes)) {
if (tripleCount > 6) {
resetCount()
}
}
}
Because the ordering is incorrect the tripleCount and count are out of sync.
When using a setTimeout the invalidate runs after the "updateData" completed and the invalidate('count') wil trigger a new update cycle and that one will update the tripleCount 3️⃣
( PS. I'd like to see a reactivity system that runs more than once, I've written an Svelte RFC about it, but in day-to-day writing Svelte components, I rarely encounter this issue )
As Bob clearly pointed out in his answer, a single variable cannot be updated twice in a single update cycle (a tick in Svelte lingo).
That double update requirement becomes obvious when you try to pack your update code into the smallest possible function:
function processUpdate(_count) {
tripleCount = count * 3
if (tripleCount > 6) {
count = 0
tripleCount = 0 // this is the second time tripleCount is updated
}
}
In truth, the only way around your issue is to eliminate the cyclic dependency (as pointed out by Svelte's error message, incidentally) altogether.
This means reducing the reactive statements that cause this cyclical dependency to a single reactive statement based on one of the variables.
We can do this by reusing the minimal update function stated above:
<script>
let count = 0
let tripleCount = 0
$: processUpdate(count)
const processUpdate = (_count) => {
tripleCount = count * 3
if (tripleCount > 6) {
count = 0
tripleCount = 0
}
}
const increaseCount = () => {
count++;
}
</script>
<button on:click={increaseCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>
Triple count is: {tripleCount}
</p>
And this works as you would expect: REPL
I have a such HTML code.
<div id ='pages'>
<div id='wrapper'>1 </div>
<div id='wrapper'>2 </div>
</div>
I am want to find elements count with id wrapper.
I using Cypress. I'm starting to learn Cypress.
If I try:
cy.get('div#wrapper').should('have.length', 2)
I get AssertionError:
CypressError: Timed out retrying: expected 1 to equal 2
As jonrsharpe pointed out, it's invalid HTML to have multiple elements with identical id attribute.
That being said, DOM is quite smart and can recover and work even with invalid HTML. Duplicate-id elements shouldn't cause much trouble.
If you e.g. try doing document.querySelectorAll('#wrapper') it should return list of 2 elements (in your case).
Problem is, Cypress is using jQuery to query the DOM instead of using native DOM methods and I guess jQuery isn't as smart (or it's more pedantic).
That being said, I can't reproduce that error when running:
// succeeds
cy.get('div#wrapper').should('have.length', 2)
Only when querying #wrapper directly (without the preceding div):
// fails
cy.get('#wrapper').should('have.length', 2)
I reckon this is because jQuery uses a heuristic of exiting early when a selector string (#wrapper) contains only a single id (and that's why div#wrapper returns both elements).
Also, your solution in comments (cy.get('#pages') .find('div#wrapper') .should(($div) => { expect($div).to.have.length(2) })), while working, isn't ideal because it won't retry. Let me demonstrate:
In the following code, the 2nd #wrapper will appear in the DOM only after 1 sec.
describe( 'test', () => {
beforeEach(() => {
cy.document().then( doc => {
doc.body.innerHTML = `
<div id='pages'>
<div id='wrapper'>1</div>
</div>
`;
setTimeout(() => {
doc.body.innerHTML = `
<div id='pages'>
<div id='wrapper'>1</div>
<div id='wrapper'>2</div>
</div>
`;
}, 1000 );
});
});
// will fail
it('solution A', () => {
cy.get('#pages') // <- won't be retried
.find('div#wrapper') // <- only this command will be retried
.should( $div => expect($div).to.have.length(2) );
});
// will pass
it('solution B', () => {
cy.get('#pages #wrapper') // <- will be retried and succeed in 1sec
.should( $div => {
expect($div).to.have.length(2);
});
});
// will pass
it('solution C', () => {
cy.get('#pages')
.should($pages => {
// using native DOM querying
expect($pages[0].querySelectorAll('#wrapper').length).to.eq(2);
});
});
});
Thus, you should go with solution similar to B or C.
I have been working on a website that has to function on both desktop and tablets. Part of the website is having three columns and being able to drag orders from column to column. Sometimes on drop, the user has to answer a few questions or change some of the data of that specific order. This happens in a pop-up window that is triggered by an #drop function (for example #drop="approved()". The method approved() then checks the status of the dropped order and shows the pop-up window).
When I am on desktop, everything works just fine. But when I switch to iPad Pro in the developer tools, nothing happens. I implemented Vue Draggable, which says to work with touch devices. In their examples I can't find anything about touch events or adding new handles for touch, so I don't know what to do now.
The dragging works just fine with touch devices, it's just the #drop function that doesn't trigger.
The dropzone (it includes a component that contains the draggables and a lot of if-statements):
<div class="col-md-4 border" #dragover.prevent #drop="approved()">
<Wachtrij class="fullHeight" :data2="opdrachtenData2"></Wachtrij>
</div>
The method:
export default {
methods: {
...
approved() {
console.log("Function approved() is being executed.")
if (this.draggingOrder.status === 5) {
this.popupGekeurd = true;
}
else if (this.draggingOrder.status === 6) {
this.popupTochGoed = true;
}
else if ([40, 52, 42,41,49,55,54].indexOf(this.draggingOrder.status) !== -1) {
this.back = true;
}
},
...
}
}
The problem seems to be that you are using native events, while the touch implementation does not (always?) use these events. It is intended that you use a draggable component with one of the events outlined in the documentation. In your case the start and end events look promising. This event has a few properties (docs), some of them being to and from.
Let's assume that we have the following code:
<draggable v-for="(zone, index) in zones" v-model="zones[index]" :class="['dropzone', `zone-${index}`]" :key="`dropzone-${index}`" :options="options" #start="start" #end="end">
<div v-for="item in zones[index]" class="dropitem" :key="`dropitem-${item.id}`">
{{ item.title }}
</div>
</draggable>
This creates a few zones, each filled with their own items. Each array item of zones is changed based on where you move each item. You can then use start to have information on when you start moving an item, and end to have information on when you stop moving an item, and where that item came from and where it ended up. The following methods show off what you can do with that in this case:
methods: {
start (event) {
console.log('start', event);
},
end (event) {
console.log('end', event);
const { from, to } = event;
if (to.className.match(/\bzone-2\b/)) {
console.log('Zone 2 has something added!')
}
if (from.className.match(/\bzone-0\b/)) {
console.log('Zone 0 had something removed!');
}
}
}
We make our dropzones with a class zone-0, zone-1 or zone-2 in this case, so we can use the class name to determine which dropzone we ended up in.
An alternative way to determine which zone was changed is to simply use a watcher. Since zones changes based on where you move items, you can simply watch a particular dropzone for changes and do things based on that.
watch: {
'zones.1': {
handler (oldZone, newZone) {
if (Array.isArray(oldZone) && Array.isArray(newZone) && oldZone.length !== newZone.length) {
console.log('Zone 1 was changed from', oldZone, 'to', newZone);
}
}
}
}
A full example can be found on codesandbox.
I am using the Angular2 async pipe to stream values into the DOM. Here's a real simple example:
const stream = Observable.interval(1000)
.take(5)
.map(n => { if (n === 3) throw "ERROR"; return n; });
<div *ngFor="for num of stream | async">
{{num}}
</div>
<div id="error"></div>
What I would like to do is to have the sequence of 1-5 displayed, but on the error item (3), somehow populate the #error div with the error message.
This seems to require two things: first is the ability of the Angular async pipe to do something intelligent with errors, which I see no sign of. Looking at the source code, apparently it throws a JS exception, which doesn't seem too friendly.
Second is the ability to restart or continue the sequence after the error. I have read about catch and onErrorResumeNext and so on, but they all involve another sequence which will be switched to on an error. This greatly complicates the logic of generating the stream, on which I would just like to put a series of numbers (in this simple example). I have the sinking feeling that once an error occurs the game is over and the observable is completed and can only be "restarted" with a different observable. I'm still learning observables; is this in fact the case?
So my question is twofold:
Can Angular2's async pipe do something intelligent with errors?
Do observables have some simple way to continue after an error?
Yes you're right regarding the catch operator and the ability to do something after errors occur...
I would leverage the catch operator to catch the error and do something:
const stream = Observable.interval(1000)
.take(5)
.map(n => {
if (n === 3) {
throw Observable.throw(n);
}
return n;
})
.catch(err => {
this.error = error;
(...)
});
and in the template:
<div>{{error}}</div>
To be able to go on the initial observable, you need to create a new one starting at the point where the error occurs:
createObservable(i) {
return Observable.interval(1000)
.range(i + 1, 5 - i)
.take(5 - i)
});
}
and use it in the catch callback:
.catch(err => {
this.error = error;
return this.createObservable(err);
});
These two questions could help you:
How to resumeOnError (or similar) in RxJS5
RxJS Continue Listening After Ajax Error (last answer)
1) no, The async pipe subscribes and unsubscribes and returns the events it receives. You would need to handle the errors before they receive the async pipe.
2) You can use the catch operator and when it returns an observable then its value(s) is emitted by the .catch(err => Observable.of(-1)) instead of the error.
You could use this to emit a special "error" value and then use something like *ngIf="num === -1 to show the error value in some special way.
You can find more information on this https://blog.thoughtram.io/angular/2017/02/27/three-things-you-didnt-know-about-the-async-pipe.html
#Thierry Templier answer was correct but is now a bit outdated. Here's how to do it with the latest RXJS.
this.myObservable$ = this.myService.myFunc().pipe(
catchError(() => of([])) // this will emit [] if the request fails - u could handle this [] emit on error in the service itself
)
then HTML as normal:
<div *ngFor="let xxx of (myObservable$ | async)">
</div>
Note $ at end of Observable name is Angular recommended way to denote an Observable.
I was facing a similar issue and came up with another approach. I do not know if it's a good way of doing it, but it works.
template where you want to show the result of your observable:
<div *ngIf="tableData$ | async as tableData; else loader" class="mt-4">
<!-- do something with tableData -->
</div>
<ng-template #loader>
<loading [target]="tableData$"></loading>
</ng-template>
The loading component:
export class LoadingComponent implements OnInit {
private _errorMessageSubject : Subject<string> = new Subject<string>();
private _errorMessage$ : Observable<string> = this._errorMessageSubject.asObservable();
public get errorMessage$() : Observable<string> { return this._errorMessage$; }
private _target : Observable<any> | null = null;
public get target() : Observable<any> | null { return this._target }
// this input does nothing except catch the error and feed the
// message into the errorMessage subject.
#Input() public set target(o: Observable<any> | null) {
if(o == null) { return; }
this._target = o.pipe(
catchError((error, _) => {
this._errorMessageSubject.next(error);
return of(null);
}),
);
};
constructor() { }
ngOnInit(): void {
}
}
loader template:
<div *ngIf="target && target | async;">
</div>
<div *ngIf="errorMessage$ | async as error; else loading">
<p class="text-danger">{{ error }}</p>
</div>
<ng-template #loading> <!-- simply a spinner icon -->
<div class="d-flex justify-content-center">
<fa-icon [icon]="['fas', 'spinner']" size="6x" [spin]="true"></fa-icon>
</div>
</ng-template>
I am not perfectly sure if its a good approach to subscribe to the observable twice, as subscribing is done in the original component that needs the data and in the loader, but otherwise this seems to work properly.
I have set up the aurelia-dialog plugin. It's working using the example in the GitHub readme, but the documentation doesn't explain anything about how to use it otherwise. I have a simple use case with a list page. I want to click an "add new" button, pop the modal dialog which has it's own VM. The modal contains a simple dropdown list. I need to select an item on the list and make an API call to save the data, but I can't seem to figure out how to wire up my save method with the save button on the dialog.
The method that opens the dialog on my list page (which works just fine):
loadAgencyDialog(id){
this.dialogService.open({ viewModel: AddAgency, model: { id: id }}).then((result) => {
if (!result.wasCancelled) {
console.log('good');
console.log(result.output);
} else {
console.log('bad');
}
});
My modal add-agency.js (VM for the modal, also loads the select list just fine and yes, I have a variable named kase because case is reserved):
import {DialogController} from 'aurelia-dialog';
import {ApiClient} from 'lib/api-client';
import {inject} from 'aurelia-framework';
#inject(DialogController, apiClient)
export class AddAgency {
kase = { id: '' };
constructor(controller, apiClient){
this.controller = controller;
this.agencies = [];
this.apiClient = apiClient;
}
activate(kase){
this.kase = kase;
this.apiClient.get('agencies')
.then(response => response.json())
.then(agencies => this.agencies = agencies.data)
.then(() => console.log(this.agencies)); //these load fine
}
addAgency() {
//Do API call to save the agency here, but how?
}
}
This is part I'm unsure about. In the example, they use controller.ok(theobjectpassedin), which returns a promise. But I don't get where I can call my addAgency method. Any ideas?
It's possible I'm misunderstanding your question, but you should be able to just call addAgency() in your HTML:
<button click.trigger="addAgency()">Add</button>
And then do what you need to do in addAgency(), finishing with a call to this.controller.ok() to wrap up the modal.
As an example, here's my modal's dialog-footer:
<ai-dialog-footer>
<button click.trigger="controller.cancel()">Cancel</button>
<button click.trigger="ok(item)">Save</button>
</ai-dialog-footer>
And in my code:
ok(item) {
this.controller.ok(item);
}
Not too complex. Hope that helps.