mhanberg / mhanberg/temple

Ordering seems to matter when calling components with named slots

Open
#269 4 comments 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.