Skip to content

Commit

Permalink
Prep for new HEEx interpolation syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismccord committed Nov 19, 2024
1 parent 222d9ab commit 51252ac
Show file tree
Hide file tree
Showing 31 changed files with 261 additions and 227 deletions.
34 changes: 18 additions & 16 deletions guides/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ At the end of the Request life-cycle chapter, we created a template at `lib/hell

```heex
<section>
<h2>Hello World, from <%= @messenger %>!</h2>
<h2>Hello World, from {@messenger}!</h2>
</section>
```

Expand All @@ -34,7 +34,7 @@ That's simple enough. There's only two lines, `use HelloWeb, :html`. This line c

All of the imports and aliases we make in our module will also be available in our templates. That's because templates are effectively compiled into functions inside their respective module. For example, if you define a function in your module, you will be able to invoke it directly from the template. Let's see this in practice.

Imagine we want to refactor our `show.html.heex` to move the rendering of `<h2>Hello World, from <%= @messenger %>!</h2>` to its own function. We can move it to a function component inside `HelloHTML`, let's do so:
Imagine we want to refactor our `show.html.heex` to move the rendering of `<h2>Hello World, from {@messenger}!</h2>` to its own function. We can move it to a function component inside `HelloHTML`, let's do so:

```elixir
defmodule HelloWeb.HelloHTML do
Expand All @@ -46,7 +46,7 @@ defmodule HelloWeb.HelloHTML do

def greet(assigns) do
~H"""
<h2>Hello World, from <%= @messenger %>!</h2>
<h2>Hello World, from {@messenger}!</h2>
"""
end
end
Expand Down Expand Up @@ -89,19 +89,21 @@ Next, let's fully understand the expressive power behind the HEEx template langu

## HEEx

Function components and templates files are powered by [the HEEx template language](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2), which stands for "HTML+EEx". EEx is an Elixir library that uses `<%= expression %>` to execute Elixir expressions and interpolate their results into the template. This is frequently used to display assigns we have set by way of the `@` shortcut. In your controller, if you invoke:
Function components and templates files are powered by [the HEEx template language](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2), which stands for "HTML+EEx". EEx is an Elixir library that uses `<%= expression %>` to execute Elixir expressions and interpolate their results into arbitrary text templates. HEEx extends EEx for writing HTML templates mixed with Elixir interpolation. We can write Elixir code inside `{...}` for HTML-aware interpolation inside tag attributes and the body. We can also interpolate arbitrary HEEx blocks using EEx interpolation (`<%= ... %>`). We use `@name` to access the key `name` defined inside `assigns`.

This is frequently used to display assigns we have set by way of the `@` shortcut. In your controller, if you invoke:

```elixir
render(conn, :show, username: "joe")
```

Then you can access said username in the templates as `<%= @username %>`. In addition to displaying assigns and functions, we can use pretty much any Elixir expression. For example, in order to have conditionals:
Then you can access said username in the templates as `{@username}`. In addition to displaying assigns and functions, we can use pretty much any Elixir expression. For example, in order to have conditionals:

```heex
<%= if some_condition? do %>
<p>Some condition is true for user: <%= @username %></p>
<p>Some condition is true for user: {@username}</p>
<% else %>
<p>Some condition is false for user: <%= @username %></p>
<p>Some condition is false for user: {@username}</p>
<% end %>
```

Expand All @@ -115,8 +117,8 @@ or even loops:
</tr>
<%= for number <- 1..10 do %>
<tr>
<td><%= number %></td>
<td><%= number * number %></td>
<td>{number}</td>
<td>{number * number}</td>
</tr>
<% end %>
</table>
Expand All @@ -131,20 +133,20 @@ HEEx also comes with handy HTML extensions we will learn next.
Besides allowing interpolation of Elixir expressions via `<%= %>`, `.heex` templates come with HTML-aware extensions. For example, let's see what happens if you try to interpolate a value with "<" or ">" in it, which would lead to HTML injection:

