Reputation: 33
I'm using both React and Aurelia across different projects.
My Aurelia application is becoming really complex and I really want to do the deal with duplicated logics. Say I want to have a throttled button which would await async request to be clickable again, with React it's pretty straightforward:
I wish my <ThrottledButton />
have exact same behavior with native <button />
, except it would automatically prevent multiple requests from happening until previous request have completed or failed:
class MyComponent extends Component {
handleClick = async e => {
const response = await this.longRequest(e)
console.log(`Received: `, response)
}
longRequest = async e => {
return new Promise((res, rej) => {
setTimeout(() => { res(e) }, 1000)
})
}
render() {
return <div>
<button className="btn" onClick={this.handleClick}>Click me</button>
<ThrottledButton className="btn" onClick={this.handleClick}>Click me</ThrottledButton>
</div>
}
}
Here's an implementation of the <ThrottledButton />
:
class ThrottledButton extends Component {
constructor(props) {
super(props)
this.state = { operating: false }
}
render() {
const { onClick, ...restProps } = this.props
const decoratedOnClick = async e => {
if (this.state.operating) return
this.setState({ operating: true })
try {
await Promise.resolve(onClick(e))
this.setState({ operating: false })
} catch (err) {
this.setState({ operating: false })
throw err
}
}
return <button onClick={decoratedOnClick} {...restProps}>{this.props.children}</button>
}
}
Now I really want to achieve the same (or similar) thing in Aurelia, and here's the use case component:
<template>
<require from="components/throttled-button"></require>
<button
class="btn"
click.delegate="handleClick($event)">
Click me
</button>
<throttled-button
class="btn"
on-click.bind="handleClick">
Click me
</throttled-button>
</template>
export class MyComponent {
handleClick = async e => {
const response = await this.longRequest(e)
console.log(`Received: `, response)
}
async longRequest(e) {
return new Promise((res, rej) => {
setTimeout(() => { res(e) }, 1000)
})
}
}
And here's the throttled-button I currently have:
<template>
<button click.delegate="decoratedClick($event)" class.bind="class">
<slot></slot>
</button>
</template>
export class ThrottledButton {
@bindable onClick = () => {}
@bindable class
constructor() {
this.operating = false
}
decoratedClick = async e => {
console.log(this.operating)
if (this.operating) return
this.operating = true
try {
await Promise.resolve(this.onClick(e))
this.operating = false
} catch (err) {
this.operating = false
throw err
}
}
}
As you can see, the <throttled-button />
is quite different from native <button />
, especially:
Instead of something similar to React's <button {...props} />
spread
operator, I have to manually pass every attributes from what I bound
to <throttled-button />
to native button. For most component it's a
huge effort and could lead to many mistakes.
I have to bind (or call) the on-click attribute, which make it works pretty different to the native one. To solve this problem, I tried to use DOM dispatchEvent API
Here's what I tried:
<template>
<button ref="btn">
<slot></slot>
</button>
</template>
@inject(Element)
export class ThrottledButton {
constructor(el) {
this.el = el
this.operating = false
}
attached = () => {
this.btn.addEventListener('click', this.forwardEvent)
}
detached = () => {
this.btn.removeEventListener('click', this.forwardEvent)
}
forwardEvent = e => {
this.operating = true
this.el.dispatchEvent(e) // no way to get the promise
}
}
However, there's no way to get the reference to the promise, which makes throttling impossible.
Is there any hope to resolve these in Aurelia? Any idea would be appreciated.
Upvotes: 2
Views: 671
Reputation: 357
You can use a custom attribute to solve the problem. Here is the code in typescript.
import { autoinject, customAttribute } from 'aurelia-framework';
@customAttribute('single-click')
@autoinject()
export class SingleClickCustomAttribute {
clicked: (e: Event) => void;
private value: () => Promise<any>;
private executing: boolean = false;
constructor(
private element: Element
) {
this.clicked = e => {
if (this.executing) {
return;
}
this.executing = true;
setTimeout(async () => {
try {
await this.value();
}
catch (ex) {
}
this.executing = false;
}, 100);
};
}
attached() {
this.element.addEventListener('click', this.clicked);
}
detached() {
this.element.removeEventListener('click', this.clicked);
}
}
And use it like this.
<button single-click.call="someAsyncMethod()" type="button">...</button>
The async method to call should something like this:
async someAsyncMethod(): Promise<void> {
await this.saveData();
}
The trick is to pass the async function with .call and use a state property.
Upvotes: 2