Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support {...} interpolation in body #3514

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 81 additions & 80 deletions lib/phoenix_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ defmodule Phoenix.Component do

~H"""
<div title="My div" class={@class}>
<p>Hello <%= @name %></p>
<p>Hello {@name}</p>
<MyApp.Weather.city name="Kraków"/>
</div>
"""
Expand All @@ -589,43 +589,14 @@ defmodule Phoenix.Component do

### Interpolation

Both `HEEx` and `EEx` templates use `<%= ... %>` for interpolating code inside the body
of HTML tags:
`HEEx` allows using `{...}` for HTML-aware interpolation, inside tag attributes
as well as the body:

```heex
<p>Hello, <%= @name %></p>
<p>Hello, {@name}</p>
```

Similarly, conditionals and other block Elixir constructs are supported:

```heex
<%= if @show_greeting? do %>
<p>Hello, <%= @name %></p>
<% end %>
```

Note we don't include the equal sign `=` in the closing `<% end %>` tag
(because the closing tag does not output anything).

There is one important difference between `HEEx` and Elixir's builtin `EEx`.
`HEEx` uses a specific annotation for interpolating HTML tags and attributes.
Let's check it out.

### HEEx extension: Defining attributes

Since `HEEx` must parse and validate the HTML structure, code interpolation using
`<%= ... %>` and `<% ... %>` are restricted to the body (inner content) of the
HTML/component nodes and it cannot be applied within tags.

For instance, the following syntax is invalid:

```heex
<div class="<%= @class %>">
...
</div>
```

Instead do:
If you want to interpolate an attribute, you write:

```heex
<div class={@class}>
Expand Down Expand Up @@ -658,60 +629,48 @@ defmodule Phoenix.Component do
as a different class. `nil` and `false` elements are discarded.

For multiple dynamic attributes, you can use the same notation but without
assigning the expression to any specific attribute.
assigning the expression to any specific attribute:

```heex
<div {@dynamic_attrs}>
...
</div>
```

The expression inside `{...}` must be either a keyword list or a map containing
the key-value pairs representing the dynamic attributes.
In this case, the expression inside `{...}` must be either a keyword list or
a map containing the key-value pairs representing the dynamic attributes.

### HEEx extension: Defining function components
### Interpolating blocks

Function components are stateless components implemented as pure functions
with the help of the `Phoenix.Component` module. They can be either local
(same module) or remote (external module).
The curly brackets syntax is the default mechanism for interpolating code.
However, it cannot be used in all scenarios, in particular:

`HEEx` allows invoking these function components directly in the template
using an HTML-like notation. For example, a remote function:
* it cannot be used inside `<script>` and `<style>` tags,
as that would make writing JS and CSS quite tedious

```heex
<MyApp.Weather.city name="Kraków"/>
```
* it does not support block constructs

A local function can be invoked with a leading dot:
For example, if you need to interpolate a string inside a script tag,
you could do:

```heex
<.city name="Kraków"/>
<script>
window.URL = "<%= @my_url %>"
</script>
```

where the component could be defined as follows:

defmodule MyApp.Weather do
use Phoenix.Component

def city(assigns) do
~H"""
The chosen city is: <%= @name %>.
"""
end
Similarly, for block constructs in Elixir, you can write:

def country(assigns) do
~H"""
The chosen country is: <%= @name %>.
"""
end
end
```heex
<%= if @show_greeting? do %>
<p>Hello, <%= @name %></p>
<% end %>
```

It is typically best to group related functions into a single module, as
opposed to having many modules with a single `render/1` function. Function
components support other important features, such as slots. You can learn
more about components in `Phoenix.Component`.
However, for conditionals and for-comprehensions, there are built-in constructs
in HEEx too, which we will explore next.

### HEEx extension: special attributes
### Special attributes

Apart from normal HTML attributes, HEEx also supports some special attributes
such as `:let` and `:for`.
Expand Down Expand Up @@ -781,7 +740,49 @@ defmodule Phoenix.Component do
```

Note that unlike Elixir's regular `for`, HEEx' `:for` does not support multiple
generators in one expression.
generators in one expression. In such cases, you must use `EEx`'s blocks.

### Function components

Function components are stateless components implemented as pure functions
with the help of the `Phoenix.Component` module. They can be either local
(same module) or remote (external module).

