Overriding @inner_block for LiveView component(s)

I’d like to have a generic, abstract (I apologise for the OO terminology :wink: component, which takes care of all the styling and can be rendered by calling its function from specific “subclass instances” providing some additional bits like title, size and the actual content. This seem to work well until I want to change the default @inner_block. It seems to have some structure:

inner_block: [
    %{
      __slot__: :inner_block,
      inner_block: #Function<1.68901124/2 in PhxAppWeb.PageLive.render/1>
    }
  ]

which makes it hardly override-able… or? Is there a way to keep the “default slot” being default but overriding its content when needed rather than using some conditionals?

I understand I can rewrite things and work this around but would like to know that I have to :wink:

Docs are here: Phoenix.Component — Phoenix LiveView v0.17.11

Also interested in this.

I’m playing around with a similar idea on a attempt to make it easier to use HotwireTurbo on phoenix.

For now I have the following hack to reuse a generic component. Not really sure yet what parameters the inner_block function expects so I’m just ignoring them for now.

  def render_inline_stream(conn, action, target, rendered_template \\ nil) do
    rendered =
      TurboStreamComponent.stream_tag(%{
        action: action,
        target: target,
        inner_block: %{inner_block: fn _, _ -> rendered_template end}
      })
      |> Phoenix.HTML.Safe.to_iodata()
      |> IO.iodata_to_binary()

    conn
    |> put_resp_content_type("text/vnd.turbo-stream.html")
    |> text(rendered)
  end

The idea here is just to wrap an existing template on a <turbo-stream></turbo-stream> tag

Can you describe this with a few examples? It’s not really clear why you’d need to alter inner_block?

Ane example - let’s say I pass default content inside a component. Something like

<.button>
  This renders <strong>inside</strong> the button!
</.button>

The above taken from the docs mentioned above. And in the majority of cases it is OK. F. e.

<.queue_widget>
  The queue is empty - nothing to see here
</.queue_widget>

But on some rare occasions there is something in the queue and needs to be put out. One of the ways to do it would be to override the default content provided in @inner_block (there is no pubsub involved and the widget does not update itself on change). I understand it is not the only way to do it but would like to know if this is at all feasible.

What’s wrong with this as the template for <.queue_widget>?

<%= if @queue_size > 0 do %>
  …
<% else %>
  <%= render_slot(@inner_block) %>
<% end %>
1 Like

The template is currently common for all widgets and is located in the “abstract” one. Specific widgets provide their name, size and default content, which I’d like to override on occasions but want to keep the abstract widget generic so that it doesn’t have to “know” what it is rendering. I can do generic “assign” and use that. That’s what I am now doing but I feel like running in circles around the original idea of just assigning stuff to @inner_block

I think I still don’t know exactly what you’re attempting here. If your abstract components is meant to be abstract, then it shouldn’t be involved in markup at all. It feels a bit strange to try to abstract on one hand and on the other still try to break out of the abstraction.

Hmm… The abstraction means that all things common go there - widget has a bounding box, header, title, some styling, etc. Specific widgets provide values for the variable parameters. At least that’s how I used to do those things. Here I have a function called widget/1 and specific ones like queue_widget/1 which assigns its title, size etc. calling the “abstract” widget/1 in the end for rendering the actual output

That sounds like slots and overwriting default slot markup would be the solution, but hard so say without code.

defp widget(assigns) do
		widget_size_class = case assigns[:size] do
			:single -> "widget"
			:double -> "widget-double"
			_ -> "widget"
		end

		~H"""
		<div class={widget_size_class} data-widget-id={@widget_id}>
			<div class="titlebar">
				<div class="titlebar-close"></div>
				<div class="titlebar-title">
					<%= @widget_title %>
				</div>
			</div>
			<div class="widget-content">
				<%= render_slot(@inner_block) %>
			</div>
		</div>
		"""
	end

	def queue_widget(assigns) do
		assigns = assigns
		|> assign(:widget_title, gettext("Queue"))
		|> assign(:widget_id, "queue")

		widget(assigns)
	end

That’s how I originally thought of doing it. And there should be part assigning the non-default content in queue_widget/1

My directly delegating to widget as a function I feel like you’re leaving options lying on the ground. <.queue_widget> and <.widget> can have different views on @inner_block.

def queue_widget(assigns) do
  assigns = 
    assigns
    |> assign(:widget_title, gettext("Queue"))
    |> assign(:widget_id, "queue")
    |> assign_new(:inner_block, fn -> [] end)

  ~H"""
  <.widget>
    <%= if Enum.empty?(@inner_block) do %> 
      …queue widget default
    <% else %>
      <%= render_slot(@inner_block) %>
    <% end %>
  </.widget>
  """
end

That would be almost perfect but how does the “abstract” widget get the other assigns if I don’t call it as a function but rather the way you wrote above?

def queue_widget(assigns) do
  pass_through = assigns_to_attributes(assigns, [])
  assigns = 
    assigns
    |> assign(:widget_title, gettext("Queue"))
    |> assign(:widget_id, "queue")
    |> assign(:pass_through, pass_through)
    |> assign_new(:inner_block, fn -> [] end)

  ~H"""
  <.widget widget_title={@widget_title} widget_id={@widget_id} {@pass_through}>
    <%= if Enum.empty?(@inner_block) do %> 
      …queue widget default
    <% else %>
      <%= render_slot(@inner_block) %>
    <% end %>
  </.widget>
  """
end
1 Like

Thank you, Benjamin. While this is noticeably more verbose than what I admittedly was hoping for. It still looks to me more “correct” than what I do now. And… it can probably be shortened to:

<.widget {assigns_to_attributes(assigns, [])}>

without introducing the pass_through part, can’t it? Seems to work and is more pleasing to the eye :wink:

I’d be cautious about change tracking though – that’s one of the reasons why assigns is not just a dump map you can edit.

1 Like

Roger, tnx

Might be Captain Obvious but noticed that doing simply

<.widget {assigns}>

seems to work too. Tried to check this against change tracking docs but am still unsure about the change tracking reliability in such case/usage. Do you have any pointers?

Oh re-checking the docs it doesn’t say, but if other sources I’ve seen like LiveView Assigns: Three Common Pitfalls and Their Solutions | AppSignal Blog are correct then it is a problem for change tracking. Would be good to have a definitive answer (and update the docs if needed…) pinging @chrismccord :slight_smile:

1 Like

Ah I found another source and the relevant phrase in the docs which confirms it: Assigns and HEEx templates — Phoenix LiveView v0.20.2

Generally speaking, avoid accessing variables inside LiveViews, as code that access variables is always executed on every render. This also applies to the assigns variable.

I’d find it useful if LiveView added something like @assigns for cases where you need it, like when using assigns_to_attributes.

1 Like

I read that paragraph in the docs too. But I couldn’t be sure whether doing things like <.widget {assigns}> constitutes “accessing variables inside LiveViews” - IOW whether it is equivalent to e. g. <%= assigns.my_assign %> or more to calling widget(assigns) and how do the two differ from change tracking perspective. The second source you linked to, lists the two constructs next to each other though. OTOH I am still not sure how does the approach, which @LostKobrakai suggested (with assigns_to_attributes/2 and @pass_thorugh) sidestep the issue.