rails จะมี helper ที่ชื่อ
date_select
date_select("period", "start_date")
ซึ่งได้หน้าตาของ UI ออกมาอย่างนี้
ดูแล้วไม่ถูกใจ user ชาวไทยอย่างแน่นอน
ผมก็เลยหาทางเปลี่ยนไปใช้ jsCalendar แทน
หน้าตาที่ได้ ก็จะเปลี่ยนไปเป็นแบบนี้
โดย jsCalendar มีวิธีการใช้ง่ายๆดังนี้
<input type="text" id="data" name="period[dp_start_date]"/>
<button id="trigger">...</button>
<script type="text/javascript">
Calendar.setup(
{
inputField : "data",
ifFormat : "%d/%m/%y",
button : "trigger"
}
);
</script>
ที่นี้ ถ้าอยากนำมาใช้ใน rails
จะทำอย่างไรให้ดูเป็น rails-style
(เขียนน้อยๆ, default เยอะๆ)
ประเด็นของการทำ widget เอง คงแยกเป็น 2 เรื่อง คือ
- การ render
- การ parse request parameter
กรณีของการ render เราจะทำ helper method ขึ้นมาใหม่ตัวหนึ่ง
ให้ชื่อว่า date_picker
มีลักษณะการใช้งานดังนี้
<%= date_picker "period", "start_date" %>
(จะเห็นว่ามีวิธีการใช้ที่เหมือนกับ date_select ของ Rails เลย)
ส่วนการ parse parameter นั้น
จะออกแบบให้ transparent กับ code เดิม
โดยต้องการให้ใช้กับ code ที่ gen จาก scaffold ได้เลย
(code ที่อยู่ใน controller class)
ของเดิม เวลา date_select submit ข้อมูลกลับมา
ข้อมูลที่กลับมาจะอยู่ในรูปแบบนี้
param-name param-value
===========================
start_date(1i) 2006
start_date(2i) 03 (เดือน)
start_date(3i) 20 (วัน)
เราก็จะเลียนแบบวิธีการส่ง parameter แบบเดิมนี้ (rails เรียกว่า multiparameter_attributes)
โดยแทรก filter เข้าไป
เพื่อทำการดักแปลงข้อมูลที่ submit มาจาก form
การใช้งาน filter ออกแบบให้ใช้คำสั่งแบบนี้้
class ApplicationController < ActionController::Base
date_picker_filter
end
เมื่อตกลงใจเรื่อง design ได้แล้ว ก็มาถึงคำถามว่า
จะ implement เป็น plugin ได้อย่างไร
เริ่มด้วยการตั้งชื่อให้ plugin เราก่อน
โดยใช้ชื่อว่า
datepicker
directory structure ของ plugin เป็นแบบนี้
+project-name
+app
+...
+vendor
+plugins
+datepicker
+lib
datepicker.rb
init.rb
file init.rb เป็น file ที่จะถูก rails เรียกใช้
มีเนื้อหาดังนี้
require 'datepicker'
ActionController::Base.send :include, DatePicker
การทำงานก็คือ สั่งให้ Class ActionController::Base include
Module
Datepicker
ของเรา (Mixin)ใน module
Datepicker
จะเริ่มด้วย method
self.included
method นี้เป็น callback method
ซึ่งจะถูกเรียกใช้เมื่อมีการเรียก include module ของเรา
logic ที่
Datepicker
ทำ ก็คือ- สั่ง extend controller ด้วย module ClassMethods
add class methoddate_picker_filter
ให้เรียกใช้จาก controller ได้ - เพิ่ม helper
date_picker
ที่เรียกใช้จากใน view หรือใน controller ได้
module DatePicker
def self.included(controller)
controller.extend(ClassMethods)
controller.helper_method(:date_picker)
end
module ClassMethods
def date_picker_filter(options = {})
before_filter do |c|
ParamUpdator.new(options).update(c.params)
end
end
end
...
def date_picker(object, method, options = {})
...
end
ใน method
date_picker_filter
จะมีการ add filter โดยใช้คำสั่ง
before_filter
)และใช้ class ParamUpdator ในการ scan และแปลงวันที่ที่อยู่ใน params object
class ParamUpdator
def initialize(options={})
@prefix = options[:prefix] || 'dp_'
@be = options[:ad] || false # buddhist era
@delim = options[:delim] || '/'
@prefix_regexp = Regexp.new("^#{@prefix}")
@delim_regexp = Regexp.new(@delim);
end
def update(hash)
hash.each do |key, value|
if value.class.to_s =~ /^Hash/
update(value)
else
if @prefix_regexp =~ key
update_param(hash, key, value)
end
end
end
end
def update_param(hash, key, value)
darys = parseDate(value)
realName = key[(@prefix.length)..(key.length)]
hash["#{realName}(1i)"] = darys[0]
hash["#{realName}(2i)"] = darys[1]
hash["#{realName}(3i)"] = darys[2]
hash.delete(key)
end
def parseDate(value)
darys = value.split(@delim_regexp);
if darys[2].length <= 2
darys[2] = (darys[2].to_i + (@be ? 1957 : 2000)).to_s
end
darys.reverse
end
end #end ParamUpdator
ประเด็นที่น่าสนใจในส่วนของการแปลง parameter ก็คือ
object
params
ไม่ได้เป็น instance ของ Hash
แต่เป็น instance ของ HashWithIndifferentAccess
ส่วน
date_picker
ที่ใช้ render ก็มีหน้าตาแบบนี้
def date_picker(object, method, options = {})
prefix = options[:prefix] || "dp_"
format = options[:format] || "%d/%m/%y"
size = options[:size] || "10"
inputClazz = options[:input_class] || "date_input"
triggerClazz = options[:trigger_class] || "date_trigger"
be = options[:be] || false
maxsize = options[:maxsize] || format.length
id = "#{object}_#{method}"
name = "#{object}[#{prefix}#{method}]"
obj = self.instance_variable_get "@#{object}"
value = obj == nil ? "" : (date_to_string(format, obj.send(method), be))
<<EOS
<input type="text" size="#{size}" maxsize="#{maxsize}"
id="#{id}" name="#{name}"
class="#{inputClazz}" value="#{value}"/>
<button id="trigger_#{id}" class="#{triggerClazz}">...</button>
<script>
Calendar.setup(
{
inputField : "#{id}",
ifFormat : "#{format}",
button : "trigger_#{id}"
}
);
</script>
EOS
end
ประเด็นที่น่าสนใจ ก็คือการ get value จาก Model instance
ที่ใช้ method
instance_variable_get
กับ assumption ที่ว่า model instance จะอยู่ในรูป instance variable
เหลือที่ยังไม่ได้ทำ ก็คือ ส่วนของการ validate
ที่จะ implement ด้วย javascript ที่ฝั่ง browser เลย
1 comment:
Hi,
I was wondering if you could post an English translation of the text about Rails' JavaScript-based DatePicker.
Just looking at the code examples gets me very far, but not far enough...
Thank you very much!
Onno
Post a Comment