`HEEx` allows invoking these function components directly in the template
using an HTML-like notation. For example, a remote function:

```heex
<MyApp.Weather.city name="Kraków"/>
```

A local function can be invoked with a leading dot:

```heex
<.city name="Kraków"/>
```

where the component could be defined as follows:

defmodule MyApp.Weather do
use Phoenix.Component

def city(assigns) do
~H"""
The chosen city is: {@name}.
"""
end

def country(assigns) do
~H"""
The chosen country is: {@name}.
"""
end
end

It is typically best to group related functions into a single module, as
opposed to having many modules with a single `render/1` function. Function
components support other important features, such as slots. You can learn
more about components in `Phoenix.Component`.

## Code formatting

Expand Down Expand Up @@ -2407,7 +2408,7 @@ defmodule Phoenix.Component do
<%= if @csrf_token do %>
<input name="_csrf_token" type="hidden" hidden value={@csrf_token} />
<% end %>
<%= render_slot(@inner_block, @form) %>
{render_slot(@inner_block, @form)}
</form>
"""
end
Expand Down Expand Up @@ -2715,7 +2716,7 @@ defmodule Phoenix.Component do
<input type="hidden" name={name} value={value} />
<% end %>
<% end %>
<%= render_slot(@inner_block, finner) %>
{render_slot(@inner_block, finner)}
<% end %>
"""
end
Expand Down Expand Up @@ -2970,7 +2971,7 @@ defmodule Phoenix.Component do

def link(%{} = assigns) do
~H"""
<a href="#" {@rest}><%= render_slot(@inner_block) %></a>
<a href="#" {@rest}>{render_slot(@inner_block)}</a>
"""
end

Expand Down Expand Up @@ -3011,7 +3012,7 @@ defmodule Phoenix.Component do
~H"""
<div id={@id} phx-hook="Phoenix.FocusWrap" {@rest}>
<span id={"#{@id}-start"} tabindex="0" aria-hidden="true"></span>
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
<span id={"#{@id}-end"} tabindex="0" aria-hidden="true"></span>
</div>
"""
Expand Down Expand Up @@ -3101,12 +3102,12 @@ defmodule Phoenix.Component do

if assigns.inner_block != [] do
~H"""
<%= {:safe, [?<, @tag]} %><%= @escaped_attrs %><%= {:safe, [?>]} %><%= render_slot(@inner_block) %><%= {:safe,
[?<, ?/, @tag, ?>]} %>
{{:safe, [?<, @tag]}}{@escaped_attrs}{{:safe, [?>]}}{render_slot(@inner_block)}{{:safe,
[?<, ?/, @tag, ?>]}}
"""
else
~H"""
<%= {:safe, [?<, @tag]} %><%= @escaped_attrs %><%= {:safe, [?/, ?>]} %>
{{:safe, [?<, @tag]}}{@escaped_attrs}{{:safe, [?/, ?>]}}
"""
end
end
Expand Down Expand Up @@ -3331,13 +3332,13 @@ defmodule Phoenix.Component do
def async_result(%{assign: async_assign} = assigns) do
cond do
async_assign.ok? ->
~H|<%= render_slot(@inner_block, @assign.result) %>|
~H|{render_slot(@inner_block, @assign.result)}|

async_assign.loading ->
~H|<%= render_slot(@loading, @assign.loading) %>|
~H|{render_slot(@loading, @assign.loading)}|

async_assign.failed ->
~H|<%= render_slot(@failed, @assign.failed) %>|
~H|{render_slot(@failed, @assign.failed)}|
end
end
end
4 changes: 2 additions & 2 deletions lib/phoenix_live_view/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ defmodule Phoenix.LiveView.Helpers do

assigns = %{opts: opts, content: block_or_text}

~H|<a {@opts}><%= @content %></a>|
~H|<a {@opts}>{@content}</a>|
end

@deprecated "Use <.live_title> instead"
Expand All @@ -99,7 +99,7 @@ defmodule Phoenix.LiveView.Helpers do

~H"""
<Phoenix.Component.live_title prefix={@prefix} suffix={@suffix}>
<%= @title %>
{@title}
</Phoenix.Component.live_title>
"""
end
Expand Down
45 changes: 35 additions & 10 deletions lib/phoenix_live_view/html_algebra.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
# * :preserve - for preserving text in <pre>, <script>, <style> and HTML Comment tags
#
def build(tree, opts) when is_list(tree) do
{migrate, opts} = Keyword.pop(opts, :migrate_eex_to_curly_brackets, true)

