Skip to content

Commit 98076c7

Browse files
authored
Merge pull request #13 from plausible/render-as
refactor: extract render_as helper for consistent component rendering
2 parents 0dd2897 + 44613bf commit 98076c7

File tree

5 files changed

+83
-45
lines changed

5 files changed

+83
-45
lines changed

demo/test/wallaby/demo_web/dropdown_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ defmodule DemoWeb.DropdownTest do
66
@dropdown_menu Query.css("#dropdown [role=menu]")
77
@dropdown_items Query.css("#dropdown [role=menuitem]")
88

9+
feature "default dropdown trigger has type='button'", %{session: session} do
10+
session
11+
|> visit_fixture("/fixtures/dropdown", "#dropdown")
12+
|> assert_has(Query.css("#dropdown button[aria-haspopup=menu][type=button]"))
13+
end
14+
915
feature "shows dropdown menu when button is clicked", %{session: session} do
1016
session
1117
|> visit_fixture("/fixtures/dropdown", "#dropdown")

demo/test/wallaby/demo_web/modal_title_test.exs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,19 @@ defmodule DemoWeb.ModalTitleTest do
4545
import Prima.Modal
4646

4747
assert_raise RuntimeError,
48-
"Cannot render modal title `as` 123. Expected a function or string",
48+
"Cannot render component `as` 123. Expected a function or string",
4949
fn ->
5050
render_component(&modal_title/1, %{as: 123}, %{})
5151
end
5252

5353
assert_raise RuntimeError,
54-
"Cannot render modal title `as` []. Expected a function or string",
54+
"Cannot render component `as` []. Expected a function or string",
5555
fn ->
5656
render_component(&modal_title/1, %{as: []}, %{})
5757
end
5858

5959
assert_raise RuntimeError,
60-
"Cannot render modal title `as` %{}. Expected a function or string",
60+
"Cannot render component `as` %{}. Expected a function or string",
6161
fn ->
6262
render_component(&modal_title/1, %{as: %{}}, %{})
6363
end

lib/prima/component.ex

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
defmodule Prima.Component do
2+
@moduledoc false
3+
import Phoenix.Component, only: [dynamic_tag: 1]
4+
5+
@doc """
6+
Renders a component using either a custom component function, a custom tag name, or a default HTML tag.
7+
8+
This helper function provides a consistent pattern for components that support
9+
the `as` attribute for custom rendering.
10+
11+
## Parameters
12+
13+
* `assigns` - The component assigns map
14+
* `default_tag_attrs` - Map of attributes for the default tag, including `:tag_name`.
15+
This map is passed directly to `dynamic_tag/1` when rendering the default element.
16+
Attributes in this map are NOT passed to custom components.
17+
18+
## Returns
19+
20+
Returns the rendered component by either:
21+
* Calling the custom component function with merged assigns (if `as` is a function)
22+
* Rendering a dynamic tag with the custom tag name (if `as` is a string)
23+
* Rendering a dynamic tag with default_tag_attrs (if `as` is not provided)
24+
25+
## Behavior
26+
27+
1. Pops the `:as` attribute from assigns
28+
2. Pops and merges the `:rest` attributes into assigns
29+
3. If `as` is a function, calls it with the merged assigns (without default_tag_attrs)
30+
4. If `as` is a string, renders a dynamic tag with that tag name (without default_tag_attrs)
31+
5. Otherwise, renders a dynamic tag with default_tag_attrs merged into assigns
32+
33+
## Examples
34+
35+
# With default button element
36+
def my_button(assigns) do
37+
assigns = assign(assigns, %{"aria-label": "My Button"})
38+
render_as(assigns, %{tag_name: "button", type: "button"})
39+
end
40+
41+
# Can be used with as={&custom_component/1}
42+
# Custom component won't receive the type="button" attribute
43+
<.my_button as={&custom_component/1} />
44+
45+
# Can be used with as="span"
46+
# Renders a span without type="button"
47+
<.my_button as="span" />
48+
49+
"""
50+
def render_as(assigns, default_tag_attrs) when is_map(default_tag_attrs) do
51+
{as, assigns} = Map.pop(assigns, :as)
52+
{rest, assigns} = Map.pop(assigns, :rest, %{})
53+
assigns = Map.merge(assigns, rest)
54+
55+
cond do
56+
is_nil(as) ->
57+
dynamic_tag(Map.merge(assigns, default_tag_attrs))
58+
59+
is_function(as) ->
60+
as.(assigns)
61+
62+
is_binary(as) ->
63+
dynamic_tag(Map.merge(assigns, %{tag_name: as}))
64+
65+
true ->
66+
raise "Cannot render component `as` #{inspect(as)}. Expected a function or string"
67+
end
68+
end
69+
end

lib/prima/dropdown.ex

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule Prima.Dropdown do
22
use Phoenix.Component
3+
import Prima.Component, only: [render_as: 2]
34
alias Phoenix.LiveView.JS
45

56
attr :id, :string, default: ""
@@ -75,20 +76,7 @@ defmodule Prima.Dropdown do
7576
"aria-expanded": "false"
7677
})
7778

78-
{as, assigns} = Map.pop(assigns, :as)
79-
{rest, assigns} = Map.pop(assigns, :rest, %{})
80-
assigns = Map.merge(assigns, rest)
81-
82-
if as do
83-
as.(assigns)
84-
else
85-
dynamic_tag(
86-
Map.merge(assigns, %{
87-
tag_name: "button",
88-
type: "button"
89-
})
90-
)
91-
end
79+
render_as(assigns, %{tag_name: "button", type: "button"})
9280
end
9381

9482
attr :transition_enter, :any, default: nil
@@ -209,10 +197,6 @@ defmodule Prima.Dropdown do
209197
will not function correctly with keyboard navigation and screen readers.
210198
"""
211199
def dropdown_item(assigns) do
212-
{as, assigns} = Map.pop(assigns, :as)
213-
{rest, assigns} = Map.pop(assigns, :rest, %{})
214-
assigns = Map.merge(assigns, rest)
215-
216200
assigns =
217201
assign(assigns, %{
218202
role: "menuitem",
@@ -221,14 +205,6 @@ defmodule Prima.Dropdown do
221205
"data-disabled": if(assigns.disabled, do: "true", else: nil)
222206
})
223207

224-
if as do
225-
as.(assigns)
226-
else
227-
dynamic_tag(
228-
Map.merge(assigns, %{
229-
tag_name: "div"
230-
})
231-
)
232-
end
208+
render_as(assigns, %{tag_name: "div"})
233209
end
234210
end

lib/prima/modal.ex

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ defmodule Prima.Modal do
7070
by using Phoenix LiveView routing.
7171
"""
7272
use Phoenix.Component
73+
import Prima.Component, only: [render_as: 2]
7374
alias Phoenix.LiveView.JS
7475

7576
attr :id, :string, required: true
@@ -376,20 +377,6 @@ defmodule Prima.Modal do
376377
"data-prima-ref": "modal-title"
377378
})
378379

379-
case assigns[:as] do
380-
as when is_function(as) ->
381-
{_, assigns} = Map.pop(assigns, :as)
382-
as.(assigns)
383-
384-
as when is_binary(as) ->
385-
dynamic_tag(
386-
Map.merge(assigns, %{
387-
tag_name: as
388-
})
389-
)
390-
391-
as ->
392-
raise "Cannot render modal title `as` #{inspect(as)}. Expected a function or string"
393-
end
380+
render_as(assigns, %{tag_name: "h3"})
394381
end
395382
end

0 commit comments

Comments
 (0)