Recursive surface component?

Is it possible to have a Surface component that instantiates itself from within (conditionally, of course)
I was trying to do this in mail_node.sface:

        <MailNode :for={{ id <- MailClient.children(@mail_client, @id) }}
                  props={{ id: id,                                            
                       meta: MailClient.mail_meta(@mail_client, id),             
                       mail_client: @mail_client }} />

Ant it complains:

** (CompileError) lib/liv_web/live/mail_node.sface:8: you are trying to use the module LivWeb.MailNode which is currently being defined.

If this is not possible, how do I model a recursive data structure like a tree with surface component?

Hi @derek-zhou!

Currently, Surface does not allow using a component recursively. However, you can work around this issue by moving the recursion to a separate function. For instance:

defmodule Tree do
  ...
    
  @doc "The root node"
  prop node, :map
  
  def render(assigns) do
    ~H"""
    {{ render_node(@node) }}
    """
  end

  defp render_node(node) do
    ~H"""
    <div>
      {{ node.name }}
      <div :for={{ child <- node.children }}>
        {{ render_node(child) }}
      </div>
    </div>
    """
  end
end
4 Likes

Thanks. I will try this and the other venue I am trying, which is to use a MacroComponent that hide the recursion inside.

@msaraiva 's example can be simplified to:

defmodule Tree do
  ...
    
  @doc "The root node"
  prop node, :map
  
  def render(assigns) do
    ~H"""
    <div>
      {{ @node.name }}
      <div :for={{ child <- @node.children }}>
        {{ render(%{node: child}) }}
      </div>
    </div>
    """
  end
end

Although the component cannot be recursively defined, the render/1 method can call itself recursively, with a made up assigns. This method works as long as the component has no state; because state-less component is basically just a render/1 function with maybe some pure function helpers.

I don’t know how to achieve the equivalent of recursive stateful component though. I don’t have a usage case so far.

Note: If you are going to call render/1 manually with a made up assigns like above, it is best to pass in @socket too, like {{ render(%{socket: @socket, node: child}) }} The reason is components pass @socket implicitly, and many things need @socket; so without the @socket in the assigns it can be very confusing.