ruby on railsにおいて、特定のページでのみ実行するJavascriptが必要になることはよくあると思います。今回はassetsパイプラインのメリットを損なうことなく、それを実現するための方法を紹介します。
実現方法と考察
『特定のページでのみ実行するJavascript』を実現するための方法とし、およそ次のようなパターンがあると思います。
- View(.erbとか.slimとか)の中で他のjsファイルを読み込むか、直接Javascriptを書く
- applicationレイアウト上で、コントローラ名やアクション名を使って javascript_include_tag で読み込むファイルを切り替える
- 目的のページがどうかを判定してから処理を実行するようなJavascriptを書く
1.は可能な限り避けられる方法です。Viewの中にJavascriptのソースが混在することになりますし、当然ながらassetsパイプラインの対象になりません。例えば次の例のように、erbファイルの中から他のJSファイルを読み込むコードを記述したとすると・・・
📄hogehoge.html.erb
... <%= javascript_include_tag 'other_javascript' %> ...
当然ですがother_javascript
はapplication.js
の中で読み込まないように、つまりassetsパイプラインの管理外にする必要があります。結果的にproduction環境ではひとつのJSファイルにまとまらないので、その点はデメリットになります。
2.はlayout/application.html.erb
なんかで、読み込むJSファイルを切り替えてやる方法です。
📄layout/application.html.erb
<%= javascript_include_tag "application" %> <%= javascript_include_tag controller_name %>
コントローラ名に合わせたJSファイルを用意しておけば、そのコントローラアクセス時に自動的に読み込んでくれます。幾分スマートなやり方に見えますが、本質的には1.の方法となんら変わりません。各コントローラ用のJSファイルはapplication.js
からはずしておく必要があります。また(設定にもよりますが)コントローラ用JSファイルがないとproduction環境では500エラーになるので、特別な処理が必要でない場合も空のJSファイルを置いておかなければなりません。
そこで3.の方法です。これは、すべてのJSファイルをapplication.js
の管理下に置くので、assetsパイプラインの恩恵を受けることができます。各JSでは特定のページかどうか判定し、そうでなかったら空振りさせるという塩梅です。判定処理が必要になるというデメリットもありますが、他の2つの方法よりはベターだと思うので、今回はこいつを実装してみたいと思います。
ようやく実装
どうやって特定のページかどうか判断するかは、コントローラ名・アクション名を使うことにします。まずはapplication.html.erb
(というかbodyタグを書いてるとこ)を編集します。
📄app/views/layout/application.html.erb
<body data-controller="<%= controller_name %>" data-action="<%= action_name %>">
bodyのdata属性にコントローラ名とアクション名を付与するようにしました。ちなみにclassを使用する例もあるようですが、classはcssのためだけに使用し、JS用にはdata属性を使用するのがモダンだと思います。(jQuery流なので、いまさらモダンでもないけど・・・)
続いて、特定のページを判定する共通の関数を作成します。
📄app/assets/javascripts/on_page_load.coffee
# # Call the given callback function when the indicated page is loaded # # Usage: # # onPageLoad 'posts#index', -> # # Do something when controller is 'posts' and action is 'index'. # # onPageLoad 'posts', -> # # Do something when controller is 'posts' (in any action). # # # Accepts multiple conditions # onPageLoad ['posts#index', 'comments'], -> # # Do something # @onPageLoad = (controller_and_actions, callback) -> $(document).on 'turbolinks:load', -> conditions = regularize(controller_and_actions) unless conditions console.error '[onPageLoad] Unexpected arguments!' return conditions.forEach (a_controller_and_action) -> [controller, action] = a_controller_and_action.split('#') callback() if isOnPage(controller, action) regularize = (controller_and_actions) -> if typeof(controller_and_actions) == 'string' [controller_and_actions] else if Object.prototype.toString.call(controller_and_actions).includes('Array') controller_and_actions else null isOnPage = (controller, action) -> selector = "body[data-controller='#{controller}']" selector += "[data-action='#{action}']" if action $(selector).length > 0
使い方はUsage
の部分にも書きましたが、"コントローラ名#アクション名"
という形式で第一引数を渡してやれば、そのページがロードされた後に第二引数のコールバック関数が実行されます。
例えばCustomersControllerがあったとして、そのコントローラのindexアクションでのみ処理したい場合は、
📄app/assets/javascripts/customers.coffee
onPageLoad 'customers#index', -> # ここに処理を書く
CustomersControllerの全てのアクションと、ProductsConrollerのindexアクションでのみ処理したい場合だったら、
📄app/assets/javascripts/hogehoge.coffee
onPageLoad ['customers', 'products#index'], -> # ここに処理を書く
てな感じです。
なお、サンプルではturbolinks使用するようなってますが、使ってないならonPageLoad関数の冒頭$(document).on 'turbolinks:load', ->
をjqueryのreadyに書き換えればOKです。
on_page_load.coffeeはgistにも置いてあるので良かったら使ってみてください。
関連する記事
- Rails+JSフレームワークでリアルタイム掲示板を作成してみる(Ember.js編)
- Rails+JSフレームワークでリアルタイム掲示板を作成してみる(AngularJS編)
- Rails+JSフレームワークでリアルタイム掲示板を作成してみる(Backbone.js編)
- Rails用のgemを作成する手順 (Rails 4.0以降)
- simple_formとTwitter bootstrapで作る俺流鉄板Railsアプリ(その3)
色々な方法を見てきましたが、このやり方が一番スマートだと思います。
あなたのonPageLoad使わせて頂きます!
ありがとうございます。
参考になれば嬉しいです!
3の方法は、jsコードが特定の”アクション(又はコントローラ)”と対になるようになりますが、JavaScriptのコード自体は”テンプレート”と対になっているはずなので、renderを用いた別のアクションからのテンプレート表示で動作しないということがデメリットですね…
その場合、onPageLoadに呼び出されるアクションをすべて書くことになりますが、コントローラ側の条件分岐でrenderするテンプレートを分けていた場合に、意図せぬページでjsが実行されたりされなかったり…
またそうなると、クライアントサイドのプログラム中で、呼び出されるアクション(サーバ側)のことを深く意識しないといけなくなる…
「特定の”テンプレート”でのみ実行する」 かつ「アセットパイプラインにまとめられる」
↑を満たす方法があればいいのですが…難しいですね。
おっしゃる通りだと思います。
少し凝ったアプリだと、テンプレートを共用することがあると思うので、その場合はonPageLoadにアクションを列挙しなければなりません。
> 「特定の”テンプレート”でのみ実行する」 かつ「アセットパイプラインにまとめられる」
これが一番理想的だとは思うのですが、renderメソッドをフックするとか、ちょっと凝ったことをしないと実現できないんじゃないかと思ってます。
という意味では、今回のサンプルは「Good enough(十分に良い)」ではあるかなと思ってます。
ピンバック: コントローラー/アクション毎に JS を分ける [Rails] – Site-Builder.wiki