Template dan layout

HEEx serta layout yang disederhanakan pada Phoenix 1.8.

Daftar isi

HEEx adalah bahasa template Phoenix untuk menyusun HTML dengan interpolasi nilai yang aman dan struktur yang dapat dikomposisi. Topik ini membahas sintaks HEEx yang digunakan sehari-hari, komponen fungsi dengan atribut, serta struktur layout yang disederhanakan pada Phoenix 1.8. Setelah membaca, template *.html.heex pada proyek dapat dibaca dan ditulis, termasuk menelusuri hubungan antara root layout dan app layout.

Interpolasi dan atribut dinamis#

HEEx ditulis dalam sigil ~H pada modul Elixir atau dalam berkas *.html.heex yang disematkan oleh modul view. Nilai Elixir disisipkan dengan kurung kurawal, misalnya {@judul}, sedangkan ekspresi Elixir lengkap menggunakan tanda <%= %>. Keduanya menyandikan nilai secara otomatis, sehingga string yang berisi karakter HTML tidak dapat menyusup sebagai markah. Sifat ini menjaga template tetap aman dari serangan injeksi tanpa usaha tambahan.

Atribut HTML juga menerima nilai dinamis dengan kurung kurawal, misalnya <a href={~p"/catatan/#{c.id}"}>. Sigil ~p memverifikasi jalur tersebut terhadap rute yang terdaftar pada waktu kompilasi, sehingga salah ketik rute terdeteksi sebelum aplikasi berjalan. Kontrol perulangan dan percabangan pun berbentuk atribut, yaitu :for untuk perulangan dan :if untuk percabangan, sebagaimana ditunjukkan pada skrip berikut.

Delegasi ke komponen lain juga berlangsung dengan sintaks yang sama. Komponen dipanggil seperti tag HTML dengan awalan titik, misalnya <.sapa nama="Ali" />, dan atributnya divalidasi terhadap deklarasi attr komponen tersebut. Validasi ini menjangkau tipe atribut, sehingga atribut yang dideklarasikan :string tidak menerima daftar maupun bilangan. Efeknya, kesalahan yang paling sering terjadi pada template, yaitu nama nilai yang berubah tanpa semua pemanggilnya ikut diubah, terdeteksi saat kompilasi.

Merender HEEx ke string#

Skrip berikut mendefinisikan dua komponen HEEx, lalu merendernya menjadi string HTML. Komponen sapa/1 memiliki atribut wajib nama, sedangkan daftar/1 menggunakan :for dan :if sekaligus.

elixir sapaan.exs
Mix.install([{:phoenix_live_view, "~> 1.1"}, {:jason, "~> 1.4"}])
Logger.configure(level: :warning)

defmodule SapaanHTML do
  use Phoenix.Component

  attr :nama, :string, required: true

  def sapa(assigns) do
    ~H"""
    <h1>Selamat belajar, {@nama}</h1>
    """
  end

  def daftar(assigns) do
    ~H"""
    <ul>
      <li :for={c <- @catatan}>{c.judul} <em :if={c.draf}>(draf)</em></li>
    </ul>
    """
  end
end

Code.ensure_loaded(Phoenix.LiveViewTest)

IO.puts(Phoenix.LiveViewTest.rendered_to_string(SapaanHTML.sapa(%{nama: "Ali"})))

assigns = %{catatan: [%{judul: "Puisi hujan", draf: true}, %{judul: "Rumput beton", draf: false}]}
IO.puts(Phoenix.LiveViewTest.rendered_to_string(SapaanHTML.daftar(assigns)))

Jalankan skripnya dengan elixir sapaan.exs. Keluarannya berupa string HTML persis hasil template, termasuk perulangan yang melebarkan menjadi dua butir <li> dan percabangan yang hanya menampilkan tanda draf untuk catatan pertama.

text Keluaran
<h1>Selamat belajar, Ali</h1>
<ul>
  <li>Puisi hujan <em>(draf)</em></li><li>Rumput beton </li>
</ul>

Makro attr di atas fungsi komponen mendeklarasikan atribut beserta tipenya dan keharusannya. Pemanggilan yang tidak menyertakan atribut wajib gagal pada waktu kompilasi, dan validasi ini berlaku juga pada template .heex yang disematkan oleh modul view. Fungsi Phoenix.LiveViewTest.rendered_to_string/1 digunakan di sini agar hasil HEEx dapat diperiksa sebagai string biasa dalam skrip mandiri. Pemanggilan Code.ensure_loaded/1 sebelum fungsi tersebut menjamin modul penguji sudah dimuat pada skrip tanpa proyek Mix, dan bantuan yang sama digunakan oleh rangkaian tes LiveView untuk memeriksa hasil render.

