Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

StimulusJSでカウンタをアニメーション化する(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

StimulusJSでカウンタをアニメーション化する(翻訳)

以下のような目的に使える汎用のカウンタ用Stimulusコントローラを常時使えるようにしておくと、何かと便利です。

  • 値下げをアニメーション表示することでユーザーの注目を集める(例: $99がたったの$49ドルに!)
  • タイマーで時間経過を表示する

Stimulusでこうした機能を作る方法を見ていくことにしましょう。
Stimulusコントローラを書くときの定番作業として、まずHTMLを書いてみましょう。

<p data-controller="counter" data-counter-start-value="10" data-counter-end-value="0"></p>

このHTMLを見れば、それだけでここで何が行われているかを把握できます(ここがStimulusの素晴らしい点の1つだと思います)。

次に、10から0までカウントダウンする最も基本的なカウンタをコントローラで作りましょう。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    start: Number
  }

  connect() {
    this.timer = setInterval(() => {
      this.element.textContent = this.startValue--
    }, 1000)
  }
}

これでも動きますが、まだ手を加える余地があります(例: 終了値が考慮されていません)。以下のように追加しましょう。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    start: Number,
    end: Number
  }

  connect() {
    this.timer = setInterval(() => {
      this.element.textContent = this.startValue--

      if (this.startValue === this.endValue) {
        clearInterval(this.timer)
      }
    }, 1000)
  }
}

別記事『Stimulusコントローラでdisconnectを書くべき理由』で書いたように、表示をクリアする間隔が重要です。そのためのコードを、以下のようにライフサイクル制御用のdisconnectメソッドに追加しましょう。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    start: Number,
    end: Number
  }

  connect() {
    this.timer = setInterval(() => {
      this.element.textContent = this.startValue--

      if (this.startValue === this.endValue) {
        clearInterval(this.timer)
      }
    }, 1000)
  }

  disconnect() {
    if (this.timer) clearInterval(this.timer)
  }
}

要素のコンテンツを自動的に更新するためのコールバックメソッドを追加しましょう。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    start: Number,
    end: Number
  }

  connect() {
    this.timer = setInterval(() => {
      this.startValue--

      if (this.startValue === this.endValue) {
        clearInterval(this.timer)
      }
    }, 1000)
  }

  disconnect() {
    if (this.timer) clearInterval(this.timer)
  }

  // private

  startValueChanged() {
    this.element.textContent = this.startValue
  }
}

こうすることで、開始値が変更されるたびに他の変更を行える適切な場所ができます(変更を強調するためにCSSをいくつか追加しておくと後で見やすくなります)。Stimulusのコールバックについて詳しくは以下の記事をどうぞ。

参考: Refactor Stimulus.js Controllers to Use Change Callbacks | Rails Designer

カウントダウンだけでなく、カウントアップしたい場合はどうでしょうか。開始値が終了値よりも大きい場合はカウントダウンし、その逆の場合はカウントアップするのがよいでしょう。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    start: Number,
    end: Number,
    direction: { type: String, default: "auto" }
  }

  connect() {
    this.timer = setInterval(() => {
      this.startValue = this.direction === "up"
        ? this.startValue + 1
        : this.startValue - 1

      if (this.startValue === this.endValue) {
        clearInterval(this.timer)
      }
    }, 1000)

  }

  disconnect() {
    if (this.timer) clearInterval(this.timer)
  }

  // private

  startValueChanged() {
    this.element.textContent = this.startValue
  }

  get direction() {
    if (this.directionValue !== "auto") {
      return this.directionValue
    }

    return this.startValue < this.endValue ? "up" : "down"
  }
}

direction()ゲッターは、カウントの向き(アップ、ダウン)が指定されているかどうかをチェックします。未指定の場合は、開始値と終了値を元にカウントの向きを決定します。これで、カウントアップにもカウントダウンにも対応できますね。素晴らしい!

ここで1箇所改善できる点があります。現在はカウントの間隔が1000でハードコードされていますが、これを変更可能にして、元の価格から値下げ後の価格へのカウントダウンを高速にすれば、金額が大幅に値引きされたことを効果的に表示できるようになります。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    start: Number,
    end: Number,
    direction: { type: String, default: "auto" },
    interval: { type: Number, default: 1000 }
  }

  connect() {
    this.timer = setInterval(() => {
      this.startValue = this.direction === "up"
        ? this.startValue + 1
        : this.startValue - 1

      if (this.startValue === this.endValue) {
        clearInterval(this.timer)
      }
    }, this.intervalValue)

  }

  disconnect() {
    if (this.timer) clearInterval(this.timer)
  }

  // private

  startValueChanged() {
    this.element.textContent = this.startValue
  }

  get direction() {
    if (this.directionValue !== "auto") {
      return this.directionValue
    }

    return this.startValue < this.endValue ? "up" : "down"
  }
}

これでHTML側で以下のようにカウンタの間隔も指定できるようになりました。素晴らしいですね!

<p data-controller="counter" data-counter-start-value="99" data-counter-end-value="49" data-counter-interval-value="10"></p>

これで、いくつかの便利な機能を備えたシンプルなカウンタ用Stimulusコントローラができました。しかしここで終わってはもったいない!表示される数字のフォーマットをJavaScriptのIntl.NumberFormatオブジェクトで設定できます。
これは以下を追加することでできます。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  // ...

  startValueChanged() {
    this.element.textContent = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(this.startValue)
  }
}

このNumberFormatオブジェクトで使えるすべてのオプションをぜひチェックしてみましょう!

開始値を更新するときに何らかのCSSトランジションを適用してもよいでしょう。これも簡単にstartValueChanged()に追加できます。

訳注

以下は、ここまでのコードを元にしたCodePenです。

参考: StimulusJS -- counter animations

See the Pen
StimulusJS -- counter animations
by hachi8833 (@hachi8833)
on CodePen.

最後のささやかなボーナスとして、ページをリダイレクトしたときにも動き続けるカウンタが欲しい場合は、以下のように data-turbo-permanentを使えます。

<p id="counter" data-turbo-permanent>
  Time is running out! You only got
  <span data-controller="counter" data-counter-start-value="60" data-counter-end-value="0"></span> seconds to complete this task.
</p>

関連記事

Stimulusで知っておきたい10の機能(翻訳)

Stimulusコントローラでdisconnectを書く理由(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。