HEEx adalah bahasa template milik Phoenix untuk menulis HTML dengan ekspresi Elixir, dan LiveComponent adalah bentuk komponen yang menyimpan statenya sendiri. Topik ini membahas interpolasi dan atribut dinamis pada HEEx, direktif :if dan :for, function component yang didefinisikan dengan attr, lalu LiveComponent yang memiliki state internal. Semuanya diperagakan pada dua skrip utuh yang merender hasilnya ke HTML, sehingga keluaran setiap fitur dapat dilihat secara nyata.
Interpolasi, atribut dinamis, dan pelolosan#
Template HEEx ditulis di dalam sigil ~H, dan dua sintaks interpolasi tersedia di dalamnya. Bentuk kurung kurawal {@hitung} adalah sintaks modern, sedangkan <%= @hitung %> adalah bentuk lama yang setara dan masih banyak digunakan. Keduanya mengeksekusi satu ekspresi Elixir lalu menulis hasilnya ke keluaran HTML, dan keduanya melakukan pelolosan (escape) secara otomatis, yaitu mengubah karakter <, &, dan tanda kutip menjadi entitas HTML sebelum keluaran dikirim.
Pelolosan otomatis adalah fitur keamanan yang berjalan tanpa pengaturan apa pun. Nilai assigns yang berisi <b>diperbarui otomatis</b> dirender sebagai teks <b>diperbarui otomatis</b>, sehingga data pengguna tidak pernah dapat menyisipkan tag HTML atau skrip ke halaman. Apabila HTML mentah memang diinginkan, nilainya harus dibungkus Phoenix.HTML.raw/1 secara sengaja, dan keputusan itu menjadi tanggung jawab eksplisit penulisnya.
Atribut juga dapat ditulis secara dinamis. Bentuk id={dom_id} menyetel nilai atribut dari ekspresi Elixir, sedangkan bentuk class={["kartu", @nilai >= 75 && "tinggi"]} menggabungkan daftar kelas dan melewatkan elemen yang bernilai false atau nil. Direktif :for mengulang elemen untuk setiap item koleksi, dan :if merender elemen hanya ketika kondisinya benar:
<div class={["kartu", @nilai >= 75 && "tinggi"]}>
<span>{@nama}</span>
<strong>{@nilai}</strong>
</div>
<p :if={Enum.empty?(@siswa)}>belum ada nilai yang tercatat</p>
<li :for={s <- @siswa}>{s.nama}</li>Function component#
Function component adalah fungsi biasa yang menerima assigns dan mengembalikan template HEEx, dengan deklarasi attr di atas tubuh fungsinya. Deklarasi attr :nama, :string, required: true menetapkan kontrak atribut, dan pemanggilan yang tidak mengirim atribut wajib tertangkap sebagai error pada saat kompilasi. Bentuk pemanggilannya menyerupai tag HTML, yaitu <.kartu nama="Ali" nilai={85} /> untuk komponen yang didefinisikan pada modul yang sama, atau <Komponen.kartu nama="Ali" nilai={85} /> untuk komponen yang didefinisikan pada modul lain.
Function component tidak menyimpan state apa pun. Setiap render memanggil ulang fungsi tersebut dengan assigns terbaru, sehingga komponen ini hanya tepat untuk bagian antarmuka yang nilainya sepenuhnya ditentukan oleh masukannya, misalnya kartu nilai, tombol, atau baris tabel. Kebutuhan state internal ditangani LiveComponent.
Skrip utuh: papan nilai dengan kartu#
Skrip berikut menyusun papan nilai siswa dari function component kartu. Direktif :for mengulang kartu untuk tiap siswa, atribut class menyusun daftar kelas secara dinamis, :if menyiapkan pesan untuk daftar kosong, dan interpolasi @catatan memperlihatkan pelolosan otomatis. Render kedua mengulang template dengan daftar siswa yang dikosongkan agar cabang :if terlihat:
# heex.exs — interpolasi, atribut dinamis, :for/:if, dan function component dalam HEEx
Mix.install([{:phoenix_live_view, "~> 1.1"}, {:jason, "~> 1.4"}])
Logger.configure(level: :warning)
defmodule PapanLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
siswa = [
%{nama: "Ali", nilai: 85},
%{nama: "Budi", nilai: 58}
]
catatan = "<b>diperbarui otomatis</b> setiap & kali"
{:ok, assign(socket, judul: "Papan nilai kelas 10A", siswa: siswa, catatan: catatan)}
end
# function component: fungsi yang menerima assigns dan mengembalikan template HEEx
attr :nama, :string, required: true
attr :nilai, :integer, required: true
def kartu(assigns) do
~H"""
<div class={["kartu", @nilai >= 75 && "tinggi"]}>
<span>{@nama}</span>
<strong>{@nilai}</strong>
</div>
"""
end
def render(assigns) do
~H"""
<h2>{@judul}</h2>
<.kartu :for={s <- @siswa} nama={s.nama} nilai={s.nilai} />
<p :if={Enum.empty?(@siswa)}>belum ada nilai yang tercatat</p>
<p>{@catatan}</p>
"""
end
end
Code.ensure_loaded(Phoenix.LiveViewTest)
{:ok, socket} = PapanLive.mount(%{}, %{}, struct(Phoenix.LiveView.Socket))
IO.puts(Phoenix.LiveViewTest.rendered_to_string(PapanLive.render(socket.assigns)))
# kondisi :if baru terlihat saat daftarnya kosong
IO.puts(Phoenix.LiveViewTest.rendered_to_string(PapanLive.render(Map.put(socket.assigns, :siswa, []))))<h2>Papan nilai kelas 10A</h2>
<div class="kartu tinggi">
<span>Ali</span>
<strong>85</strong>
</div><div class="kartu ">
<span>Budi</span>
<strong>58</strong>
</div>
<p><b>diperbarui otomatis</b> setiap & kali</p>
<h2>Papan nilai kelas 10A</h2>
<p>belum ada nilai yang tercatat</p>
<p><b>diperbarui otomatis</b> setiap & kali</p>Semua perilaku yang dibahas muncul pada keluaran itu. Ali dengan nilai 85 memperoleh class="kartu tinggi", sedangkan Budi dengan nilai 58 memperoleh class="kartu " dengan satu spasi sisa, karena elemen false pada daftar kelasnya menyisakan slot kosong. Teks catatan tercetak dalam bentuk terloloskan, dan render kedua menghapus kedua kartu serta menampilkan pesan :if. Skripnya dapat dijalankan ulang dari terminal:
# menjalankan skrip HEEx dari folder topik
elixir heex.exs<h2>Papan nilai kelas 10A</h2>
<div class="kartu tinggi">
<span>Ali</span>
<strong>85</strong>
</div><div class="kartu ">
<span>Budi</span>
<strong>58</strong>
</div>
<p><b>diperbarui otomatis</b> setiap & kali</p>
<h2>Papan nilai kelas 10A</h2>
<p>belum ada nilai yang tercatat</p>
<p><b>diperbarui otomatis</b> setiap & kali</p>LiveComponent: komponen stateful#
LiveComponent didefinisikan dengan use Phoenix.LiveComponent dan menyimpan state di socket milik komponen, terpisah dari state LiveView pemanggilnya. Callback pembukanya adalah update/2 yang menerima assigns dari pemanggil, sedangkan handle_event/3 milik komponen menangani event yang ditargetkan kepadanya. Agar state tiap komponen dapat dibedakan, setiap pemanggilan wajib mengirim atribut id, dan dua pemanggilan dengan id berbeda menghasilkan dua instance yang statenya benar-benar terpisah.
Skrip berikut memuat komponen kotak penghitung, lalu membuat dua instance dari modul yang sama dan meneruskan dua event tambah hanya ke instance pertama:
# komponen.exs — LiveComponent dengan state lokal terpisah untuk dua instance
Mix.install([{:phoenix_live_view, "~> 1.1"}, {:jason, "~> 1.4"}])
Logger.configure(level: :warning)
defmodule KotakLive do
use Phoenix.LiveComponent
# update/2 adalah "mount" LiveComponent: dipanggil dengan assigns yang dikirim pemanggil
def update(assigns, socket) do
{:ok, assign(socket, id: assigns.id, label: assigns.label, hitung: 0)}
end
def handle_event("tambah", _params, socket) do
{:noreply, update(socket, :hitung, &(&1 + 1))}
end
def render(assigns) do
~H"""
<div id={@id} class="kotak">
<span>{@label}</span>
<button phx-click="tambah">Tambah</button>
<span>hitung: {@hitung}</span>
</div>
"""
end
end
Code.ensure_loaded(Phoenix.LiveViewTest)
# dua instance dari modul komponen yang sama, masing-masing dengan socketnya sendiri
{:ok, kotak_a} = KotakLive.update(%{id: "kotak-a", label: "Kelas A"}, struct(Phoenix.LiveView.Socket))
{:ok, kotak_b} = KotakLive.update(%{id: "kotak-b", label: "Kelas B"}, struct(Phoenix.LiveView.Socket))
# event hanya diteruskan ke instance kotak A, dua kali
{:noreply, kotak_a2} = KotakLive.handle_event("tambah", %{}, kotak_a)
{:noreply, kotak_a3} = KotakLive.handle_event("tambah", %{}, kotak_a2)
IO.puts(Phoenix.LiveViewTest.rendered_to_string(KotakLive.render(kotak_a3.assigns)))
IO.puts(Phoenix.LiveViewTest.rendered_to_string(KotakLive.render(kotak_b.assigns)))<div id="kotak-a" class="kotak">
<span>Kelas A</span>
<button phx-click="tambah">Tambah</button>
<span>hitung: 2</span>
</div>
<div id="kotak-b" class="kotak">
<span>Kelas B</span>
<button phx-click="tambah">Tambah</button>
<span>hitung: 0</span>
</div>Keluaran menunjukkan sifat utama LiveComponent, yaitu state lokal per instance. Kedua kotak dibuat dari satu modul yang sama, tetapi dua event hanya mengubah kotak A menjadi hitung: 2, sedangkan kotak B tetap hitung: 0. Pada aplikasi nyata, pemisahan target event dinyatakan dengan atribut phx-target={@myself} pada elemen pemicunya, sehingga klik tombol satu kotak tidak pernah sampai ke kotak lain:
<.live_component module={KotakLive} id="kotak-a" label="Kelas A" />
<.live_component module={KotakLive} id="kotak-b" label="Kelas B" />Skrip LiveComponent dijalankan dengan cara yang sama seperti skrip lain pada modul ini:
# menjalankan skrip LiveComponent dari folder topik
elixir komponen.exs<div id="kotak-a" class="kotak">
<span>Kelas A</span>
<button phx-click="tambah">Tambah</button>
<span>hitung: 2</span>
</div>
<div id="kotak-b" class="kotak">
<span>Kelas B</span>
<button phx-click="tambah">Tambah</button>
<span>hitung: 0</span>
</div>Kesalahan umum#
Kesalahan pertama adalah lupa mengirim atribut id saat memanggil LiveComponent. Atribut itu adalah identitas instance bagi kerangka, sehingga pemanggilan tanpa id membuat komponen tidak dapat dilacak dan eventnya tidak dapat ditujukan. Konvensi yang baik adalah menggunakan kunci utama data, misalnya id={"kotak-#{s.kelas}"}, agar identitasnya stabil dari satu render ke render berikutnya.
Kesalahan kedua adalah mengira function component dapat menyimpan state. Assigns milik function component dibangun ulang dari assigns pemanggilnya pada setiap render, sehingga nilai yang dihitung di dalamnya hilang seketika. Apabila sebuah bagian antarmuka perlu menyimpan sesuatu di antara dua event, pindahkan ke LiveComponent atau naikkan statenya ke LiveView dan kirim kembali sebagai properti.
Kesalahan ketiga adalah mengira interpolasi yang memuat tag akan dirender sebagai HTML. Interpolasi {@catatan} selalu meloloskan isinya, sehingga string yang memuat tag tampil sebagai teks biasa, persis seperti baris <b>diperbarui otomatis</b> pada keluaran skrip. Pembungkus Phoenix.HTML.raw/1 hanya digunakan untuk konten yang pembuatannya sepenuhnya terkendali, dan penggunaannya pada konten pengguna adalah celah keamanan.
Ringkasan#
- HEEx menginterpolasi ekspresi Elixir dengan
{...}atau<%= %>, meloloskan semuanya secara otomatis, serta menyediakan atribut dinamis dan direktif:ifdan:for. - Function component adalah fungsi bertanda
attrtanpa state, dipanggil sebagai<.nama />atau<Modul.nama />, dan kontrak atributnya diperiksa saat kompilasi. - LiveComponent didefinisikan dengan
use Phoenix.LiveComponent, memulai statenya diupdate/2, dan setiap instance wajib diberiidagar statenya terpisah. - Event LiveComponent ditujukan per instance lewat
phx-target={@myself}, sehingga dua komponen sejenis tidak pernah saling menukar statenya.
Referensi#
- Assigns and HEEx templates — Hexdocs Phoenix LiveView — panduan resmi interpolasi, atribut dinamis, dan pelolosan HEEx.
- Modul Phoenix.Component — Hexdocs Phoenix LiveView
— dokumentasi
attr, direktif:ifdan:for, sertalive_component. - Modul Phoenix.LiveComponent — Hexdocs Phoenix LiveView
— kontrak
update/2,handle_event/3, dan atributid.