Reputation: 18299
I'm trying to make a button component that will hide the button and display a spinner when it is clicked and is waiting for some asynchronous job to complete, and then hide the spinner and display the button after the asynchronous job is complete.
The key difficulity I'm facing is that I want parents to use on:click
in this way:
<ActionButton
on:click={someAsycnFunction}
>
Go
</ActionButton>
I do not want the parent to have to use a workaround such as:
onClick={someAsyncFunction}
Here is my ActionButton.svelte
child component now. While this successfully wraps the call to the parents someAsyncFunction
, I am unable to await the result of this.
<script>
import { createEventDispatcher } from 'svelte';
let isRunning = false;
const dispatch = createEventDispatcher();
// Wrapper function for the click event
function handleClick(event) {
console.log('child handleClick');
isRunning = true;
/* This doesn't work because the parent function is async
* and there is no way to await on this */
dispatch('click', event);
isRunning = false;
}
</script>
{#if !isRunning}
<button
on:click={handleClick}
>
<slot />
</Button>
{:else}
...
{/if}
The handleClick
successfully wraps the parent's call to someAsyncFunction
, however, I do not see a way to await the dispatch('click', event)
The result is that isRunning is set to false immediately instead of waiting for the parent someAsyncFunction
to complete.
Is there any way to accomplish this while still allowing the parent to 1) not have to know about the internal isRunning
and for the parent to be able to use on:click
instead of something like onClick
?
Upvotes: 2
Views: 1095
Reputation: 315
Parent components (importing ActionButton
) have their async tasks, but state ActionButton.isRunning
depends on the state from parents.
It means that component ActionButton
is not owner of the state isRunning
.
running state
is inherently coupled with async task from each parents.
class ButtonModel encapsulates action and state.
+- ActionButton.svelte
+- App.svelte - Parent component
+- button-model.js - defines class ButtonModel
class ButtonModel manages async action and it's state(running
).
import {writable} from 'svelte/store';
/**
* proxy implementation encapsulating store updates
*/
export class ButtonModel {
/** @type{boolean} */
running;
/** @type {() => Promise<void>} async operation(maybe) */
action;
/** @type {Wriable<ButtonModel>} */
store;
constructor(asyncAction) {
this.running = false;
this.action = asyncAction
this.store = writable(this)
}
// store contract
// @see https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values-store-contract
subscribe(callback) {
return this.store.subscribe(callback);
}
get isRunning() {
return this.running
}
update(callback) {
if(callback) {
callback()
}
// calling store.update(...) makes changes visible
this.store.update((state) => state)
}
async execute() {
if(this.running) {
return
}
this.update(() => { this.running = true })
await this.action()
this.update(() => { this.running = false })
}
}
<script>
import {ButtonModel} from './button-model';
import ActionButton from './ActionButton.svelte';
const asyncTask = () => new Promise((resolve, reject) => {
console.log('sending request to server....')
setTimeout(() => {
console.log('response received from server...')
resolve()
}, 3000)
})
const model = new ButtonModel(asyncTask)
</script>
<h1>Button Model</h1>
<ActionButton {model}>Click Me</ActionButton>
<ActionButton {model}>
- passing model to ActionButton
<script>
export let model; // Writable<ButtonModel>
const handleClick = () => model.execute()
</script>
<button disabled={$model.isRunning} on:click={handleClick}><slot/>{#if $model.isRunning}<span>[SPINNER]</span>{/if}</button>
model.execute()
isRunning
from model
model
with prefix $
(like $model
) guarantees the changes visibleUpvotes: 0
Reputation: 682
I do not want the parent to have to use a workaround such as:
`onClick={someAsyncFunction}`
I had exactly the same problem, and used exactly that workaround, and I had the same feeling about it as you do. However, this works beautifully and is very clean.
async
function, without any special considerations.await
it.on:click
to onClick
.onClick
call. That reliefs all the event handlers from needing a try/catch and improves the reliability of your app and also makes your code more concise.
Here's what I have in my Button
component:
<button on:click on:click={myOnClick}
{disabled} class:disabled ...>...</button>
<script lang="ts">
...
export let disabled = false;
export let onClick: (event: Event) => void = null;
export let onError = showError;
async function myOnClick(event: Event) {
if (!(onClick && typeof(onClick) == "function")) {
return;
}
let previousDisabled = disabled;
disabled = true;
try {
await onClick(event);
} catch (ex) {
onError(ex);
}
disabled = previousDisabled;
}
</script>
Use it like this:
<Button ... onClick={onSubmit} />
async function onSubmit(event) {
let resp = await fetch("https://..."); ...
}
This will:
async
function to finishshowError(ex)
functiononError
function.onClick
handler, you can still use the previous on:click
handler, and you manage state and errors yourself.Upvotes: 2
Reputation: 185140
You can pass a function in the event object that signals that the operation has finished.
(If you want to also allow synchronous operations, you would need to also add some other function or property that signal what the ActionButton
is supposed to expect.)
Here is a simple example that is async only:
<!-- ActionButton -->
<script>
import { createEventDispatcher } from 'svelte';
let executing = false;
const dispatch = createEventDispatcher();
async function onClick() {
executing = true;
await new Promise(resolve => {
dispatch('click', { done: resolve })
});
executing = false;
}
</script>
{#if !executing}
<button type="button" on:click={onClick}>
<slot />
</button>
{:else}
[Executing]
{/if}
// event handler usage in parent:
async function onClick(e) {
// [async stuff here]
e.detail.done();
}
Note that passing callbacks will be the default in Svelte 5 rather than a workaround.
Upvotes: 2