หัดเล่น I18n ใน Rails 2.2

(คัดลอกมาจาก http://www.rails66.com/blog/?p=604)

ฟีเจอร์ใหม่อีกอย่างหนึ่งของ Rails 2.2 ก็คือความสามารถในการทำ internationalization พื้นฐานที่ถูกบรรจุอยู่ใน core (อ่าน api, เว็บหลัก Rails I18n)

ก่อนจะใช้ได้ก็ต้องไปใช้ Rails 2.2 เสียก่อน ถ้าจะทดลองกับ project ใหม่ก็ติดตั้ง Rails 2.2 แล้วก็สั่งสร้าง project ได้เลย ถ้าจะแก้ project เก่าก็แก้ไฟล์ /config/environment.rb ตามนี้ครับ

# เปลี่ยน version rails
RAILS_GEM_VERSION = '2.2.2' unless defined? RAILS_GEM_VERSION

require File.join(File.dirname(__FILE__), 'boot')
  # ..
  # Setting locales
  config.i18n.default_locale = 'en'  # เปลี่ยนเป็น 'th' สำหรับไทย
  # ..
end

เปลี่ยนเสร็จก็อย่าลืมสั่ง rake rails:update ให้ปรับแก้อะไรต่าง ๆ ด้วยนะครับ

หลักการคร่าว ๆ ของการทำ i18n ใน rails ก็คือ เราจะแยกสตริงที่ใช้ไปใส่ไว้ในไฟล์ต่างหาก โดยแบ่งตามภาษา จากนั้นเวลาเราจะแสดงสตริงเหล่านั้นก็จะเรียกผ่านฟังก์ชัน I18n.translate หรือย่อ ๆ ว่า I18n.t แทนที่จะเขียนสตริงเหล่านั้นออกไปตรง ๆ

ที่เก็บของไฟล์สตริงเหล่านี้จะอยู่ใน /config/locales/ โดยสามารถเก็บเป็นไฟล์ yml หรือเป็น ruby hash ก็ได้ คราวนี้เราจะลองอะไรง่าย ๆ กันก่อน โดยลองไปเพิ่ม (หรือแก้) ไฟล์ en.yml ในไดเร็กทรอรีดังกล่าวเป็นดังด้านล่างครับ

en:
  hello: "Hello world"
  hello_name: "Hello, {{name}}"

  config:
    hello: "Hello, admin"

ทีนี้ เราไปทดลองเรียกสตริงดังกล่าวใน script/console ครับ

I18n.translate :hello                #=>; "Hello world"
I18n.t :hello                         #=>; "Hello world"
I18n.t :hello_name, :name => 'John'  #=>; "Hello, John"
I18n.t 'config.hello'                 #=>; "Hello, admin"

จากตัวอย่างด้านบน แสดงการเรียกสตริงแบบทั่วไป ส่วน hello_name เป็นสตริงที่รับพารามิเตอร์ name ส่วน config.hello เป็นการระบุสตริงแบบที่มีขอบเขต (อยู่ใน config)

ทีนี้ ลองไปสร้างไฟล์ th.yml แล้วใส่ข้อมูลตามด้านล่างนะครับ

th:
  hello: "สวัสดี"
  hello_name: "สวัสดี, {{name}}"

  config:
    hello: "กราบสวัสดีท่านผู้ดูแล"

แล้วไปทดลองใหม่ใน script/console ครับ

>> I18n.locale = 'th'                   # ตั้ง locale
>> I18n.translate :hello                #=>; "สวัสดี"
>> I18n.t :hello_name, :name => 'John'  #=>; "สวัสดี, John"
>> I18n.t 'config.hello'                #=>; "กราบสวัสดีท่านผู้ดูแล"

ทีนี้ ถ้าเราต้องการให้ locales เริ่มต้นของเราเป็น th เลย ก็ไปแก้บรรทัด config.i18n.default_locale ใน environment.rb นะครับ

เพื่อความสะดวกใน view ฟังก์ชัน I18n.t สามารถเรียกสั้น ๆ ได้ด้วย t ดังนั้นเราสามารถเขียน <%=t :hello %> ได้เลย

อันนี้เป็นการใช้งานแบบขั้นต้นนะครับ ถ้ามีเวลาจะมาเขียนเกี่ยวกับการแปลอื่น ๆ เช่นการแปลชื่อ model และ attributes ใน Active Record ต่อครับ ถ้าใครอยากเล่นก่อนก็ไปโหลดไฟล์ locale th.rb ที่มีการแปลข้อความใน Active Record และส่วนอื่น ๆ เช่นวันที่และจำนวนนับ (แปลโดยคุณ Sikachu! ขอบคุณมากครับ!) แล้วมาเล่นดูได้ครับ เอาไปใส่เพิ่มไว้ใน /config/locales แล้วก็เปลี่ยน locale เป็น th ดู

ไฟล์คำแปลดังกล่าวผมไปโหลดมาจาก github ซึ่งเป็นไฟล์ที่ Sven Fuchs เอามาจาก demo application (อ่านเพิ่ม) โดยการแปลในนั้นทำโดยคุณ Prem Sichanugrist (หรือคุณ Sikachu! นั่นเอง) อย่างไรก็ตาม ตอนผมเอามาลองแล้วพบว่าเหมือนการอ้างถึงสตริงใน active record มันจะเปลี่ยนไป ผมเลยแก้กลายมาเป็นไฟล์ด้านบนครับ (ส่ง patch ไปให้ Sven Fuchs แล้ว)

Advertisements

Dependency Injection กับ Ruby

(คัดลอกมาจาก http://www.rails66.com/blog/?p=593)

แนวคิดเกี่ยวกับ Dependency Injection เป็นแนวคิดที่สำคัญมากในการโปรแกรมสำหรับภาษาเช่น Java ด้วยสาเหตุหลาย ๆ ประการ

สาเหตุหนึ่งก็คือมันทำให้เราสามารถทำ unit test กับโปรแกรมที่มีการขึ้นต่อกันได้ ยกตัวอย่างเมท็อดด้านล่าง

public class RegistrationController {
	// ...
	void sendConfirmationEmail(User newUser) {
		MailSender sender = new MailSender();
		
		String msg = buildEmailMessage(newUser);
		sender.send(msg,myemail,newUser.getEmail());
	}
}

แทบเราจะไม่สามารถ test ได้เลยว่าเมท็อดดังกล่าวเรียก MailSender ได้ถูกต้องหรือเปล่า ที่ผมนึกออกคงจะต้องเข้าไปจัดการแก้โปรแกรมหลายจุดอยู่

ปัญหาก็มาจากการที่เมท็อดนี้สร้าง MailSender ขึ้นมาเอง ทำให้เราไม่สามารถเข้าไปแก้ไขได้ วิธีการที่นิยมใช้ในการจัดการเรื่องเหล่านี้ก็คือการแยกการขึ้นต่อกันของคลาส MailSender ออกมา โดยทำเป็นเมท็อดให้กำหนดค่าเข้าไป อาจจะที่ constructor หรือเขียนเป็นเมท็อดแยก หรือไม่ก็ใช้ DI framework ต่างๆ

เช่นแก้โปรแกรมเป็นแบบนี้

public class RegistrationController {
	// ...
	private MailSenderInterface mailSender;
	
	void setMailSender(MailSenderInterface sender) {
		mailSender = sender;
	}
	
	void sendConfirmationEmail(User newUser) {
		String msg = buildEmailMessage(newUser);
		mailSender.send(msg,myemail,newUser.getEmail());
	}
}

ในปัจจุบัน Java มี dependency injection framework หลายตัว (เท่าที่ผมทราบ) ซึ่งทำให้การ “ร้อย” (ของยืมพี่ป๊อกหน่อย) component ต่าง ๆ เข้าด้วยกันเป็นไปได้สะดวกมาก

แนวคิดดังกล่าวได้รับการตอบรับจากทางฝั่งนักพัฒนา Ruby เช่นเดียวกัน เช่น Jim Weirich ได้เขียนบล็อกเกี่ยวกับเรื่องนี้เอาไว้เมื่อปี 2004 ใน Ruby ก็มี framework ทำ DI อยู่หลายตัวเช่น Needle เขียนโดย Jamis Buck (คนทำ Capistrano) Jamis Buck ถึงขนาดเขียน di framework มาสองตัวเลยทีเดียว (อีกตัวคือ Copland)

อย่างไรก็ตาม ก็เป็นที่น่าสงสัยว่าทำไม DI framework ไม่เป็นที่นิยมใน Ruby

หนึ่งปีถัดมา Jim Weirich ได้ไปพูดที่ OSCON ในหัวข้อว่า “Dependency Injection: Vitally Important or Totally Irrelevant?” โดยสรุปว่าเนื่องจาก Ruby ไม่เหมือน Java ในปัจจุบันยังไม่เห็นความจำเป็นของ DI framework

Jamis Buck เองก็ออกมาเขียนถึงเรื่องดังกล่าวเช่นกัน โดยเขาแก้โปรแกรมในไลบรารี Net::SSH ใหม่ โดยเอา DI (ที่เขาเขียนเอง) ออก แล้วพบว่าโปรแกรมเล็กลงและอ่านง่ายขึ้น

ทำไม DI ดูเหมือนจะยังไม่จำเป็นใน Ruby?

พิจารณาจากตัวอย่างข้างต้น ถ้าเอาเมท็อด sendConfirmationEmail มาเขียนเป็นโปรแกรม Ruby จะได้ประมาณด้านล่างครับ

class RegistrationController
  # ..
  def send_confirmation_email(new_user)
    sender = MailSender.new
    msg = build_email_message(new_user)
    sender.send(msg, self.myemail, new_user.get_email)
  end
end

แล้วจะ test อย่างไร?

สิ่งที่เรามักจะลืมไปก็คือภาษาแต่ละภาษามีลักษณะที่แตกต่างกัน บางอย่างที่ไม่สามารถทำได้เลยในบางภาษา อาจเป็นสิ่งธรรมดามากในบางภาษา

ใน Ruby มีความสามารถ (หรือความบกพร่อง?) อย่างหนึ่งคือ Open Class

นั่นคือเราสามารถแกะคลาสมาแก้ได้ตลอดเวลา (รวมถึงตอน run-time) นอกจากนี้เรายังแก้ไขการทำงานของเมท็อดของแต่ละวัตถุได้โดยง่าย (ในระหว่างที่โปรแกรมทำงานอยู่เช่นกัน)

เมท็อดด้านบนถ้าจะเขียน test case ใน rspec ก็เป็นประมาณนี้ครับ

describe RegistrationController do
  #..
  it "should send mail to user's address from admin's mail" do
    my_email = 'jittat@internet.com'
    user_email = 'user@space.com'
    user = mock_model(User, :email =&gt; user_email)
    sender = mock(&quot;mock sender&quot;)
    sender.should_receive(:send).
      with(anything, my_email, user_email)
    MailSender.should_receive(:new).and_return(mock_sender)    
    controller = RegistrationController.new :adm_mail =&gt; my_email
    controller.send_confirmation_email(user)
  end
end

สังเกตว่าเนื่องจาก class และ object ใน ruby แก้ได้ตลอดเวลา mock framework จึงสามารถเข้าไปปรับแก้อะไรต่าง ๆ ได้มากมาย โดยไม่ต้องแยก dependency ออกมา

ไม่รู้ว่าผลที่ได้จะดีหรือไม่ดี? แต่ก็ทำให้ความจำเป็นของการใช้ DI framework ใน Ruby ลดลงไป

อ่านเพิ่มเติมได้ใน slide ของ Jim Weirich นะครับ อธิบายเห็นภาพมาก (มีอีกอันที่น่าสนใจเหมือนกันคือ 10 Things Every Java Programmer Should Know About Ruby ลองไปกดเล่นได้ครับ)

การใช้งาน Validation

(คัดลอกมาจาก http://www.rails66.com/blog/?p=496)

เวลาเราพัฒนาโปรแกรมประยุกต์ภายใต้กรอบงานแบบ MVC โดยเฉพาะบน Rails บางทีเราจะพบว่า model ของเรานั่นว่างโล่ง (เพราะว่าทำหน้าที่เชื่อมกับ table อย่างเดียว) ส่วน controller เราเต็มไปด้วยตรรกซับซ้อนซ่อนเงื่อน จนทำให้นึกไปถึงสมัยก่อนที่เขียนโปรแกรมแบบไม่มีโครงสร้างและใช้ goto กันจนโปรแกรมพันกันเป็นเส้นก๋วยเตี๋ยว

พักหลัง ๆ เลยมีคนพยายามบอกว่า model ควรจะอ้วน ๆ แต่ controller ควรจะผอมเพรียว (ตัวอย่างเพิ่มเติม)

แล้วอะไรบ้างที่สามารถนำไปอยู่ในโมเดลได้? ที่ชัดที่สุดก็คือการตรวจสอบความถูกต้องของข้อมูล (validation)

จริง ๆ แล้วในตัวอย่างและหนังสือ Rails แทบจะทุกเล่มทุกอันก็จะแสดงให้เห็นว่าการตรวจสอบความถูกต้องของข้อมูลทำได้ง่ายมากในโมเดล แต่บางทีเวลาเรารีบ ๆ เขียนก็มักจะลืม ๆ ไป หรือบางทีเราอาจจะมีการตรวจสอบอะไรที่แปลกไปจากรูปแบบการตรวจสอบพื้นฐานที่ Rails มีให้ เราก็เลยไปเขียนเอาไว้ใน controller เสียเลย

เราจะยกตัวอย่างจากโมเดล Book ที่มี attribute เป็น title และ page นะครับ

ลองดูโปรแกรมของ controller ด้านล่างที่รับค่าจาก form ผ่านทางเมท็อด create นะครับ

  def new
    @book = Book.new
  end

  def create
    @book = Book.new(params[:book])
    if @book.title==&quot;&quot;
      flash[:notice] = &quot;Error bad title&quot;
      render :action => 'new'
      return
    end
    if @book.pages==&quot;&quot;
      flash[:notice] = &quot;Error bad page number&quot;
      render :action => 'new'
      return
    end
    if @book.title[0]  ?Z
      flash[:notice] = &quot;Error title should begin with cap&quot;
      render :action => 'new'
      return
    end

    @book.save
    redirect_to :action => 'index'
  end

Controller ด้านบนมีการตรวจสอบสามอย่างคือ title ต้องไม่ว่าง, page ต้องไม่ว่าง, และ title ต้องขึ้นต้นด้วยตัวอักษรพิมพ์ใหญ่

เมื่อตรวจสอบผ่านแล้วก็จะเก็บข้อมูลไป ถ้ามีข้อผิดพลาดก็จะกลับไปแสดง view new กลับมาเหมือนเดิม เพื่อให้หน้า view แสดง flash[:notice] เพื่อบอกกับผู้ใช้ว่ามีข้อผิดพลาดทำให้เก็บค่าไม่ได้ และให้ผู้ใช้ป้อนค่าเข้ามาใหม่

ระบบการตรวจสอบข้อมูลของ Rails นั้นถูกออกแบบมาเพื่อให้ทำงานประสานกันตั้งแต่ใน model ไป controller ออกไป view ซึ่งเดียวเราจะได้ดูกันครับ

เราไปย้ายการตรวจสอบเข้าไปใน model กันครับ

ถ้าดู คู่มือ API ของ Rails จะพบว่าการตรวจสอบสองอย่างแรกเป็นการตรวจสอบมาตรฐานที่มีมาอยู่แล้ว ส่วนอันที่ 3 นั้นไม่มีทำให้ต้องเขียนเอง ดังนั้นผมขอสมมติว่าเราเลิกสนใจการตรวจสอบว่าชื่อหนังสือขึ้นด้วยตัวพิมพ์ใหญ่ไปก่อน

การตรวจสอบที่เกี่ยวข้องก็มี validates_presence_of (ตรวจสอบว่าไม่ว่าง นั่นคือไม่เป็น nil หรือ "") อีกอันก็คือ validates_numericality_of (ตรวจสอบว่าเป็นตัวเลข — สังเกตว่าเราตรวจได้ดีกว่าในตัวอย่าง controller ข้างต้น) เราก็ไปเพิ่ม validation ทั้งสองนี้ในโมเดลดังด้านล่างครับ

class Book < ActiveRecord::Base
  #...
  validates_presence_of :title
  validates_numericality_of :pages
  #...
end
&#91;/sourcecode&#93;

ส่วน validation ที่เราใส่ไปนี้จะทำงานเมื่อเราสั่ง <tt>save</tt> หรือเมื่อเราทดสอบว่าตรวจสอบผ่านหรือไม่ด้วยเมท็อด <tt>valid?</tt> (ซึ่งมักใช้ในกรณีที่ยังมีการทดสอบบางอย่างเหลืออยู่ที่ต้องเรียกเอง)  

ตัว controller ข้างต้นเราสามารถแก้ได้ดังนี้ครับ

  def create
    @book = Book.new(params[:book])
    if @book.save
      redirect_to :action => 'index'
    else
      render :action => 'new'
    end
  end

สังเกตว่าการตรวจสอบต่าง ๆ หายไปหมด (รวมถึงการจัดการพวกข้อความแสดงข้อผิดพลาดด้วย) ทีนี้ก่อนจะ save ข้อมูลในโมเดลของเราจะถูกตรวจสอบ ถ้าไม่ผ่านเมท็อด save จะคืนค่า false ทำให้เรากลับไปแสดงหน้า new ใหม่

ทีนี้ พวกข้อผิดพลาดต่าง ๆ ที่เกิดขึ้นระหว่างการตรวจสอบนั้น เราสามารถสั่งให้แสดงใน view ได้โดยเรียก error_messages_for สำหรับแสดงข้อผิดพลาดโดยรวมทั้งหมดของข้อมูลนั้น ๆ และ error_message_on ซึ่งจะแสดงข้อผิดพลาดเฉพาะ field ไป ตัวอย่างการใช้แสดงใน view new.html.erb ด้านล่าง

<h1>New Book</h1>

<%= error_messages_for :book %>

<% form_for :book,@book,:url =>{:action => 'create'} do |f| %>

  Title: <%= f.text_field :title %>   
  <%= error_message_on @book, 'title', 'The title ' %>
  <br/>
  Pages: <%= f.text_field :pages %>
  <%= error_message_on @book, 'pages', 'The number of pages ' %>
  <br/>
  <%= submit_tag %>
<% end %>

สังเกตการใช้งาน error_messages_for ที่อยู่ตรงหัว และ error_message_on ที่อยู่บริเวณ field ต่าง ๆ นะครับ ในส่วนของ error_message_on เรามีการแก้ให้แทนที่จะเรียก field ด้วยชื่อตรง ๆ ก็ให้เรียกเป็นภาษาที่ดูเป็นภาษาคนเสียหน่อยครับ ภาพด้านล่างแสดงตัวอย่างเวลาเกิด error ครับ (ปกติจะมีสีสรรค์สวยกว่านี้ครับ แต่ css ผมว่างเปล่าเลยดูไม่สวยเท่าใดครับ)

rails_ex-validation

ทีนี้ เหลือ validation อีกอันที่เราต้องการทำพิเศษ (custom) ก็ทำไม่ยากครับ วิธีการก็คือไปเขียนเมท็อดสำหรับตรวจสอบไว้ โดยเมท็อดนี้ถ้าพอข้อผิดพลาดก็ให้ใส่ข้อผิดพลาดนั้นลงใน attribute errors ด้วยเมท็อด add ครับ จากนั้นก็ไปบอก Active Record ให้ทำ validation ที่เราสร้างขึ้นมาโดยสั่ง validate ชื่อเมท็อด ทีตรงหัวโมเดล ตัวอย่างในด้านล่างครับ

class Book < ActiveRecord::Base
  #...
  validate :title_begins_with_capital_letters     # บอกให้ตรวจด้วย

  #...
  protected

  def title_begins_with_capital_letters
    return if self.title==nil
    return if self.title.length==0

    if self.title&#91;0&#93; < ?A or self.title&#91;0&#93; > ?Z
      # เพิ่ม errors
      errors.add &quot;title&quot;,&quot;must begin with a capital letter&quot;  
    end
  end
end

สังเกตว่าค่าที่คืนจากเมท็อดนี้จะไม่ถูกนำไปใช้ สิ่งที่สนใจคือ attribute errors อย่างเดียวเท่านั้น นอกจากการประกาศข้อผิดพลาดที่เกิดกับ field แล้ว เรายังเพิ่มข้อผิดพลาดให้แสดงในภาพรวมได้ ด้วยเมท็อด add_to_base ได้

โดยสรุปก็คือระบบ validation ของ Rails จะเริ่มที่การประกาศไว้ใน model (ด้วย validates_*) เรียกตรวจสอบใน controller (ด้วย save หรือ valid?) แล้วไปแสดงผลที่ใน view (ด้วย error_messages_for กับ error_message_on)

การใช้ partial และ helper

(คัดลอกจาก http://www.rails66.com/blog/?p=424)

Rails มีวิธีที่เราสามารถใช้เพื่อทำให้ view ของเราอ่านและจัดการได้ง่ายอยู่หลายวิธี

เราจะแสดงตัวอย่างโดยค่อย ๆ แก้ view ของเราด้านล่างนี้ที่ใช้แสดงข้อมูลของหนังสือกับผู้เขียนครับ

<ul>
  <% @books.each do |book| %>
    <li>
      <b><%= book.title %></b><br/>
      Written by:
      <%= (book.authors.collect do |a| 
             &quot;#{a.first_name} #{a.last_name}&quot;
           end).join(&quot;,&quot;) %>
    </li>
  <% end %>
<ul>

หน้าตาของ view เมื่อแสดงผลแล้วเป็นดังด้านล่างครับ

rails_partial

view นี้รับรายการ @books ที่ค้นมาจาก controller วัตถุ Book จะมีความสัมพันธ์ไปยังโมเดล Writer ผ่านทาง collection authors โดยวัตถุคลาส Writer จะมี first_name กับ last_name

view ด้านบนอาจจะดูสั้น ๆ แล้วไม่ซับซ้อนมากนัก แต่ว่ามีสิ่งที่เราทำให้ดีขึ้นได้หลายอย่าง

อย่างแรกคือใน view นี้มีโปรแกรม Ruby ที่ใช้จัด format รายการผู้เขียนเพื่อแสดงผล (บรรทัด 6-8) กล่าวคือในรายการผู้เขียน ถ้ามีหลายคนเราต้องการคั่นระหว่างชื่อด้วยเครื่องหมายลูกน้ำ (“,”) ใน view ข้างต้นก็เลยมีการเรียก book.authors.collect เพื่อเอาชื่อ (first_name) กับนามสกุล (last_name) มาต่อกัน แล้วมาเรียก join อีกที

เราจะย้ายส่วนของโปรแกรมนี้ออกไปนอก view ครับ โดยที่ที่เหมาะสมสำหรับมันก็คือใน helper (โดยปกติสำหรับ controller หนึ่ง ๆ จะมีโมดูล helper หนึ่งโมดูล โดย Rails จะสร้าง /app/helpers/ชื่อ_helper.rb ไว้ให้ครับ) ในที่นี้เราจะใส่ไว้ที่ books_helper.rb ดังด้านล่างครับ

module BooksHelper
  def format_authors(authors)
    authornames = authors.collect do |a| 
      &quot;#{a.first_name} #{a.last_name}&quot;
    end
    authornames.join(&quot;, &quot;)
  end
end

แล้วเราก็ไปตัดส่วนดังกล่าวออกจาก view ครับ ได้ผลดังด้านล่าง

<ul>
  <% @books.each do |book| %>
    <li>
      <b><%= book.title %></b><br/>
      Written by: <%= format_authors(book.authors) %>
    </li>
  <% end %>
<ul>

ทีนี้สังเกตว่า ถ้าในการแสดงผลหนังสือแต่ละเล่มค่อนข้างซับซ้อน จะทำให้ view นี้อ่านยาก (แต่ในกรณีของเราค่อนข้างอ่านง่ายแล้ว) เราสามารถย้ายส่วนดังกล่าวออกไปใส่ไว้ใน view เล็ก ๆ ที่ Rails เรียกว่า partial ได้ โดยเราจะสร้าง partial ชื่อ book โดยเก็บไว้ในแฟ้มชื่อ _book.html.erb ในไดเร็กทอรีเดียวกับ view เดิม

สังเกตว่า partial ชื่อ book แต่ชื่อไฟล์จะขึ้นต้นด้วยขีดล่าง เป็น _book.html.erb นะครับ ใน partial ดังกล่าวก็ตัดส่วนที่แสดงผล book ออกมาเลย ดังด้านล่าง

  <b><%= book.title %></b><br/>
  Written by: <%= format_authors(book.authors) %>

โดยปกติแล้ว partial จะได้รับตัวแปรภายในมาจาก view ด้วย (เช่นพวก @books) ในกรณีนี้ เราใช้ตัวแปร book ที่อ้างมาจากในวนรอบข้างต้นครับ ดังนั้นเราจะต้องส่งตัวแปรพวกนี้ให้กับ partial เอง ใน view หลัก เราจะเรียก render :partial โดยส่งค่าตัวแปร book ไปด้วย ผ่านทาง option locals ดังด้านล่างครับ

<ul>
  <% @books.each do |book| %>
    <li>
      <%= render :partial => 'book', 
                  :locals => {:book => book} %>
    </li>
  <% end %>
<ul>

นอกจากที่เราจะใช้ partial เพื่อแสดงผลบางส่วนของ view แล้ว คำสั่ง render :partial ยังมีอีก option หนึ่งที่มีประโยชน์มาก ก็คือการสั่งให้แสดงให้ทุก ๆ ข้อมูลในรายการ

จากตัวอย่างข้างต้น view หลักเราสามารถเขียนใหม่เหลือแค่

<ul>
  <%= render :partial => 'book', :collection => @books %>
<ul>

สังเกตว่าเราไม่ต้องมานั่ง for อีกต่อไปแล้ว

การทำงานของ render collection จะวิ่งเข้าไปในรายการแล้วเรียก render ข้อมูลแต่ละตัว เนื่องจากเราต้องการครอบทุก ๆ ข้อมูลด้วยแท็ก <li> เราจึงต้องย้ายไปไว้ใน partial _book.html.erb ดังด้านล่างครับ

 <li>
   <b><%= book.title %></b><br/>
   Written by: <%= format_authors(book.authors) %>
 </li>

ในการเรียกใช้ render collection เราใช้ข้อตกลงอย่างหนึ่งครับ นั่นคือข้อมูลแต่ละตัวจาก collection จะถูกส่งไปยัง partial ด้วยชื่อตัวแปรที่เป็นชื่อของ partial ครับ เช่น ในกรณีนี้คือตัวแปร book ครับ

ขอแถมนิดหนึ่งครับ ถ้าเขียน partial ด้วย haml ผลที่ได้จะยิ่งโล่งโปร่งสบายเข้าไปอีก ดังด้านล่างครับ

%li
  %b= book.title
  %br/
  Written by:
  = format_authors(book.authors)