Root layout dan app layout pada Phoenix 1.8#

Phoenix 1.8 menyederhanakan layout menjadi dua bagian. Root layout berada di berkas components/layouts/root.html.heex dan memuat kerangka dokumen, yaitu <head> dengan judul, CSS, dan JavaScript. Berikut potongan aslinya dari proyek hasil generator Phoenix 1.8:

html Potongan root.html.heex proyek Phoenix 1.8
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title default="NamaProyek" suffix=" · Phoenix Framework">
      {assigns[:page_title]}
    </.live_title>
    <link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
  </head>
  <body>
    {@inner_content}
  </body>
</html>

Penyederhanaan terjadi pada app layout. Pada Phoenix 1.8, app layout bukan berkas .heex terpisah lagi, melainkan komponen fungsi bernama app/1 di dalam modul Layouts. Setiap template halaman membuka komponen tersebut secara eksplisit, sebagaimana tampak pada potongan new.html.heex hasil phx.gen.auth:

elixir Pembuka templat halaman yang membungkus diri dengan Layouts.app
# tidak-dijalankan (fragmen templat, bukan skrip utuh)
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
  <.header>
    <p>Log in</p>
  </.header>
  <!-- isi halaman -->
</Layouts.app>
"""

Komponen app/1 menerima slot inner_block dan merendernya dengan {render_slot(@inner_block)}, lalu mengelilingi isi dengan menu dan area utama. Pesan flash dirender oleh <.flash_group flash={@flash} /> yang juga berada di dalam app/1. Pola pembungkusan eksplisit ini membuat template halaman menetapkan sendiri layout yang digunakannya. Root layout tetap menjadi kerangka terluar yang disetel oleh pipeline :browser melalui plug :put_root_layout.

Pemisahan dua lapisan layout ini memiliki konsekuensi praktis pada perilaku respons HTTP. Karena root layout disetel oleh plug pada pipeline, rute API yang tidak melewati :browser tidak membawa kerangka dokumen sama sekali, sehingga respons JSON tidak pernah terbungkus <html>. Sementara itu, halaman yang membutuhkan kerangka berbeda, misalnya halaman cetak tanpa menu, cukup membungkus dirinya dengan komponen lain tanpa mengubah pipeline.

Kesalahan umum#

Kesalahan pertama adalah mencampur sintaks interpolasi. Kurung kurawal hanya berlaku di dalam sigil ~H dan berkas .heex, sehingga template .eex lama yang berisi {@nilai} menampilkan kurung kurawal apa adanya. Pastikan berkas memiliki akhiran .html.heex sebelum menggunakan sintaks HEEx.

Kesalahan kedua adalah mengakses assign yang belum dikirim. Setiap template hanya melihat nilai yang dikirim pemanggilnya, misalnya render(conn, :new, form: form) membuat @form tersedia, sedangkan @current_scope hanya ada apabila pipeline mengisinya. Mengakses assign yang tidak ada melempar error KeyError, dan perbaikannya adalah mengirim nilai tersebut atau mendeklarasikan nilai bawaan pada makro attr.

Kesalahan ketiga adalah memindahkan isi halaman ke root layout. Root layout memuat kerangka dokumen dan berlaku untuk semua respons HTML, termasuk halaman error, sehingga menu aplikasi yang ditaruh di sana juga muncul pada halaman error. Menu dan bagian yang terikat aplikasi ditempatkan pada komponen app/1, dan isi halaman diteruskan melalui slot inner_block.

Ringkasan#

  • HEEx menyandikan interpolasi secara otomatis, baik melalui {nilai} maupun <%= ekspresi %>, dengan :for dan :if sebagai kontrol template.
  • Komponen fungsi mendeklarasikan atributnya dengan makro attr dan divalidasi pada waktu kompilasi.
  • Layout Phoenix 1.8 terdiri atas root.html.heex sebagai kerangka dokumen dan komponen Layouts.app sebagai pembungkus isi halaman melalui slot.
  • Template halaman membungkus dirinya dengan <Layouts.app ...> dan pesan flash dirender oleh <.flash_group> di dalamnya.

Referensi#