Stimulus ปะทะ Turbo
จากบทความเรื่อง Turbo 👉 ลิงค์ และตัวอย่างการใช้งาน CustomEvent 👉 ลิงค์ ทำให้เกิดความคิดว่าน่าจะลองนำ Turbo มาใช้ในการแสดงเนื้อหาก็น่าจะได้นะ
และโจทย์ในวันนี้ต่อยอดจากครั้งที่แล้ว โดยเริ่มต้นเราจะถอด CustomEvent ที่ใช้สื่อสารกันระหว่าง controller ออก และใช้ Turbo Frame แทน จากนั้นเพิ่ม controller เข้าไปอีกตัวใช้สำหรับการดึงเนื้อหาย่อยมาแสดงอีกรอบหนึ่ง ซึ่งตรงนี้จะเห็นได้ว่าเราจะมี controller ซ้อน controller อยู่ เรามาดูกันว่าการสื่อสารระหว่าง controller จะเป็นอย่างไร
ก่อนอื่นขอย้อนกลับไปที่บทความก่อนหน้า เราได้ใช้ CustomEvent ในการสื่อสารกันระหว่าง controller ที่อยู่ในระนาบเดียวกัน แต่สำหรับบทความนี้เราจะใช้พูดถึงการสื่อสารระหว่าง controller ในแนวตั้ง ทั้งนี้ผมขอเรียกแบบนี้นะครับ
-
การสื่อสารในแนวราบ หรือการสื่อสารระหว่าง controller กับ controller ที่อยู่ข้างๆ กันหรือระนาบเดียวกัน
สำหรับการสื่อสารระหว่าง controller ในรูปแบบนี้ ผมก็จะเลือกใช้ CustomEvent ส่งระหว่าง controller A ไปยัง controller B หรือจาก controller B กลับไปยัง controller A
-
การสื่อสารในแนวตั้ง หรือการสื่อสารระหว่าง controller แม่ กับ controller ลูก
ลงมือแก้ไขกันเถอะ
- แรกเริ่มก็ให้เปลี่ยน
div
ที่ใช้แสดงเนื้อหาเป็นturbo_frame
และก็ให้เอา Sidebar Controller ออกเลย ระบุdata-turbo-frame
ในลิงค์ที่จะกดคลิกให้ชื่อตรงกับที่เราตั้งชื่อไว้ในturbo_frame
เพราะทันทีที่คลิกลิงค์ Turbo จะนำเนื้อหาที่ได้ไปแสดงยังdata-turbo-frame
ที่ระบุเอาไว้
index.html.erb
<main class="container section">
<div class="columns">
<div class="column is-one-third">
<aside class="menu">
<ul class="menu-list">
<li><a data-turbo-frame="content" href="/about">About</a></li>
<li><a data-turbo-frame="content" href="/contact">Contact</a></li>
</ul>
</aside>
</div>
<div class="column">
<%= turbo_frame_tag "data_content", class: "card", data: { controller: "content", "content-target": "body" }, src: about_path do %>
<% end %>
</div>
</div>
</main>
- ปรับแก้ view เล็กน้อย โดยการนำ
turbo_frame
ไปครอบเนื้อหาไว้ดังตัวอย่างหน้าabout.html.erb
about.html.erb
<%= turbo_frame_tag "content" do %>
<%= render 'pages/about' %>
<% end %>
เอาตรงๆ โค้ดเพียงแค่นี้เราก็สามารถเปลี่ยนเนื้อหาตามลิงค์ที่เราคลิกได้แล้ว โดยไม่ต้องใช้ CustomEvent ใดๆ เลย
-
ถัดมาเราจะสร้าง controller อีกตัวหนึ่งไว้สำหรับจัดการเมนูและดึงเนื้อหาย่อยมาแสดงในหน้า About โดยเราจะทำเป็นเมนูแบบ dropdown คือ Books, Blogs และ Activities ซึ่งแต่ละหน้าก็จะไปดึงหน้าย่อยที่แตกต่างกัน
-
ปกติแล้ว Turbo จะทำงานก็ต่อเมื่อเราใช้แท็ก
a
เท่านั้น แต่ในตัวอย่างเราจะใช้แท็กselect
ดังนั้นจะต้องใช้ controller เข้ามาช่วยในการเรียกใช้งาน Turbo
_about.html.erb
<div class="card" data-controller="colorize">
<div class="card-content" data-colorize-target="body">
<div class="content" data-controller="about" data-about-init-value="<%= books_path %>" data-action="loaded->colorize#loaded">
<h1>About</h1>
<p>My name is Karn Tirasoontorn</p>
<p>I am computer engineer who love to code with <strong class="has-text-primary-dark">Ruby</strong></p>
<div class="select">
<select data-action="change->about#change">
<option data-url="<%= books_path %>">Books</option>
<option data-url="<%= blogs_path %>">Blogs</option>
<option data-url="<%= activities_path %>">Activities</option>
</select>
</div>
<div class="content my-4">
<span class="tag">
Tag label
</span>
<%= turbo_frame_tag "about", src: books_path, data: { "about-target": "result" } do %>
<% end %>
</div>
</div>
</div>
</div>
จากส่วนของ HTML ข้างบนจะพบว่าเราจะใช้ controller ด้วยกัน 2 ตัวคือ
- ColorizeConroller จะใช้สำหรับเปลี่ยนสีพื้นหลัง
card
โดยจะเปลี่ยนก็ต่อเมื่อ AboutController ซึ่งเป็น controller ลูกเมื่อโหลดเนื้อหาเสร็จแล้ว ผ่าน CustomEvent ชื่อloaded
- AboutController จะคอยจัดโหลดเนื้อหา เมื่อมีการเลือกเมนู ทันทีที่เนื้อหาโหลดก็จะสร้าง CustomEvent ชื่อ
loaded
แล้วส่งออกไป
about_controller.rb
change (event) {
const selected = event.target[event.target.selectedIndex]
const url = selected.getAttribute('data-url')
this.loadContent(url)
}
loadContent (url) {
document.querySelector("turbo-frame[id='about']").src = url
this.dispatchLoadedEvent(url)
}
dispatchLoadedEvent (url) {
this.element.dispatchEvent(new CustomEvent("loaded", {
bubbles: true,
detail: { url: url }
}))
}
CustomEvent ที่สามารถส่งผ่านจากลูกไปถึงแม่ได้นั้นจะต้องกำหนด option
bubbules
เป็นtrue
ด้วยนะ
สำหรับ controller แม่สามารถรับรู้เหตุการณ์ที่ลูกผ่นออกได้ ด้วยการระบุชื่อเหตุการณ์ loaded
ไว้ใน data-action
ดังโค้ดด้านล่าง
...
<div class="content" data-controller="about" data-action="loaded->colorize#loaded">
...
</div>
ผลลัพท์ที่ได้ก็จะประมาณนี้