DynamicCalendarHelper と link_to_remote
DynamicCalendar ヘルパーをインストールして、次の月や前の月を XMLHttpRequest を使用して表示する機能を実装するまでのまとめを以下に。
何回やるんだ、と言われてはイカんので、なるべく端折りつつ、という事で以下の 3 つのエントリで ajax を使わない状態な DynamicCalendarHelper の盛り込みはできます。
- http://d.hatena.ne.jp/yamanetoshi/20060810/1155196947
- http://d.hatena.ne.jp/yamanetoshi/20060810/1155219965
- http://d.hatena.ne.jp/yamanetoshi/20060811/1155267009
この状態を前提として、link_to_remote で前月と次月に XMLHttpRequest で画面遷移できる状態にしてみるサンプルを作成する手順を以下に。
config/routes.rb の修正
上記コンテンツでは config/routes.rb の設定に不備があります。最低限、以下に示す map.connect が必要と思われます。(現時点では)
map.connect "", :controller => 'calendar', :action => 'index', :year => Date.today.year, :month => Date.today.mon map.connect 'events/:action/:id', :controller => 'events' map.connect ':year/:month', :controller => 'calendar', :action => 'index' map.connect ':controller/:action' map.connect ':controller/:action/:id'
ajax の盛り込み
DynamicCalendarHelper のデフォルトの状態では、ajax が使えない状態なので、その準備が必要になります。おおまかには以下の 2 点。
- app/views/layouts/application.rhtml の修正
- scriptaculous な *.js の盛り込み
まず 1. を。
以下に示す二行を app/views/layouts/application.rhtml に挿入します。
<%= javascript_include_tag 'prototype' %> <%= javascript_include_tag 'scriptaculous' %>
コピー後はこんな感じになります。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>Calendar Companion</title> <%= stylesheet_link_tag 'calendar' %> <%= stylesheet_link_tag 'scaffold' %> <%= javascript_include_tag 'prototype' %> <%= javascript_include_tag 'scriptaculous' %> </head>
RoR で実現する Ajax アプリ (写経) その 1 というエントリにて script.aculo.us から最新版を co する方法を記述していますので home にオとしておいて下さい。そこから rails プロジェクトの所定の位置にコピーします。(以下の例ではカレントディレクトリは RAILS_HOME とします)
$ cp ~/scriptaculous/lib/*.js todo/public/javascripts/. $ cp ~/scriptaculous/src/*.js todo/public/javascripts/.
app/helpers/calendar_helper.rb の修正
ajax の盛り込みには DOM オブジェクトにするための id 属性の盛り込みが必要です。以下に calendar メソドの全文を引用。ただし、この時点で追加するのは
- table タグの直前の div タグ
- /table タグの直後の /div タグ
の二点のみ。
app/helpers/calendar_helper.rb
def calendar(options = {}, &block) raise ArgumentError, "No year given" unless defined? options[:year] raise ArgumentError, "No month given" unless defined? options[:month] block ||= lambda {|d| nil} options[:table_class ] ||= "calendar" options[:month_name_class ] ||= "monthName" options[:other_month_class ] ||= "otherMonth" options[:day_name_class ] ||= "dayName" options[:day_class ] ||= "day" options[:abbrev ] ||= (0..2) # initialize range first = Date.civil(options[:year], options[:month], 1) last = Date.civil(options[:year], options[:month], -1) # draw of table cal = <<EOF <div id="#{options[:table_class]}" > <table class="#{options[:table_class]}" > <thead> <tr class="#{options[:month_name_class]}"> <th colspan="7">#{Date::MONTHNAMES[options[:month]]}</th> </tr> <tr class="#{options[:day_name_class]}"> EOF Date::DAYNAMES.each {|d| cal << " <th>#{d[options[:abbrev]]}</th>\n"} # day names cal << " </tr> </thead> <tbody> <tr>\n" 0.upto(first.wday - 1) {|d| cal << " <td class=\"#{options[:other_month_class]}\"></td>\n"} unless first.wday == 0 # empty cells first.upto(last) do |cur| cell_text, cell_attrs = block.call(cur) # allow block to render contents of table cells cell_text ||= cur.mday cell_attrs ||= {:class => options[:day_class]} # allow user to define attributes of table cells cell_attrs = cell_attrs.map {|k, v| "#{k}=\"#{v}\""}.join(' ') cal << " <td #{cell_attrs}>#{cell_text}</td>\n" cal << " </tr>\n <tr>\n" if cur.wday == 6 # start new table row end last.wday.upto(5) {|d| cal << " <td class=\"#{options[:other_month_class]}\"></td>\n"} unless last.wday == 6 # empty cells cal << " </tr>\n </tbody>\n</table></div>" end end
基本的には ID をカレンダに対して、という修正のみになります。
app/views/calendar/index.rhtml の修正
以下の修正を盛り込みます。
- link_to を link_to_remote に修正
- HOME への link_to を修正
上記二点の修正を盛り込んだものを以下に。
app/views/calendar/index.rhtml
<h1>Dynamic Calendar Example</h1> <table noborder> <tr><td><%= link_to_remote 'before', { :url => { :controller => 'calendar', :action => 'before' }, :update => 'calendar', }, :class => 'menu_link' %> </td><td><%= link_to 'HOME', { :controller => 'calendar', :action => 'index' }, :class => 'menu_link' %> </td><td><%= link_to_remote 'next', { :url => { :controller => 'calendar', :action => 'after' }, :update => 'calendar', }, :class => 'menu_link' %> </td></tr> </table> <%= render :partial => 'cal', :locals => { :year => @year, :month => @month, :databinder => @databinder } %>
menu_link な CSS はパクリ物なんで引用は略。
controller の修正
上記修正にて after と before というアクションを盛り込んでいるので、controller に追加。諸種の事由より全部引用。
app/controllers/calendar_controller.rb
class CalendarController < ApplicationController private def getDatabinder events = Event.find(:all) lambda do |d| cell_text = "#{d.mday}<br />" cell_attrs = {:class => 'day'} events.each do |e| if e.startdate == d cell_text << e.name << "<br />" cell_attrs[:class] = 'specialDay' end end [cell_text, cell_attrs] end end public def index @year = session[:year] = @params[:year].to_i @month = session[:month] = @params[:month].to_i @databinder = getDatabinder end def before session[:year] = (session[:month] == 1 ? session[:year] - 1 : session[:year]) session[:month] = (session[:month] == 1 ? 12 : session[:month] - 1) render :partial => 'cal', :locals => { :year => session[:year], :month => session[:month], :databinder => getDatabinder } end def after session[:year] = (session[:month] == 12 ? session[:year] + 1 : session[:year]) session[:month] = (session[:month] == 12 ? 1 : session[:month] + 1) render :partial => 'cal', :locals => { :year => session[:year], :month => session[:month], :databinder => getDatabinder } end end
# session[:month] == 1 ではなく session[:month] <= 1 かなぁ。
# ま、いいや。
integration 試験の盛り込み
とりあえずこの時点では、/calendar/index から 前月、次月への遷移が正常であればヨシ、とゆー事で試験としてはこんなモノで良いのでしょうか (まだログインとかないし)。
test/integration/calendar_test.rb
require File.dirname(__FILE__) + '/../test_helper' class CalendarTest < ActionController::IntegrationTest def test_before_next open_session do |sess| get "/" assert_equal assigns(:session)[:year], Date.today.year assert_equal assigns(:session)[:month], Date.today.month assert_equal 200, status assert_template 'index' get "/calendar/before/" assert_equal assigns(:session)[:year], Date.today.year assert_equal assigns(:session)[:month], Date.today.month - 1 assert_equal 200, status assert_template '_cal' get "/" assert_equal assigns(:session)[:year], Date.today.year assert_equal assigns(:session)[:month], Date.today.month assert_equal 200, status assert_template 'index' get "/calendar/after" assert_equal assigns(:session)[:year], Date.today.year assert_equal assigns(:session)[:month], Date.today.month + 1 assert_equal 200, status assert_template '_cal' end end end
自宅と職場でファイルのやりとりするのがマジでツラいので、そろそろ svn に登録したい。(tar.bz2 で 2MB くらいになる)
あ、あとヘルパーの試験についても調べておいた方が良いな。
追記
プレビュー見るに、routes.rb のあたりは理解が微妙だなぁ。