Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
91 changes: 91 additions & 0 deletions lib/phoenix_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,97 @@ defmodule Phoenix.Component do

Changing this configuration will require `mix clean` and a full recompile.

## Root tag annotations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a more advanced topic, I'm not sure if we should document it in Phoenix.Component. Probably better in the macro component docs. Those are private right now, but I think that's fine. The main way users would interact with this would be through colocated CSS for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point :) I was 50/50 on it when I was writing the docs as well. I ended up throwing it in in case they could be useful for some people as CSS or JS selectors, but I think it makes total sense since it is niche functionality to not immediately throw it at people in the Phoenix.Component docs!


HEEx templates support adding root tag annotations to the rendered page.
These can be useful for debugging or as selectors for things such as CSS.

Note that root tag annotations are applied to all root tags in the given
template, not just the outermost root tags. This means that tags at the root
of the template itself, at the root of any component inner blocks, or at
the root of any component slots will all be annotated.

For example, imagine the following component definition:

```elixir
defmodule MyAppWeb.MyModule do
slot :inner_block, required: true
slot :named_slot, required: true

def my_function(assigns) do
~H"""
<section>
<div>
{render_slot(@inner_block)}
</div>
</section>
<aside>
<div>
{render_slot(@named_slot)}
</div>
</aside>
"""
end
end
```

And the following HEEx template:

```heex
<div>
<div>
<.my_function>
<p>
<span>
Inner Block
</span>
</p>
<:named_slot>
<p>
<span>
Named Slot
</span>
</p>
</:named_slot>
</.my_function>
</div>
</div>
```

By setting `root_tag_annotation` to "phx-r", the rendered HTML would look as follows:

```html
<div phx-r>
<div>
<section phx-r>
<div>
<p phx-r>
<span>
Inner Block
</span>
</p>
</div>
</section>
<aside phx-r>
<div>
<p phx-r>
<span>
Named Slot
</span>
</p>
</div>
</aside>
</div>
</div>
```

This feature works on any `~H` or `.html.heex` template. They can be enabled
globally with the following configuration in your `config/config.exs` file:

config :phoenix_live_view, root_tag_annotation: "phx-r"

Changing this configuration will require `mix clean` and a full recompile.

## Dynamic Component Rendering

Sometimes you might need to decide at runtime which component to render.
Expand Down
30 changes: 29 additions & 1 deletion lib/phoenix_component/macro_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ defmodule Phoenix.Component.MacroComponent do
# LiveView's end to end tests: a macro component that performs
# [syntax highlighting at compile time](https://github.com/phoenixframework/phoenix_live_view/blob/38851d943f3280c5982d75679291dccb8c442534/test/e2e/support/colocated_live.ex#L4-L35)
# using the [Makeup](https://hexdocs.pm/makeup/Makeup.html) library.
#
# ## Directives
#
# Macro components may return directives from `transform/2` which can be used to influence
# other elements in the template outside of the macro component at compile-time. For example:
#
# ```elixir
# defmodule MyAppWeb.TagRootSampleComponent do
# @behaviour Phoenix.Component.MacroComponent
#
# @impl true
# def transform(_ast, _meta) do
# {:ok, "", %{}, [root_tag_annotation: "test1", root_tag_annotation: "test2"]}
# end
# end
# ```
# The following directives are currently supported:
#
# ### Options
#
# * `:root_tag_annotation` - A value to apply as an annotation to all root tags during template compilation.
# Requires that a `:root_tag_annotation` be configured for the application. Value must be a string. May be
# provided multiple times to apply multiple annotations. See the docs for `Phoenix.Component` for details on

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an issue with this: having multiple HTML attributes with the same name is invalid

<div phx-r phx-r="foo" phx-r="bar" />

@josevalim had an idea to call it root_tag_attributes instead and then do something like this

# default: no root_tag_attributes configured
config :phoenix_live_view, root_tag_attributes: [some_attribute: "some-value"]

Then, the configuration for phx-r would be

config :phoenix_live_view, :colocated_css, style_attribute: "phx-r"

And we'd add our style attribute to that list:

attrs = Application.get_env(:phoenix_live_view, :root_tag_attributes, [])

if style = Keyword.get(Application.get_env(:phoenix_live_view, :colocated_css, []), :style_attribute) do
  [{style, true} | attrs]
else
  attrs
end

Application.put_env(:phoenix_live_view, :root_tag_attributes, attrs)

I'm just not sure right now where that code would live.

Then the macro component would return

style_attribute = Application.get_env(:phoenix_live_view, :colocated_css, []) |> Keyword.get(:style_attribute, "phx-r")

{:ok, ..., root_tag_attributes: [{style_attribute, hash}]}

