Defining attributes for Phoenix component from a list

I’m not a macro expert, so maybe someone can help me understand this.

I want to define attributes in my function component from a list. I’ve used the following approach successfully with Ecto schemas in the past:

@list_of_fields ~w(field1 field2 field3 ... )a

for field_name <- @list_of_fields do 
   field field_name, :string
end

But when I try to do the same with a function component:

@list_of_attributes ~w(attr1 attr2 attr3 ... )a

for attr_name <- @list_of_attributes do 
   attr attr_name, :string
end

I get a lovely:

== Compilation error in file lib/live_select.ex ==
** (FunctionClauseError) no function clause matching in Phoenix.Component."MACRO-attr"/4    
    
    The following arguments were given to Phoenix.Component."MACRO-attr"/4:
    
        # 1
        #Macro.Env<aliases: [{ChangeMsg, LiveSelect.ChangeMsg}], context: nil, context_modules: [LiveSelect], file: "/Users/max/code/live_select/lib/live_select.ex", function: nil, functions: [{Phoenix.Component, [assign: 2, assign: 3, assign_new: 3, assigns_to_attributes: 1, assigns_to_attributes: 2, changed?: 2, dynamic_tag: 1, focus_wrap: 1, form: 1, inputs_for: 1, intersperse: 1, link: 1, live_component: 1, live_file_input: 1, live_flash: 2, live_img_preview: 1, live_render: 2, live_render: 3, live_title: 1, to_form: 1, to_form: 2, update: 3, upload_errors: 1, upload_errors: 2]}, {Kernel, [!=: 2, !==: 2, *: 2, **: 2, +: 1, +: 2, ++: 2, -: 1, -: 2, --: 2, /: 2, <: 2, <=: 2, ==: 2, ===: 2, =~: 2, >: 2, >=: 2, abs: 1, apply: 2, apply: 3, binary_part: 3, binary_slice: 2, binary_slice: 3, bit_size: 1, byte_size: 1, ceil: 1, div: 2, elem: 2, exit: 1, floor: 1, function_exported?: 3, get_and_update_in: 3, get_in: 2, hd: 1, inspect: 1, inspect: 2, is_atom: 1, is_binary: 1, is_bitstring: 1, ...]}, {Phoenix.LiveView.Helpers, [live_file_input: 2, live_img_preview: 2, live_patch: 1, live_patch: 2, live_redirect: 1, live_redirect: 2, live_title_tag: 1, live_title_tag: 2]}], lexical_tracker: #PID<0.252.0>, line: 262, macro_aliases: [], macros: [{Phoenix.Component.Declarative, [def: 2, defp: 2]}, {Phoenix.Component, [attr: 2, attr: 3, embed_templates: 1, embed_templates: 2, render_slot: 1, render_slot: 2, sigil_H: 2, slot: 1, slot: 2, slot: 3]}, {Kernel, [!: 1, &&: 2, ..: 0, ..: 2, ..//: 3, <>: 2, @: 1, alias!: 1, and: 2, binding: 0, binding: 1, dbg: 0, dbg: 1, dbg: 2, def: 1, defdelegate: 2, defexception: 1, defguard: 1, defguardp: 1, defimpl: 2, defimpl: 3, defmacro: 1, defmacro: 2, defmacrop: 1, defmacrop: 2, defmodule: 2, defoverridable: 1, defp: 1, defprotocol: 2, defstruct: 1, destructure: 2, get_and_update_in: 2, if: 2, in: 2, is_exception: 1, ...]}, {Phoenix.LiveView.Helpers, [live_component: 2, live_component: 3, render_block: 1, render_block: 2, sigil_L: 2]}], module: LiveSelect, requires: [Application, Kernel, Kernel.Typespec, Phoenix.Component, Phoenix.Component.Declarative, Phoenix.LiveView.Helpers, Phoenix.Template], ...>
    
        # 2
        {:attr_name, [line: 262], nil}
    
        # 3
        :string
    
        # 4
        []
    
    (phoenix_live_view 0.18.13) lib/phoenix_component.ex:1824: Phoenix.Component."MACRO-attr"/4
    (phoenix_live_view 0.18.13) expanding macro: Phoenix.Component.attr/2
    lib/live_select.ex:262: LiveSelect (module)

Why does it work in one case but not in the other? Is there a way to make it work?

Cheers

In ecto the context is the schema macro, which was probably made to be able to deal with for to define fields. For function component attributes you likely need to use unquote to pass the arguments to attr for things to work.

Thanks @LostKobrakai . I had tried that already earlier, and just to be sure I tried it again:

for attr_name <- @list_of_attributes do 
   attr unquote(attr_name), unquote(:string)
end

And I get the same error with of course a different list of arguments:

...

# 2
{:unquote, [line: 261], [{:attr_name, [line: 261], nil}]}
    
# 3
{:unquote, [line: 261], [:string]}
    
# 4
[]

If I also try to unquote the last optional argument ([]) with:

attr unquote(attr_name), unquote(:string), unquote([])

I get a different error:

== Compilation error in file lib/live_select.ex ==
** (FunctionClauseError) no function clause matching in Phoenix.Component.attr/3    
    (phoenix_live_view 0.18.13) expanding macro: Phoenix.Component.attr/3
    lib/live_select.ex:261: LiveSelect (module)

I’m actually a bit confused, I thought unquote can only be used inside a quote block?

You can use unquote in module bodies as well.

1 Like

I found it. If I just copy the code wrapped by the attr/3 macro and do:

for attr_name <- @list_of_attributes do
    Phoenix.Component.Declarative.__attr__!(
      __MODULE__,
      attr_name,
      :string,
      [],
      __ENV__.line,
      __ENV__.file
    )
  end

it works.

I bet the problem are simply the guards in attr/3, specifically the is_atom(name) guard.
When calling the macro from the for comprehension for some reason name is not the atom behind attr_name but the AST representation of the variable attr_name ({:attr_name, [line: 262], nil}), which doesn’t pass the guard.

I guess it makes sense in retrospect: the AST representation of an atom is an atom, but the AST of a variable bound to an atom is not.

For posterity, this has been fixed: move guards in attr macro to __attr__! (#2449) · phoenixframework/phoenix_live_view@590a34f · GitHub