/
phoenix_component.ex
295 lines (228 loc) · 7.84 KB
/
phoenix_component.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
defmodule Phoenix.Component do
@moduledoc ~S'''
API for function components.
A function component is any function that receives
an assigns map as argument and returns a rendered
struct built with [the `~H` sigil](`Phoenix.LiveView.Helpers.sigil_H/2`).
Here is an example:
defmodule MyComponent do
use Phoenix.Component
# Optionally also bring the HTML helpers
# use Phoenix.HTML
def greet(assigns) do
~H"""
<p>Hello, <%= assigns.name %></p>
"""
end
end
The component can be invoked as a regular function:
MyComponent.greet(%{name: "Jane"})
But it is typically invoked using the function component
syntax from the `~H` sigil:
~H"""
<MyComponent.greet name="Jane" />
"""
If the `MyComponent` module is imported or if the function
is defined locally, you can skip the module name:
~H"""
<.greet name="Jane" />
"""
Similar to any HTML tag inside the `~H` sigil, you can
interpolate attributes values too:
~H"""
<.greet name={@user.name} />
"""
You can learn more about the `~H` sigil [in its documentation](`Phoenix.LiveView.Helpers.sigil_H/2`).
## `use Phoenix.Component`
Modules that define function components should call
`use Phoenix.Component` at the top. Doing so will import
the functions from both `Phoenix.LiveView` and
`Phoenix.LiveView.Helpers` modules. `Phoenix.LiveView`
and `Phoenix.LiveComponent` automatically invoke
`use Phoenix.Component` for you.
You must avoid defining a module for each component. Instead,
we should use modules to group side-by-side related function
components.
## Assigns
While inside a function component, you must use `Phoenix.LiveView.assign/3`
and `Phoenix.LiveView.assign_new/3` to manipulate assigns,
so that LiveView can track changes to the assigns values.
For example, let's imagine a component that receives the first
name and last name and must compute the name assign. One option
would be:
def show_name(assigns) do
assigns = assign(assigns, :name, assigns.first_name <> assigns.last_name)
~H"""
<p>Your name is: <%= @name %></p>
"""
end
However, when possible, it may be cleaner to break the logic over function
calls instead of precomputed assigns:
def show_name(assigns) do
~H"""
<p>Your name is: <%= full_name(@first_name, @last_name) %></p>
"""
end
defp full_name(first_name, last_name), do: first_name <> last_name
Another example is making an assign optional by providing
a default value:
def field_label(assigns) do
assigns = assign_new(assigns, :help, fn -> nil end)
~H"""
<label>
<%= @text %>
<%= if @help do %>
<span class="help"><%= @help %></span>
<% end %>
</label>
"""
end
## Slots
Slots is a mechanism to give HTML blocks to function components
as in regular HTML tags.
### Default slots
Any content you pass inside a component is assigned to a default slot
called `@inner_block`. For example, imagine you want to create a button
component like this:
<.button>
This renders <strong>inside</strong> the button!
</.button>
It is quite simple to do so. Simply define your component and call
`render_slot(@inner_block)` where you want to inject the content:
def button(assigns) do
~H"""
<button class="btn">
<%= render_slot(@inner_block) %>
</button>
"""
end
In a nutshell, the contents given to the component is assigned to
the `@inner_block` assign and then we use `Phoenix.LiveView.Helpers.render_slot/2`
to render it.
You can even have the component give a value back to the caller,
by using `let`. Imagine this component:
def unordered_list(assigns) do
~H"""
<ul>
<%= for entry <- @entries do %>
<li><%= render_slot(@inner_block, entry) %></li>
<% end %>
</ul>
"""
end
And now you can invoke it as:
<.unordered_list let={entry} entries={~w(apple banana cherry)}>
I like <%= entry %>
</.unordered_list>
You can also pattern match the arguments provided to the render block. Let's
make our `unordered_list` component fancier:
def unordered_list(assigns) do
~H"""
<ul>
<%= for entry <- @entries do %>
<li><%= render_slot(@inner_block, %{entry: entry, gif_url: random_gif()} %></li>
<% end %>
</ul>
"""
end
And now we can invoke it like this:
<.unordered_list let={%{entry: entry, gif_url: url}}>
I like <%= entry %>. <img src={url} />
</.unordered_list>
### Named slots
Besides `@inner_block`, it is also possible to pass named slots
to the component. For example, imagine that you want to create
a modal component. The modal component has a header, a footer,
and the body of the modal, which we would use like this:
<.modal>
<:header>
This is the top of the modal.
</:header>
This is the body - everything not in a
named slot goes to @inner_block.
<:footer>
<button>Save</button>
</:footer>
</.modal>
The component itself could be implemented like this:
def modal(assigns) do
~H"""
<div class="modal">
<div class="modal-header">
<%= render_slot(@header) %>
</div>
<div class="modal-body">
<%= render_slot(@inner_block) %>
</div>
<div class="modal-footer">
<%= render_slot(@footer) %>
</div>
</div>
"""
end
If you want to make the `@header` and `@footer` optional,
you can assign them a default of an empty list at the top:
def modal(assigns) do
assigns =
assigns
|> assign_new(:header, fn -> [] end)
|> assign_new(:footer, fn -> [] end)
~H"""
<div class="modal">
...
end
### Named slots with attributes
It is also possible to pass the same named slot multiple
times and also give attributes to each of them.
If multiple slot entries are defined for the same slot,
`render_slot/2` will automatically render all entries,
merging their contents. But sometimes we want more fine
grained control over each individual slot, including access
to their attributes. Let's see an example. Imagine we want
to implement a table component
For example, imagine a table component:
<.table rows={@users}>
<:col let={user} label="Name">
<%= user.name %>
</:col>
<:col let={user} label="Address">
<%= user.address %>
</:col>
</.table>
At the top level, we pass the rows as an assign and we define
a `:col` slot for each column we want in the table. Each
column also has a `label`, which we are going to use in the
table header.
Inside the component, you can render the table with headers,
rows, and columns:
def table(assigns) do
~H"""
<table>
<tr>
<%= for col <- @col do %>
<th><%= col.label %></th>
<% end %>
</tr>
<%= for row <- @rows do %>
<tr>
<%= for col <- @col do %>
<td><%= render_slot(col, row) %></td>
<% end %>
</tr>
<% end %>
</table>
"""
end
Each named slot (including the `@inner_block`) is a list of maps,
where the map contains all slot attributes, allowing us to access
the label as `col.label`. This gives us complete control over how
we render them.
'''
@doc false
defmacro __using__(_) do
quote do
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
end
end
end