YesodでHTML/JavaScript/CSSを共通化する

Webアプリを作ってると、HTML/JavaScript/CSSを共通化したくなることがあります。
Yesodでは、これはWidgetって仕組みを使うようです。

なお今回出てくるコードは全て、scaffoldで生成されたソース群がある環境を前提にしています。

Widgetとは?

WidgetについてはYesod Bookに説明があります。
たぶん「hamlet/julius/luciusの3点セットを作ると再利用可能な物が出来る」というところだと思います。

hamletはHTMLのテンプレート、juliusはJavaScriptのテンプレート、luciusはCSSのテンプレートです。


英語は得意ではないので正しく理解出来ている自信はありませんが、大きくは外していないと思います。

試してみたところ、常に3点セットである必要はなくて、1つ、あるいは2つが欠けていてもいいようです。

Widget例・submitボタンにスタイルと動作を与えてみる

ではWidgetを作ってみます。

削除用のsubmitボタンに、特定のスタイルと動作を与えてみましょう。



class属性"delete"のsubmitボタンはこんな外観になるようにして、さらにこのボタンを押すと確認ダイアログが出るようにします。キャンセルを押すとサブミットをキャンセル、OKを押すとサブミット実行、というわけですね。

これを「submit」という名前のWidgetとして作ってみます。

今回の場合、HTMLは必要ないのでhamletファイルは作らないことにします。

まずはCSSのテンプレート「submit.lucius」を作ります。ファイルの置き場所はtemplatesディレクトリです。なおhamletファイル、juliusファイルもこのディレクトリに置きます。

input[type="submit"].delete {
  font-weight: bold;
  color: rgb(255, 0, 0);
  background-color: rgb(255, 180, 180);
}

ごく普通のCSSです。

次にJavaScriptのテンプレート「submit.julius」を作ります。

$(function() {
  $("input[type=submit].delete").click(function() {
    return confirm("削除していい?")
  });
});

これもごく普通のJavaScriptなんですが、jQueryに依存しています。この依存はWidgetを使う側には意識させたくないです。
そこでWidgetを使うための関数を作って、それを通して使ってもらうようにします。この関数の中でjQueryへの依存を解決してしまうわけですね。

ここではWidgetsというモジュールを作って、そこにWidgetを使うための関数を定義することにします。

module Widgets where

import Import

submitWidget :: GGWidget Jabaraster (GHandler Jabaraster Jabaraster) ()
submitWidget = do
  addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
  $(widgetFile "submit")

ポイントは2つです。

  • addScriptRemoteでjQueryへの依存を解決
  • $(widgetFile "submit")でjuliusファイルとluciusファイルを読み込んでパース

これでWidgetが作れました。

Widgetを使う

作ったWidgetを使うのは簡単で、単にsubmitWidget関数を呼び出せばOKです。
一例を挙げます。

getRootR :: Handler RepHtml
getRootR = do
  ((_, widget), enctype) <- runFormPost someForm
  defaultLayout $ do
    setTitle "title"
    submitWidget -- 作ったWidgetを使う
    $(widgetFile "homepage")

引数を取るWidget*1

引数を取るWidgetも作ってみましょう。例えば特定のid属性値に対して操作するjuliusは以下のように書けます。

$(function() {
  $("##{tagId}").click(function() {
    return confirm("削除していい?")
  });
});

#{tagId}の部分が引数の値で置き換えられます。このWidgetを使う関数は以下のように書けます。

submitWidget' :: Text -> GGWidget Jabaraster (GHandler Jabaraster Jabaraster) ()
submitWidget' tagId = do
  addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
  $(widgetFile "submit2")

引数が多くなる場合は引数オブジェクトみたいなものを導入してもいいでしょう。その場合juliusは以下のように書きます。

$(function() {
  $("##{submitTagId context}").click(function() {
    return confirm("削除していい?")
  });
});

juliusの中でHaskellの関数が呼び出せます。もちろんhamletの中やluciusの中でも呼び出せます。これはめちゃくちゃ強力です(強力故、使い方には気を付けないといけないと思いますが)。

Widgetを使うための関数と引数オブジェクトは以下のようになります。

data SubmitWidgetContext = SubmitWidgetContext {
  submitTagId :: Text
  , submitOther :: Text
}

submitWidget'' :: SubmitWidgetContext -> GGWidget Jabaraster (GHandler Jabaraster Jabaraster) ()
submitWidget'' context = do
  addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
  $(widgetFile "submit3")


今回はここまで。
なんか最近はYesodのエントリばかりで、いっそのことYesodをカテゴリにしてしまおうか、とか考えています。

*1:実は引数を取るWidgetは、引数の数が多くなってきたときに使い勝手が悪くなります。引数の数が多い関数の呼び出しは間違えやすいですから。引数オブジェクトを導入するという手もありますが、面倒くささは否めません。共通化したくてWidgetを導入したがために記述が面倒になる・・・このジレンマには、今のところ解決策を見いだせていません。