-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
view.ex
361 lines (267 loc) · 11.1 KB
/
view.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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
defmodule Phoenix.View do
@moduledoc """
Defines the view layer of a Phoenix application.
This module is used to define the application main view, which
serves as the base for all other views and templates in the
application.
The view layer also contains conveniences for rendering templates,
including support for layouts and encoders per format.
## Examples
Phoenix defines the view template at `web/web.ex`:
defmodule YourApp.Web do
def view do
quote do
use Phoenix.View, root: "web/templates"
# Import common functionality
import YourApp.Router.Helpers
# Use Phoenix.HTML to import all HTML functions (forms, tags, etc)
use Phoenix.HTML
end
end
# ...
end
We can use the definition above to define any view in your application:
defmodule YourApp.UserView do
use YourApp.Web, :view
end
Because we have defined the template root to be "web/template", `Phoenix.View`
will automatically load all templates at "web/template/user" and include them
in the `YourApp.UserView`. For example, imagine we have the template:
# web/templates/user/index.html.eex
Hello <%= @name %>
The `.eex` extension is called a template engine which tells Phoenix how
to compile the code in the file into actual Elixir source code. After it is
compiled, the template can be rendered as:
Phoenix.View.render(YourApp.UserView, "index.html", name: "John Doe")
#=> {:safe, "Hello John Doe"}
We will discuss rendering in detail next.
## Rendering
The main responsibility of a view is to render a template.
A template has a name, which also contains a format. For example,
in the previous section we have rendered the "index.html" template:
Phoenix.View.render(YourApp.UserView, "index.html", name: "John Doe")
#=> {:safe, "Hello John Doe"}
When a view renders a template, the result returned is an inner
representation specific to the template format. In the example above,
we got: `{:safe, "Hello John Doe"}`. The safe tuple annotates that our
template is safe and that we don't need to escape its contents because
all data was already encoded so far. Let's try to inject custom code:
Phoenix.View.render(YourApp.UserView, "index.html", name: "John<br />Doe")
#=> {:safe, "Hello John<br />Doe"}
This inner representation allows us to render and compose templates easily.
For example, if you want to render JSON data, we could do so by adding a
"show.json" entry to `render/2` in our view:
defmodule YourApp.UserView do
use YourApp.View
def render("show.json", %{user: user}) do
%{name: user.name, address: user.address}
end
end
Notice that in order to render JSON data, we don't need to explicitly
return a JSON string! Instead, we just return data that is encodable to
JSON.
Both JSON and HTML formats will be encoded only when passing the data
to the controller via the `render_to_iodata/3` function. The
`render_to_iodata/3` uses the notion of format encoders to convert a
particular format to its string/iodata representation.
Phoenix ships with some template engines and format encoders, which
can be further configured in the Phoenix application. You can read
more about format encoders in `Phoenix.Template` documentation.
"""
@doc """
When used, defines the current module as a main view module.
## Options
* `:root` - the template root to find templates
* `:namespace` - the namespace to consider when calculating view paths
The `:root` option is required while the `:namespace` defaults to the
first nesting in the module name. For instance, both `MyApp.UserView`
and `MyApp.Admin.UserView` have namespace `MyApp`.
The namespace is used to calculate paths. For example, if you are in
`MyApp.UserView` and the namespace is `MyApp`, templates are expected
at `Path.join(root, "user")`. On the other hand, if the view is
`MyApp.Admin.UserView`, the path will be `Path.join(root, "admin/user")`
and so on.
Setting the namespace to `MyApp.Admin` in the second example will force
the template to also be looked up at `Path.join(root, "user")`.
"""
defmacro __using__(options) do
if root = Keyword.get(options, :root) do
namespace =
if given = Keyword.get(options, :namespace) do
given
else
__CALLER__.module
|> Module.split()
|> Enum.take(1)
|> Module.concat()
end
quote do
import Phoenix.View
use Phoenix.Template, root:
Path.join(unquote(root),
Phoenix.Template.module_to_template_root(__MODULE__, unquote(namespace), "View"))
@view_resource String.to_atom(Phoenix.Naming.resource_name(__MODULE__, "View"))
@doc "The resource name, as an atom, for this view"
def __resource__, do: @view_resource
end
else
raise "expected :root to be given as an option"
end
end
@doc """
Renders a template.
It expects the view module, the template as a string, and a
set of assigns.
Notice this function returns the inner representation of a
template. If you want the encoded template as a result, use
`render_to_iodata/3` instead.
## Examples
Phoenix.View.render(YourApp.UserView, "index.html", name: "John Doe")
#=> {:safe, "Hello John Doe"}
## Assigns
Assigns are meant to be user data that will be available in templates.
However there are keys under assigns that are specially handled by
Phoenix, they are:
* `:layout` - tells Phoenix to wrap the rendered result in the
given layout. See next section.
The following assigns are reserved, and cannot be set directly:
* `@view_module` - The view module being rendered
* `@view_template` - The `@view_module`'s template being rendered
## Layouts
Templates can be rendered within other templates using the `:layout`
option. `:layout` accepts a tuple of the form
`{LayoutModule, "template.extension"}`.
To render the template within the layout, simply call `render/3`
using the `@view_module` and `@view_template` assign:
<%= render @view_module, @view_template, assigns %>
"""
def render(module, template, assigns) do
assigns
|> to_map()
|> Map.pop(:layout, false)
|> render_within(module, template)
end
defp render_within({{layout_mod, layout_tpl}, assigns}, inner_mod, inner_tpl) do
assigns = Map.merge(assigns, %{view_module: inner_mod,
view_template: inner_tpl})
render_layout(layout_mod, layout_tpl, assigns)
end
defp render_within({false, assigns}, module, template) do
assigns = Map.merge(assigns, %{view_module: module,
view_template: template})
module.render(template, assigns)
end
defp render_layout(layout_mod, layout_tpl, assigns) do
layout_mod.render(layout_tpl, assigns)
end
@doc """
Renders a template only if it exists.
Same as `render/3`, but returns `nil` instead of raising.
Useful for dynamically rendering templates in the layout that may or
may not be implemented by the `@view_module` view.
## Examples
Consider the case where the application layout allows views to dynamically
render a section of script tags in the head of the document. Some views
may wish to inject certain scripts, while others will not.
<head>
<%= render_existing @view_module, "scripts.html", assigns %>
</head>
Then the module for the `@view_module` view can decide to provide scripts with
either a precompiled template, or by implementing the function directly, ie:
def render("scripts.html", _assigns) do
~E(<script src="file.js"></script>)
end
To use a precompiled template, create a `scripts.html.eex` file in the `templates`
directory for the corresponding view you want it to render for. For example,
for the `UserView`, create the `scripts.html.eex` file at `web/templates/user/`.
## Rendering based on controller template
In some cases, you might need to render based on the template.
For these cases, `@view_template` can pair with
`render_existing/3` for per-template based content, ie:
<head>
<%= render_existing @view_module, "scripts." <> @view_template, assigns %>
</head>
def render("scripts.show.html", _assigns) do
~E(<script src="file.js"></script>)
end
def render("scripts.index.html", _assigns) do
~E(<script src="file.js"></script>)
end
"""
def render_existing(module, template, assigns \\ []) do
render(module, template, Dict.put(assigns, :render_existing, {module, template}))
end
@doc """
Renders a collection.
A collection is any enumerable of structs. This function
returns the rendered collection in a list:
render_many users, UserView, "show.html"
is roughly equivalent to:
Enum.map(users, fn user ->
render(UserView, "show.html", user: user)
end)
The underlying user is passed to the view and template as `:user`,
which is inferred from the view name. The name of the key
in assigns can be customized with the `:as` option:
render_many users, UserView, "show.html", as: :data
is roughly equivalent to:
Enum.map(users, fn user ->
render(UserView, "show.html", data: user)
end)
"""
def render_many(collection, view, template, assigns \\ %{}) do
assigns = to_map(assigns)
Enum.map(collection, fn model ->
render view, template, assign_model(assigns, view, model)
end)
end
@doc """
Renders a single item if not nil.
The following:
render_one user, UserView, "show.html"
is roughly equivalent to:
if user != nil do
render(UserView, "show.html", user: user)
end
The underlying user is passed to the view and template as
`:user`, which is inflected from the view name. The name
of the key in assigns can be customized with the `:as` option:
render_one user, UserView, "show.html", as: :data
is roughly equivalent to:
if user != nil do
render(UserView, "show.html", data: user)
end
"""
def render_one(model, view, template, assigns \\ %{}) do
if model != nil do
assigns = to_map(assigns)
render view, template, assign_model(assigns, view, model)
end
end
defp to_map(assigns) when is_map(assigns), do: assigns
defp to_map(assigns) when is_list(assigns), do: :maps.from_list(assigns)
defp to_map(assigns), do: Dict.merge(%{}, assigns)
defp assign_model(assigns, view, model) do
as = Map.get(assigns, :as) || view.__resource__
Map.put(assigns, as, model)
end
@doc """
Renders the template and returns iodata.
"""
def render_to_iodata(module, template, assign) do
render(module, template, assign) |> encode(template)
end
@doc """
Renders the template and returns a string.
"""
def render_to_string(module, template, assign) do
render_to_iodata(module, template, assign) |> IO.iodata_to_binary
end
defp encode(content, template) do
if encoder = Phoenix.Template.format_encoder(template) do
encoder.encode_to_iodata!(content)
else
content
end
end
end