対象読者
JavaScriptの基本がわかる人

学習事項
Stimulusのコントローラは再利用可能であること
デフォルトのアクションイベントは記述を省略可能

前回、Stimulusの主要概念である「コントローラ」「アクション」「ターゲット」について学習しました。

今回は、もう少し実用的なものを作ってみましょう。
テキストフィールドに入力された値など、特定DOMの値をコピーできるボタンを作ります。
モダンなブラウザでは、コピー用のAPIを用意してくれていますが、それを呼び出す便利なボタンなどは存在しません。今回はそういったボタンを作るイメージになります。

では、スタートしましょう。

HTMLにテキストフィールドとボタンを用意

HTMLファイルのBODYタグ配下に下記を記述しましょう。
(stimulus-starterリポジトリを改造します。)

  <div>
    PIN: <input type="text" value="1234" readonly>
    <button>Copy to Clipboard</button>
  </div>

できたら、ブラウザで表示して下記のようになればHTMLはOKです。

ブラウザ表示

次に、clipboardコントローラを作ります。
./src/controllers/配下にclipboard_controller.jsを作ります。

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  copy() {
  }
}

そうしましたら、上記コントローラをHTMLに接続するため
さきほど書き換えたHTMLのテキストフィールド・ボタンの親divタグにdata-controller属性を追加します。下記の通りです。

<div data-controller="clipboard">

これでコントローラとHTMLタグの接続指定ができました。

次に、コピーの対象とするデータを取得するためにターゲットの設定を行います。
今回はテキストフィールドの値としたいため下記の通り、data-clipboard-target属性を追記します。

PIN: <input data-clipboard-target="source" type="text" value="1234" readonly>

そうしましたら、コントローラ側に参照するターゲットを設定します。

export default class extends Controller {
  static targets = [ "source" ]

  // ...
}

static targets = [ “source” ]とすることでsourceと指定したDOMを参照することができるようになります。

Stimulusが実行されるとき、static targetsの内容を読み込み、自動的に三つのプロパティを作ってくれます。
1. this.sourceTarget :sourceとして指定したターゲットのDOMを参照できます。
2. this.sourceTargets:sourceとして指定した全ターゲットのDOMを配列で参照できます。
3. this.hasSourceTarget sourceとして指定したターゲットがあればtrue なければ falseを返します。
sourceの部分がstatic targetsで指定したものになります。
※詳しくは、Stimulus公式リファレンスを読んでください。

次に、ボタンを押したときのアクションを設定しましょう。
Copy to Clipboardボタンが押されたらclipboardコントローラのcopyメソッドを呼び出したいので、buttonタグのdata-action属性を下記のように追加します。

 <button data-action="clipboard#copy">Copy to Clipboard</button>

前回の記事を読まれた方は「あれ?どのイベントに対応するか記載がない・・」と思われたかもしれません。実はStimulusフレームワークは典型的なイベントに関して、指定を省略できるようになっています。

つまり、厳密に上記を書くと

<button data-action="click->clipboard#copy">Copy to Clipboard</button>

とclick->を追記した形になります。

省略可能なイベント一覧は下記です。

HTMLタグデフォルトイベント
aclick
buttonclick
detailstoggle
formsubmit
inputinput
input type=submitclick
selectchange
textareainput

ここまででアクションの設定ができました。

次に、copyメソッドの中身を実装していきましょう。

copyメソッドが呼び出されたら、sourceターゲットの値をコピーするようにします。

  copy() {
    this.sourceTarget.select()
    document.execCommand('copy')
  }

これで、ボタンをクリックするとテキストフィールドの値をクリップボードにコピーできます。※注意 上記コピーコマンドの利用は現在非推奨です。今回はローカル環境で実行できるようにするため、あえて利用しています。

実は他のテキストフィールドでも同様にコピーを再現したい要件があったとします。
その場合、clipboard2_controller.jsみたいな同じような処理のコントローラを作っていく必要があるのでしょうか。

Stimulusはコントローラを再利用可能です。
なので、コントローラを同様にしたいタグに適用すればOKです。

例として、HTMLファイルにPINフィールドを追加してみましょう。

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="main.css">
  <script src="bundle.js" async></script>
</head>
<body>
  <div data-controller="clipboard">
    PIN: <input data-clipboard-target="source" type="text" value="1234" readonly>
    <button data-action="clipboard#copy">Copy to Clipboard</button>
  </div>
  <div data-controller="clipboard">
    PIN: <input data-clipboard-target="source" type="text" value="3737" readonly>
    <button data-action="clipboard#copy">Copy to Clipboard</button>
  </div>
</body>
</html>


デフォルト値3737というPINフィールドを追加しました。
デフォルト値1234のPINフィールドと同様に、divタグにコントローラ、inputタグにターゲット、ボタンにアクションを設定することで、コピー機能を再現できました。

通常、こういった同じ機能を持つボタンを複数設置することが往々にしてあるかと思いますが、Stimulusでは簡単に実現できるようになっています。

上記までは、ターゲットをテキストフィールド、アクションをボタンに設定していました。
しかし、Stimulusではターゲットはアクションを別タグに設定することも可能です。

例えば、ターゲットをテキストエリア、アクションをリンクに設定してみましょう。
下記を追加してみてください。

<div data-controller="clipboard">
  PIN: <input data-clipboard-target="source" type="text" value="3737" readonly>
  <a href="#" data-action="clipboard#copy">Copy to Clipboard</a>
</div>

どうでしょうか。コピーができるかと思います。
しかし、リンクをクリックしたときの挙動でである画面遷移が発生しちゃってるかと思います。その挙動を抑制するためevent.preventDefault()をcopyメソッドに追記しましょう。

  copy() {
    event.preventDefault()
    this.sourceTarget.select()
    document.execCommand('copy')
  }

sourceTargetには、select()メソッドとvalueプロパティを持つタグであればなんでも指定可能です。※今回はcopyメソッドにてselect()メソッドとvalueプロパティを使っているからです。ロジックにより異なります。

いかがでしたでしょうか。
Stimulusフレームワークでは、コントローラが再利用であり、適用するコントローラやアクションも同じ挙動をするタグを指定することができます。
これにより、汎用性のある仕組みを実現可能となっています。

次回は、今回の題材を利用して、CSSの動的な切り替えを学びます。