Skip to content

Commit ed529b7

Browse files
Add MacroComponent directives and Root Tag Attribute functionality
Supersedes #4116. Co-Authored-By: David Green <134172184+green-david@users.noreply.github.com>
1 parent c0ccedd commit ed529b7

10 files changed

Lines changed: 1132 additions & 49 deletions

File tree

config/test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ config :logger, :level, :debug
44
config :logger, :default_handler, false
55

66
config :phoenix_live_view, enable_expensive_runtime_checks: true
7+
8+
# Disable :root_tag_attribute so the majority of the tests
9+
# are not polluted by it. It will be explicitly re-enabled for
10+
# tests related to :root_tag_attribute functionality.
11+
config :phoenix_live_view, :root_tag_attribute, nil

lib/phoenix_component/macro_component.ex

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,29 @@ defmodule Phoenix.Component.MacroComponent do
120120
# LiveView's end to end tests: a macro component that performs
121121
# [syntax highlighting at compile time](https://github.com/phoenixframework/phoenix_live_view/blob/38851d943f3280c5982d75679291dccb8c442534/test/e2e/support/colocated_live.ex#L4-L35)
122122
# using the [Makeup](https://hexdocs.pm/makeup/Makeup.html) library.
123+
#
124+
# ## Directives
125+
#
126+
# Macro components may return directives from `transform/2` which can be used to influence
127+
# other elements in the template outside of the macro component at compile-time. For example:
128+
#
129+
# ```elixir
130+
# defmodule MyAppWeb.TagRootSampleComponent do
131+
# @behaviour Phoenix.Component.MacroComponent
132+
#
133+
# @impl true
134+
# def transform(_ast, _meta) do
135+
# {:ok, "", %{}, [root_tag_annotation: "test1", root_tag_annotation: "test2"]}
136+
# end
137+
# end
138+
# ```
139+
#
140+
# The following directives are currently supported:
141+
#
142+
# * `:root_tag_annotation` - A value to apply as an annotation to all root tags during template compilation.
143+
# Requires that a `:root_tag_annotation` is configured for the application. The value must be a string.
144+
# May be provided multiple times to apply multiple annotations.
145+
#
123146

124147
@type tag :: binary()
125148
@type attribute :: {binary(), Macro.t()}
@@ -128,9 +151,13 @@ defmodule Phoenix.Component.MacroComponent do
128151
@type tag_meta :: %{closing: :self | :void}
129152
@type heex_ast :: {tag(), attributes(), children(), tag_meta()} | binary()
130153
@type transform_meta :: %{env: Macro.Env.t()}
154+
@type directive :: {:root_tag_annotation, String.t()}
155+
@type directives :: [directive]
131156

132157
@callback transform(heex_ast :: heex_ast(), meta :: transform_meta()) ::
133-
{:ok, heex_ast()} | {:ok, heex_ast(), data :: term()}
158+
{:ok, heex_ast()}
159+
| {:ok, heex_ast(), data :: term()}
160+
| {:ok, heex_ast(), data :: term(), directives :: directives()}
134161

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

lib/phoenix_live_view/html_formatter.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ defmodule Phoenix.LiveView.HTMLFormatter do
312312
process_buffer: &process_buffer/1
313313
)
314314
|> case do
315-
{:ok, nodes} ->
316-
nodes
315+
{:ok, result} ->
316+
result.nodes
317317
|> transform_tree(source, newlines)
318318
|> HTMLAlgebra.build(opts)
319319
|> Inspect.Algebra.format(line_length)

lib/phoenix_live_view/tag_compiler/compiler.ex

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule Phoenix.LiveView.TagCompiler.Compiler do
22
@moduledoc false
33

4+
alias Phoenix.LiveView.TagCompiler.ParseResult
45
alias Phoenix.LiveView.TagCompiler.Tokenizer.ParseError
56

67
@doc """
@@ -15,7 +16,7 @@ defmodule Phoenix.LiveView.TagCompiler.Compiler do
1516
text and expression parts and properly invoking the engine
1617
with the correct code for features like components and slots.
1718
"""
18-
def compile(nodes, opts) do
19+
def compile(%ParseResult{nodes: nodes, directives: directives}, opts) do
1920
{engine, opts} = Keyword.pop(opts, :engine, Phoenix.LiveView.Engine)
2021
tag_handler = Keyword.fetch!(opts, :tag_handler)
2122