```heex
<%= "<b>Bold?</b>" %>
{"<b>Bold?</b>"}
```

Once you render the template, you will see the literal `<b>` on the page. This means users cannot inject HTML content on the page. If you want to allow them to do so, you can call `raw`, but do so with extreme care:

```heex
<%= raw "<b>Bold?</b>" %>
{raw("<b>Bold?</b>")}
```

Another super power of HEEx templates is validation of HTML and lean interpolation syntax of attributes. You can write:
Another super power of HEEx templates is validation of HTML and interpolation syntax of attributes. You can write:

```heex
<div title="My div" class={@class}>
<p>Hello <%= @username %></p>
<p>Hello {@username}</p>
</div>
```

Expand All @@ -154,7 +156,7 @@ To interpolate a dynamic number of attributes in a keyword list or map, do:

```heex
<div title="My div" {@many_attributes}>
<p>Hello <%= @username %></p>
<p>Hello {@username}</p>
</div>
```

Expand All @@ -178,7 +180,7 @@ Likewise, for comprehensions may be written as:

```heex
<ul>
<li :for={item <- @items}><%= item.name %></li>
<li :for={item <- @items}>{item.name}</li>
</ul>
```

Expand All @@ -189,7 +191,7 @@ Layouts are just function components. They are defined in a module, just like al
You may be wondering how the string resulting from a rendered view ends up inside a layout. That's a great question! If we look at `lib/hello_web/components/layouts/root.html.heex`, just about at the end of the `<body>`, we will see this.

```heex
<%= @inner_content %>
{@inner_content}
```

In other words, after rendering your page, the result is placed in the `@inner_content` assign.
Expand Down
51 changes: 22 additions & 29 deletions guides/contexts.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,10 +525,9 @@ We added a `category_select` above our save button. Now let's try it out. Next,
<.list>
...
+ <:item title="Categories">
+ <%= for cat <- @product.categories do %>
+ <%= cat.title %>
+ <br/>
+ <% end %>
+ <ul>
+ <li :for={cat <- @product.categories}>{cat.title}</li>
+ </ul>
+ </:item>
</.list>
```
Expand Down Expand Up @@ -938,29 +937,23 @@ We created a view to render our `show.html` template and aliased our `ShoppingCa
Next we can create the template at `lib/hello_web/controllers/cart_html/show.html.heex`:

```heex
<%= if @cart.items == [] do %>
<.header>
My Cart
<:subtitle>Your cart is empty</:subtitle>
</.header>
<% else %>
<.header>
My Cart
</.header>
<.header>
My Cart
<:subtitle :if={@cart.items == []}>Your cart is empty</:subtitle>
</.header>
<div :if={@cart.items !== []}>
<.simple_form :let={f} for={@changeset} action={~p"/cart"}>
<.inputs_for :let={item_form} field={f[:items]}>
<% item = item_form.data %>
<.inputs_for :let={%{data: item} = item_form} field={f[:items]}>
<.input field={item_form[:quantity]} type="number" label={item.product.title} />
<%= currency_to_str(ShoppingCart.total_item_price(item)) %>
{currency_to_str(ShoppingCart.total_item_price(item))}
</.inputs_for>
<:actions>
<.button>Update cart</.button>
</:actions>
</.simple_form>
<b>Total</b>: <%= currency_to_str(ShoppingCart.total_cart_price(@cart)) %>
<% end %>
<b>Total</b>: {currency_to_str(ShoppingCart.total_cart_price(@cart))}
</div>
<.back navigate={~p"/products"}>Back to products</.back>
```
Expand Down Expand Up @@ -1300,20 +1293,20 @@ Next we can create the template at `lib/hello_web/controllers/order_html/show.ht
<.header>
Thank you for your order!
<:subtitle>
<strong>User uuid: </strong><%= @order.user_uuid %>
<strong>User uuid: </strong>{@order.user_uuid}
</:subtitle>
</.header>
<.table id="items" rows={@order.line_items}>
<:col :let={item} label="Title"><%= item.product.title %></:col>
<:col :let={item} label="Quantity"><%= item.quantity %></:col>
<:col :let={item} label="Title">{item.product.title}</:col>
<:col :let={item} label="Quantity">{item.quantity}</:col>
<:col :let={item} label="Price">
<%= HelloWeb.CartHTML.currency_to_str(item.price) %>
{HelloWeb.CartHTML.currency_to_str(item.price)}
</:col>
</.table>
<strong>Total price:</strong>
<%= HelloWeb.CartHTML.currency_to_str(@order.total_price) %>
{HelloWeb.CartHTML.currency_to_str(@order.total_price)}
<.back navigate={~p"/products"}>Back to products</.back>
```
Expand All @@ -1325,11 +1318,11 @@ Our last addition will be to add the "complete order" button to our cart page to
```diff
<.header>
My Cart
+ <:actions>
+ <.link href={~p"/orders"} method="post">
+ <.button>Complete order</.button>
+ </.link>
+ </:actions>
+ <:actions>
+ <.link href={~p"/orders"} method="post">
+ <.button>Complete order</.button>
+ </.link>
+ </:actions>
</.header>
```