So we expect different macro components to use different attributes. The tag engine would then merge the attributes from the directive with the attributes from the config (attributes = Map.new(config_attrs ++ directive_attr)).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like this?

config :phoenix_live_view, root_tag_attribute: "phx-r"
config :phoenix_live_view, :colocated_css, style_attribute: "phx-css"
style_attribute = Application.get_env(:phoenix_live_view, :colocated_css, []) |> Keyword.get(:style_attribute, "phx-r")

{:ok, ..., root_tag_attributes: [{style_attribute, hash}]}

This would mean that all root elements get phx-r but only root elements within components with a ColocatedCSS MacroComponent would additionally have phx-css="SCOPE_HERE". I think this would prevent us from having to "stuff" our own attribute into the config as you mention in the below example, while also providing us enough information to scope CSS correctly?:

attrs = Application.get_env(:phoenix_live_view, :root_tag_attributes, [])

if style = Keyword.get(Application.get_env(:phoenix_live_view, :colocated_css, []), :style_attribute) do
  [{style, true} | attrs]
else
  attrs
end

Application.put_env(:phoenix_live_view, :root_tag_attributes, attrs)

ColocatedCSS could then set a lower bound as any element which has phx-r but doesn't have phx-css="SCOPE_HERE", I think.

Maybe this provides a good middle-ground. Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for me! (cc @josevalim)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discussion @SteffenDE and I had were more from the point of view of adding feature feature. I think for now we only need:

config :phoenix_live_view, :colocated_css, style_attribute: "phx-css"

And we will initialize root_tag_attributes in the engine as:

if style = Keyword.get(Application.get_env(:phoenix_live_view, :colocated_css, []), :style_attribute) do
  [{style, true}]
else
  []
end

The :root_tag_attributes configuration will be "in case we need it", no need to add it right now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally, I'd like to avoid css specific code in the tag engine if possible, but José can overrule me :D

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SteffenDE unfortunately you may need to be overulled. Since this is happening at compile-time, I don't think we'd have an appropriate moment to copy the configuration from CSS to root_tag_attributes :( As there is no entry point. I don't think we even require the Phoenix LiveView to be started? Unless we can do it when we start the LivView compiler or similar...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my proposed approach I think you don't have to copy over anything from CSS to root_tag_attributes (but I may be missing something):

this sets what attribute you want to be applied to all roots in all templates, or disable root attribute application entirely (which would mean you couldn't use ColocatedCSS, but your templates wouldn't be polluted by the extra attribute you don't use), and is used in the TagEngine:

config :phoenix_live_view, root_tag_attribute: "phx-r"

this configures what special attribute to additional apply for ColocatedCSS separately, and is only used in the ColocatedCSS transform/2 logic

config :phoenix_live_view, :colocated_css, style_attribute: "phx-css"

then the TagEngine knows to always apply root_tag_attribute from the app config to all root elements in the template, but if it encounters a ColocatedCSS macro component at the root level of the template which returns a root_tag_attribute: {"phx-css", scope_here} directive, it knows to also apply the phx-css attribute in addition to the generic phx-r attribute.

So the config is completely separate, and we know to inject the CSS specific attributes as we compile that specific template. The root_tag_attribute config is really to just let them change the generic one to a different tag name or disable it entirely.

WDYT? @josevalim

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are conceptually correct and that's what we want. The downside of this approach is that it configures it twice. It would be nice if the users could opt-in by only configuring the style_attribute. But we can iron out this detail later. So we may need to overrule @SteffenDE for user convenience needs. :) but we can do it later!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense.

If user convenience is the goal, perhaps we could just default the root attribute to being applied as phx-r and the ColocatedCSS style attribute to phx-css and then let them opt in to different attribute names via adding config explicitly, or opt out of root attributes entirely via something like

config :phoenix_live_view, root_tag_attribute: false

🤔 🤔 🤔

but you are right, we can iron out the details later 😎 👍

# root tag annotations and how to configure them.

@type tag :: binary()
@type attribute :: {binary(), Macro.t()}
Expand All @@ -128,9 +152,13 @@ defmodule Phoenix.Component.MacroComponent do
@type tag_meta :: %{closing: :self | :void}
@type heex_ast :: {tag(), attributes(), children(), tag_meta()} | binary()
@type transform_meta :: %{env: Macro.Env.t()}
@type directive :: {:root_tag_annotation, String.t()}
@type directives :: [directive]

@callback transform(heex_ast :: heex_ast(), meta :: transform_meta()) ::
{:ok, heex_ast()} | {:ok, heex_ast(), data :: term()}
{:ok, heex_ast()}
| {:ok, heex_ast(), data :: term()}
| {:ok, heex_ast(), data :: term(), directives :: directives()}

@doc """
Returns the stored data from macro components that returned `{:ok, ast, data}`.
Expand Down
Loading
Loading