tree
|> block_to_algebra(%{mode: :normal, opts: opts})
|> block_to_algebra(%{mode: :normal, migrate: migrate, opts: opts})
|> group()
end

Expand Down Expand Up @@ -179,6 +181,7 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
defp text_ends_with_line_break?(_node), do: false

defp block_preserve?({:tag_block, _, _, _, %{mode: :preserve}}), do: true
defp block_preserve?({:body_expr, _, _}), do: true
defp block_preserve?({:eex, _, _}), do: true
defp block_preserve?(_node), do: false

Expand Down Expand Up @@ -314,13 +317,27 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
{:inline, concat(["<%!--", text, "--%>"])}
end

defp to_algebra({:eex, text, %{opt: opt}}, %{mode: :preserve}) do
{:inline, concat(["<%#{opt} ", text, " %>"])}
defp to_algebra({:eex, text, %{opt: opt} = meta}, context) do
cond do
context.mode == :preserve ->
{:inline, concat(["<%#{opt} ", text, " %>"])}

context.migrate and opt == ~c"=" and safe_to_migrate?(text, 0) ->
to_algebra({:body_expr, text, meta}, context)

true ->
doc = expr_to_code_algebra(text, meta, context.opts)
{:inline, concat(["<%#{opt} ", doc, " %>"])}
end
end

defp to_algebra({:eex, text, %{opt: opt} = meta}, context) do
doc = expr_to_code_algebra(text, meta, context.opts)
{:inline, concat(["<%#{opt} ", doc, " %>"])}
defp to_algebra({:body_expr, text, meta}, context) do
if context.mode == :preserve do
{:inline, concat(["{", text, "}"])}
else
doc = expr_to_code_algebra(text, meta, context.opts)
{:inline, concat(["{", doc, "}"])}
end
end

# Handle text within <pre>/<script>/<style>/comment tags.
Expand Down Expand Up @@ -445,7 +462,7 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
defp render_attribute({attr, {:string, value, _meta}, _}, _opts), do: ~s(#{attr}="#{value}")

defp render_attribute({attr, {:expr, value, meta}, _}, opts) do
case expr_to_quoted(value, meta) do
case expr_to_quoted(value, meta, opts) do
{{:__block__, meta, [string]} = block, []} when is_binary(string) ->
has_quotes? = String.contains?(string, "\"")
delimiter = Keyword.get(meta, :delimiter)
Expand Down Expand Up @@ -506,20 +523,21 @@ defmodule Phoenix.LiveView.HTMLAlgebra do
{concat(document, next), stab?}
end

defp expr_to_quoted(expr, meta) do
defp expr_to_quoted(expr, meta, opts) do
string_to_quoted_opts = [
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
token_metadata: true,
unescape: false,
line: meta.line,
column: meta.column
column: meta.column,
file: Keyword.get(opts, :file, "nofile")
]

Code.string_to_quoted_with_comments!(expr, string_to_quoted_opts)
end

defp expr_to_code_algebra(expr, meta, opts) do
{quoted, comments} = expr_to_quoted(expr, meta)
{quoted, comments} = expr_to_quoted(expr, meta, opts)
quoted_to_code_algebra(quoted, comments, opts)
end

Expand Down Expand Up @@ -571,4 +589,11 @@ defmodule Phoenix.LiveView.HTMLAlgebra do

defp inline?({:tag_block, _, _, _, %{mode: :inline}}), do: true
defp inline?(_), do: false

defp safe_to_migrate?(~S[\{] <> rest, acc), do: safe_to_migrate?(rest, acc)
defp safe_to_migrate?(~S[\}] <> rest, acc), do: safe_to_migrate?(rest, acc)
defp safe_to_migrate?("{" <> rest, acc), do: safe_to_migrate?(rest, acc + 1)
defp safe_to_migrate?("}" <> rest, acc), do: safe_to_migrate?(rest, acc - 1)
defp safe_to_migrate?(<<_::utf8, rest::binary>>, acc), do: safe_to_migrate?(rest, acc)
defp safe_to_migrate?(<<>>, acc), do: acc == 0
end
Loading
Loading