Expand Down
2 changes: 1 addition & 1 deletion guides/plug.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ In the [`init/1`] callback, we pass a default locale to use if none is present i
To see the assign in action, go to the template in `lib/hello_web/controllers/page_html/home.html.heex` and add the following code after the closing of the `</h1>` tag:

```heex
<p>Locale: <%= @locale %></p>
<p>Locale: {@locale}</p>
```

Go to [http://localhost:4000/](http://localhost:4000/) and you should see the locale exhibited. Visit [http://localhost:4000/?locale=fr](http://localhost:4000/?locale=fr) and you should see the assign changed to `"fr"`. Someone can use this information alongside [Gettext](https://hexdocs.pm/gettext/Gettext.html) to provide a fully internationalized web application.
Expand Down
2 changes: 1 addition & 1 deletion guides/real_time/presence.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ defmodule HelloWeb.OnlineLive do
def render(assigns) do
~H"""
<ul id="online_users" phx-update="stream">
<li :for={{dom_id, %{id: id, metas: metas}} <- @streams.presences} id={dom_id}><%= id %> (<%= length(metas) %>)</li>
<li :for={{dom_id, %{id: id, metas: metas}} <- @streams.presences} id={dom_id}>{id} ({length(metas)})</li>
</ul>
"""
end
Expand Down
12 changes: 7 additions & 5 deletions guides/request_lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Now that we've got the route, controller, view, and template, we should be able
There are a couple of interesting things to notice about what we just did. We didn't need to stop and restart the server while we made these changes. Yes, Phoenix has hot code reloading! Also, even though our `index.html.heex` file consists of only a single `section` tag, the page we get is a full HTML document. Our index template is actually rendered into layouts: first it renders `lib/hello_web/components/layouts/root.html.heex` which renders `lib/hello_web/components/layouts/app.html.heex` which finally includes our content. If you open those files, you'll see a line that looks like this at the bottom:

```heex
<%= @inner_content %>
{@inner_content}
```

This line injects our template into the layout before the HTML is sent off to the browser. We will talk more about layouts in the Controllers guide.
Expand Down Expand Up @@ -273,15 +273,17 @@ It's good to remember that the keys of the `params` map will always be strings,

For the last piece of this puzzle, we'll need a new template. Since it is for the `show` action of `HelloController`, it will go into the `lib/hello_web/controllers/hello_html` directory and be called `show.html.heex`. It will look surprisingly like our `index.html.heex` template, except that we will need to display the name of our messenger.

To do that, we'll use the special HEEx tags for executing Elixir expressions: `<%= %>`. Notice that the initial tag has an equals sign like this: `<%=` . That means that any Elixir code that goes between those tags will be executed, and the resulting value will replace the tag in the HTML output. If the equals sign were missing, the code would still be executed, but the value would not appear on the page.
To do that, we'll use the special HEEx tags for executing Elixir expressions: `{...}` and `<%= %>`. Notice that EEx tag has an equals sign like this: `<%=` . That means that any Elixir code that goes between those tags will be executed, and the resulting value will replace the tag in the HTML output. If the equals sign were missing, the code would still be executed, but the value would not appear on the page.

Remember our templates are written in HEEx (HTML+EEx). HEEx is a superset of EEx which is why it shares the `<%= %>` syntax.
Remember our templates are written in HEEx (HTML+EEx). HEEx is a superset of EEx, and thereby supports the EEx `<%= %>` interpolation syntax for interpolating arbitrary blocks of code. In general, the HEEx `{...}` interpolation syntax is preferred anytime there is HTML-aware intepolation to be done – such as within attributes or inline values with a body.

And this is what the template should look like:
The only times `EEx` `<%= %>` interpolation is necessary is for interpolationg arbitrary blocks of markup, such as branching logic that inects separate markup trees, or for interpolating values within `<script>` or `<style>` tags.

This is what the `hello_html/show.html.heex` template should look like:

```heex
<section>
<h2>Hello World, from <%= @messenger %>!</h2>
<h2>Hello World, from {@messenger}!</h2>
</section>
```

Expand Down
4 changes: 2 additions & 2 deletions installer/lib/phx_new/generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -465,13 +465,13 @@ defmodule Phx.New.Generator do
# ## Examples
#
# iex> ~s|<tag>#{maybe_eex_gettext("Hello", true)}</tag>|
# ~S|<tag><%= gettext("Hello") %></tag>|
# ~S|<tag>{gettext("Hello")}</tag>|
#
# iex> ~s|<tag>#{maybe_eex_gettext("Hello", false)}</tag>|
# ~S|<tag>Hello</tag>|
def maybe_eex_gettext(message, gettext?) do
if gettext? do
~s|<%= gettext(#{inspect(message)}) %>|
~s|{gettext(#{inspect(message)})}|
else
message
end
Expand Down
3 changes: 2 additions & 1 deletion installer/templates/phx_single/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ defmodule <%= @app_module %>.MixProject do
{:phoenix_html, "~> 4.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
# TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
{:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
{:phoenix_live_view,
github: "phoenixframework/phoenix_live_view", ref: "jv-interpol", override: true},
{:floki, ">= 0.30.0", only: :test},<% end %><%= if @dashboard do %>
{:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @javascript do %>
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},<% end %><%= if @css do %>
Expand Down
3 changes: 2 additions & 1 deletion installer/templates/phx_umbrella/apps/app_name_web/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ defmodule <%= @web_namespace %>.MixProject do
{:phoenix_html, "~> 4.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
# TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
{:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
{:phoenix_live_view,
github: "phoenixframework/phoenix_live_view", ref: "jv-interpol", override: true},
{:floki, ">= 0.30.0", only: :test},<% end %><%= if @dashboard do %>
{:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @javascript do %>
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},<% end %><%= if @css do %>
Expand Down
3 changes: 2 additions & 1 deletion installer/templates/phx_umbrella/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ defmodule <%= @root_app_module %>.MixProject do
<%= if @dev or @phoenix_version.pre != [] do %><%= @phoenix_dep_umbrella_root %>,
<% end %># Required to run "mix format" on ~H/.heex files from the umbrella root
# TODO bump on release to {:phoenix_live_view, ">= 0.0.0"},
{:phoenix_live_view, "~> 1.0.0-rc.1", override: true}
{:phoenix_live_view,
github: "phoenixframework/phoenix_live_view", ref: "jv-interpol", override: true}
]<% else %>
[]<% end %>
end
Expand Down
Loading

0 comments on commit 51252ac

Please sign in to comment.