@@ -26,8 +27,11 @@ defmodule Phoenix.LiveView.TagCompiler.Compiler do
2627
caller: Keyword.fetch!(opts, :caller),
2728
source: Keyword.fetch!(opts, :source),
2829
tag_handler: tag_handler,
29-
# slots is the only key that is updated when traversing nodes
30-
slots: []
30+
root_tag_attribute: Application.get_env(:phoenix_live_view, :root_tag_attribute, false),
31+
root_tag_attributes: Keyword.get_values(directives, :root_tag_attribute),
32+
# The following keys are updated when traversing nodes
33+
slots: [],
34+
local_root?: true
3135
}
3236

3337
# Live components require a single, static root tag.
@@ -328,7 +332,7 @@ defmodule Phoenix.LiveView.TagCompiler.Compiler do
328332

329333
with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->
330334
substate = handle_tag_and_attrs(name, attrs, ">", to_location(meta), substate, state)
331-
{_child_state, substate} = handle_node(children, substate, state)
335+
{_child_state, substate} = handle_node(children, substate, %{state | local_root?: false})
332336
substate = state.engine.handle_text(substate, [to_location(close_meta)], "</#{name}>")
333337
{state, substate}
334338
end)
@@ -353,7 +357,7 @@ defmodule Phoenix.LiveView.TagCompiler.Compiler do
353357
meta,
354358
close_meta,
355359
substate,
356-
state
360+
%{state | local_root?: true}
357361
)
358362

359363
ast =
@@ -383,7 +387,10 @@ defmodule Phoenix.LiveView.TagCompiler.Compiler do
383387

384388
with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->
385389
{assigns, attr_info, slot_info} =
386-
build_component_assigns(ref, attrs, children, meta, close_meta, substate, state)
390+
build_component_assigns(ref, attrs, children, meta, close_meta, substate, %{
391+
state
392+
| local_root?: true
393+
})
387394

388395
store_component_call({mod, fun}, attr_info, slot_info, line, state)
389396
call_meta = [line: line, column: column]
@@ -420,7 +427,10 @@ defmodule Phoenix.LiveView.TagCompiler.Compiler do
420427
with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->
421428
# Process children in a new nesting
422429
{assigns, attr_info, slot_info} =
423-
build_component_assigns(ref, attrs, children, meta, close_meta, substate, state)
430+
build_component_assigns(ref, attrs, children, meta, close_meta, substate, %{
431+
state
432+
| local_root?: true
433+
})
424434

425435
store_component_call({mod, fun}, attr_info, slot_info, line, state)
426436
call_meta = [line: line, column: column + mod_size]
@@ -479,17 +489,41 @@ defmodule Phoenix.LiveView.TagCompiler.Compiler do
479489

480490
defp handle_tag_and_attrs(name, attrs, suffix, meta, substate, state) do
481491
text =
482-
if Application.get_env(:phoenix_live_view, :debug_attributes, false) do
483-
"<#{name} data-phx-loc=\"#{meta[:line]}\""
484-
else
485-
"<#{name}"
486-
end
492+
"<#{name}"
493+
|> maybe_add_phx_loc(meta)
494+
|> maybe_add_root_tag_attributes(state, meta)
487495

488496
substate = state.engine.handle_text(substate, meta, text)
489497
substate = handle_tag_attrs(meta, attrs, substate, state)
490498
state.engine.handle_text(substate, meta, suffix)
491499
end
492500

501+
defp maybe_add_phx_loc(text, meta) do
502+
if Application.get_env(:phoenix_live_view, :debug_attributes, false) do
503+
"#{text} data-phx-loc=\"#{meta[:line]}\""
504+
else
505+
text
506+
end
507+
end
508+
509+
defp maybe_add_root_tag_attributes(text, %{local_root?: true} = state, _meta) do
510+
case state do
511+
%{root_tag_attribute: root_tag_attribute} when is_binary(root_tag_attribute) ->
512+
attrs =
513+
[{root_tag_attribute, true} | state.root_tag_attributes]
514+
|> Phoenix.HTML.attributes_escape()
515+
|> Phoenix.HTML.safe_to_string()
516+
517+
# Phoenix.HTML.attributes_escape/1 adds a leading space automatically
518+
"#{text}#{attrs}"
519+
520+
%{root_tag_attribute: _} ->
521+
text
522+
end
523+
end
524+
525+
defp maybe_add_root_tag_attributes(text, _state, _meta), do: text
526+
493527
defp handle_tag_attrs(meta, attrs, substate, state) do
494528
Enum.reduce(attrs, substate, fn
495529
{:root, {:expr, _, _} = expr, _attr_meta}, substate ->

0 commit comments

Comments
 (0)