ここで作るのは下記のようなUIです。Stimulusを使って、ERBのページにChart.jsを埋め込み、かつChart.jsの表示をインタラクティブに変更しています。
connect()やdisconnect()を提供しています。DOMContentLoadedなどの頼るのではなくMutationObserver APIを使用しているため、インタラクティブにDOMが変更されるページに適しています。data-*-valuesHTML属性と同期されるため、埋め込まれたライブラリ(今回はChart.js)とHTMLを簡単に連携できます。なおReactの場合はインテグレーションであるreact-chartjs-2を使うことが多いかと思いますが、Stimulusを使うとインテグラーションがなくても簡単にChart.jsをラップできます。
<% set_breadcrumbs [["ChartJS", component_path(:chartjs)]] %> <%= render 'template', title: "ChartJS", description: "" do %> <div data-controller="chartjs" data-chartjs-label-value="UFO sightings per year!!!!" data-chartjs-data-value="<%= json_escape [{ year: 2010, count: 10 }, { year: 2011, count: 20 }, { year: 2012, count: 15 }, { year: 2013, count: 25 }, { year: 2014, count: 22 }, { year: 2015, count: 30 }, { year: 2016, count: 28 }, ].to_json %>"> <div class="flex justify-between"> <% [2010, 2011, 2012, 2013, 2014, 2015, 2016].each_with_index do |year, index| %> <div> <div><%= year %>:</div> <input type="range" min="0" max="100" value="10" step="10" class="w-20" data-chartjs-bar-param="<%= index %>" data-action="input->chartjs#changeBar"/> </div> <% end %> </div> <div class="w-full"> <canvas data-chartjs-target="chart"></canvas> </div> </div> <% end %>
to_jsonでJSONに変換しておけば、StimulusのValueとして正しく処理されます。念のために#json_escapeで処理しています。なおRailsのhelperを使えばtag.div(..., data: { chartjs_data_value: [{year: 2011, count: 20}, ...]})と書いて、RailsにJSON化とエスケープを任せることもできます。<input type="range" ...>タグのところはデータを変更するスライダーです。ここがイベントを受け取るActionになります
data-action="input->chartjs#changeBar"により、スライダーの値が変更されるとChartjsControllerのchangeBarメソッドが呼び出されますdata-chartjs-bar-paramで伝えます。<canvas data-chartjs-target="chart"></canvas>はChartjsControllerからの出力を受けるtargetですChartjsController Stimulus controllerimport {Controller} from "@hotwired/stimulus" import Chart from "chart.js/auto" // Connects to data-controller="chartjs" export default class extends Controller { static values = { data: {type: Array, default: []}, label: String } static targets = ["chart"] connect() { } disconnect() { this.chart?.destroy() } changeBar(event) { const value = event.currentTarget.value const barIndex = Number(event.params.bar) const newDataValue = [...this.dataValue] newDataValue[barIndex] = {year: 2010 + barIndex, count: value} // We create a new dataValue object and use the // `this.dataValue` setter to update the value. // This is to trigger the `this.dataValueChange()` callback. // This is unnecessary if you call `this.#render()` directly // in this function. this.dataValue = newDataValue } dataValueChanged() { this.#render() } #render() { this.#renderChart() } #renderChart() { const data = this.dataValue if (this.chart) { this.chart.data = this.#data() this.chart.update() } else { this.chart = new Chart( this.chartTarget, { type: 'bar', data: this.#data() } ); } } #data() { return { labels: this.dataValue.map(row => row.year), datasets: [ { label: this.labelValue, data: this.dataValue.map(row => row.count) } ] } } }
static valuesで使用するStimulus Valuesを宣言しています
dataはChartに表示するデータです。Array型として持ちます。HTMLのdata-chartjs-data-values属性にJSON型でステートが保持されますlabelはChartの表題になりますstatic targetsでControllerで処理されたデータの出力先を指定します
<canvas>タグを指定します disconnect()はライフサイクルに関するものです。このStimulus Controllerが画面から消えるなどした場合に呼び出されます。ここではChartjsオブジェクトを削除して、メモリリークを防ぎますchangeBarはイベントハンドラです。スライダーをクリックして値を変更した時に呼び出されます
newDataValueという新しいArrayを作ってthis.dataValueにセットしている点です。古いthis.dataValueの値を変更するだけではダメで、全く新しいArrayを渡す必要があります。dataValueChangedコールバックが呼ばれません。この辺りはReactのステート変更と同じです。dataValueChangedはStimulus Valueステートが変更された時に自動的に呼ばれるコールバックです。この中で#render()を呼びます#render()ではさらに#renderChart()を呼び出し、Chart.jsにデータを渡す処理を書いています。ここではChart.jsのチュートリアル通りのデータを渡しています
new Chart()で作りますthis.chartにセットされた既存のChartを書き換えてupdate()を呼びます