Ordering seems to matter when calling components with named slots
Nobody has claimed this yet.
- Dominant language
- Elixir
- Stars
- 514
- Forks
- 18
- PR merge metrics
- No merged PRs in 30d
Description
Hey @mhanberg, have another question for you – hope these are helpful rather than tiresome.
I'm trying to convert the default Phoenix login form to Temple, and receiving some compiler errors:
Original (HEEx)
defmodule ExampleWeb.UserLoginLive do
use ExampleWeb, :live_view
def render(assigns) do
~H"""
<div class="mx-auto w-96 mt-16">
<.header class="text-center">
Log in to account
<:subtitle>
Don't have an account?
<.link navigate={~p"/users/register"} class="font-semibold text-brand hover:underline">
Sign up
</.link>
for an account now.
</:subtitle>
</.header>
<.simple_form for={@form} id="login_form" action={~p"/users/login"} phx-update="ignore">
<.input field={@form[:email_or_username]} type="text" label="Email or username" required />
<.input field={@form[:password]} type="password" label="Password" required />
<:actions>
<.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
<.link href={~p"/users/reset_password"} class="text-sm font-semibold">
Forgot your password?
</.link>
</:actions>
<:actions>
<.button phx-disable-with="Logging in..." class="w-full">
Log in <span aria-hidden="true">→</span>
</.button>
</:actions>
</.simple_form>
</div>
"""
end
def mount(_params, _session, socket) do
email_or_username = Phoenix.Flash.get(socket.assigns.flash, :email_or_username)
form = to_form(%{"email_or_username" => email_or_username}, as: "user")
{:ok, assign(socket, form: form), temporary_assigns: [form: form]}
end
end
Converted – Won't compile (Temple)
defmodule ExampleWeb.UserLoginLive do
use ExampleWeb, :live_view
def render(assigns) do
temple do
div class: "mx-auto w-96 mt-16" do
c &header/1, class: "text-center" do
"Log in to account"
slot :subtitle do
"Don't have an account?"
c &link/1,
navigate: ~p"/users/register",
class: "font-semibold text-brand hover:underline",
do: "Sign up"
"for an account now."
end
end
c &simple_form/1,
for: @form,
id: "login_form",
action: ~p"/users/login",
"phx-update": "ignore"
do
c &input/1,
field: @form[:email_or_username],
type: "text",
label: "Email or username",
required: true
c &input/1,
field: @form[:password],
type: "password",
label: "Password",
required: true
slot :actions do
c &input/1,
field: @form[:remember_me],
type: "checkbox",
label: "Keep me logged in"
c &link/1,
href: ~p"/users/reset_password",
class: "text-sm font-semibold",
do: "Forgot your password?"
end
slot :actions do
c &button/1, "phx-disable-with": "Logging in...", class: "w-full" do
"Log in"
span "aria-hidden": true, do: "→"
end
end
end
end
end
end
def mount(_params, _session, socket) do
email_or_username = Phoenix.Flash.get(socket.assigns.flash, :email_or_username)
form = to_form(%{"email_or_username" => email_or_username}, as: "user")
{:ok, assign(socket, form: form), temporary_assigns: [form: form]}
end
end
With this converted template, I receive these compiler errors (and warning) when trying to run mix phx.server:
$ mix phx.server
Compiling 25 files (.ex)
warning: code block contains unused literal "Log in" (remove the literal or assign it to _ to avoid warnings)
│
4 │ def render(assigns) do
│ ~~~~~~~~~~~~~~~~~~~~~~
│
└─ lib/example_web/live/user_login_live.ex:4: ExampleWeb.UserLoginLive.render/1
error: undefined function c/3 (expected ExampleWeb.UserLoginLive to define such a function or for it to be imported, but none are available)
│
53 │ c &button/1, "phx-disable-with": "Logging in...", class: "w-full" do
│ ^
│
└─ lib/example_web/live/user_login_live.ex:53:13: ExampleWeb.UserLoginLive.render/1
error: undefined function span/1 (expected ExampleWeb.UserLoginLive to define such a function or for it to be imported, but none are available)
│
55 │ span "aria-hidden": true, do: "→"
│ ^^^^
│
└─ lib/example_web/live/user_login_live.ex:55:15: ExampleWeb.UserLoginLive.render/1
error: undefined function c/2 (expected ExampleWeb.UserLoginLive to define such a function or for it to be imported, but none are available)
│
46 │ c &link/1,
│ ^
│
└─ lib/example_web/live/user_login_live.ex:46:13: ExampleWeb.UserLoginLive.render/1
error: undefined function c/2 (expected ExampleWeb.UserLoginLive to define such a function or for it to be imported, but none are available)
│
41 │ c &input/1,
│ ^
│
└─ lib/example_web/live/user_login_live.ex:41:13: ExampleWeb.UserLoginLive.render/1
== Compilation error in file lib/example_web/live/user_login_live.ex ==
** (CompileError) lib/example_web/live/user_login_live.ex: cannot compile module ExampleWeb.UserLoginLive (errors have been logged)
Fortunately, a reasonable workaround seems to exist – move all named slot usages to the top of the component inner block. This resolves the above errors and warning:
Converted – Compiles successfully (Temple)
defmodule ExampleWeb.UserLoginLive do
use ExampleWeb, :live_view
def render(assigns) do
temple do
div class: "mx-auto w-96 mt-16" do
c &header/1, class: "text-center" do
slot :subtitle do
"Don't have an account?"
c &link/1,
navigate: ~p"/users/register",
class: "font-semibold text-brand hover:underline",
do: "Sign up"
"for an account now."
end
"Log in to account"
end
c &simple_form/1,
for: @form,
id: "login_form",
action: ~p"/users/login",
"phx-update": "ignore"
do
slot :actions do
c &input/1,
field: @form[:remember_me],
type: "checkbox",
label: "Keep me logged in"
c &link/1,
href: ~p"/users/reset_password",
class: "text-sm font-semibold",
do: "Forgot your password?"
end
slot :actions do
c &button/1, "phx-disable-with": "Logging in...", class: "w-full" do
"Log in"
span "aria-hidden": true, do: "→"
end
end
c &input/1,
field: @form[:email_or_username],
type: "text",
label: "Email or username",
required: true
c &input/1,
field: @form[:password],
type: "password",
label: "Password",
required: true
end
end
end
end
def mount(_params, _session, socket) do
email_or_username = Phoenix.Flash.get(socket.assigns.flash, :email_or_username)
form = to_form(%{"email_or_username" => email_or_username}, as: "user")
{:ok, assign(socket, form: form), temporary_assigns: [form: form]}
end
end
All in all, not a terrible workaround, and fine for me for the time being. But this does seem like a bit of a rough edge, and not something I saw mentioned in the docs (though I very easily could have missed it).
Does this seem expected/intentional, or possibly fixable to you?
Desktop (please complete the following information):
- OS: MacOS Monterey (12.3)
- Temple Version
{:temple, "~> 0.14.1"} - Elixir Version
- Erlang Version
$ elixir --version
Erlang/OTP 27 [erts-15.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]
Elixir 1.18.2 (compiled with Erlang/OTP 27)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the reported Temple 0.14.1 template with the named slots in the order shown and run mix phx.server to observe the compiler errors and warning. Compare it with the reordered workaround; done means the original ordering compiles without those errors or warning.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- elixir
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100