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>
ผลลัพท์ที่ได้ก็จะประมาณนี้