namelos
namelos

Reputation: 33

Aurelia how to forward all attributes to child element in custom component

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:

  1. 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.

  2. 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

Answers (1)

zedL
zedL

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

Related Questions