I have a schema with a status field, that can be :draft, :published, etc. In the index live view, I only want the delete link to be available for items that have not been published, that is they are :draft.
I thought this should be extremely simple:
<%= if article.status == :draft do %>
<:action :let={{id, article}}>
<.link
phx-click={JS.push("delete", value: %{id: article.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
<% end %>
However, this tells me:
== Compilation error in file lib/foobar_web/live/article_live/index.ex ==
** (Phoenix.LiveView.Tokenizer.ParseError) lib/foobar_web/article_live/index.ex:37:11: invalid slot entry <:action>. A slot entry must be a direct child of a component
|
34 | Delete
35 | </.link>
36 | </:action>
37 | <% end %>
| ^
I don’t really get it. As far as I understand, it IS a direct child of a component, all that exists above it is a logic statement, a “preprocessor statement”, which should have been evaluated in an initial pass, or?
In any case, I accept that for some reason this doesn’t work.. But then, how would I achieve what I’m trying to do? It seems common enough that I imagine this should be easy to do. I can’t seem to find the right keywords to search on.
If I re-arrange it a bit so that the <% if … %> is embedded inside the <:action …>, then it does work. Is that the best way to do it?
Tbh this is what convinced me that named slots are not a very good API. They’re a cool idea but they don’t hold up because they don’t compose properly. The introduction of :if and :for is, IMO, an ugly bandaid. Components are a more versatile primitive.
But yeah, as mentioned above, you have to use :if (or potentially :for) if you want to elide the slot itself from the output.
Alternatively you could ditch slots and use a component like <.article_action /> and then your intuitive solution would actually work.
Gives me: undefined variable “article”. Which one get evaluated first, the :let or the :if? Or more to the point, how to I use article in the conditional?
In the OP you are using an article from the outer scope and then rebinding the variable inside the :let. Where did the outer article go?
This is kinda just proving my point, though. I have absolutely no idea how precedence is handled between :let and :if because it’s not real code, it’s a made up template language. So now we’re both confused.
(Just gonna casually tag @bartblast (hi) because this is a pretty clear real-world example of why I am anti-template, which he may appreciate.)
Since this is just an experimental project where I’m trying different things out, I was going to try both solutions. I wanted to start with the one @matt-savvy proposed and then yours. Apart from the :if clause I added, this is standard index view that phx.gen.live would create. I’m guessing that the outer article “variable” is created by <.table>.
The only way I can understand why the variable is not available to the :if would be that it creates new scope that knows nothing about article. If that is the case, then I don’t understand what the use of the conditional clause would be.
I’d love to see an alternative to that. Just having a single body is a limitation of the xml’ish nature of html. And please do not say the abominations of jsx - passing markup as attributes.
You cannot use a variable from :let in the :if. The :if is evaluated before anything related to calculating the :let value is even run. When there’s no slot, then there’s no render_slot to execute and hence no value to provide for :let.
Thanks! I suspected as much, but that also means that :if clause is of very limited us. I’m going to give the custom component alternative a go instead.
The point in this case was that I simply wanted to not allow the user to delete an article once it has been published, so I was trying to hide the “Delete” link in the index view for all published articles.
Okay, your original post uses article, which based on what I’m seeing, did not exist in your scope. This makes sense, since you’re asking about an index view.
The easy ways to do this would be to use either version of the if inside the action slot.
<:action :let={{id, article}}>
<%= if condition %>
...link
<% end %>
</:action>
But it’s funny because while JSX-ish components look like HTML they’re really deeply-nested deferred calls. They don’t behave like HTML at all. The fact that HTML syntax happens to be useful for representing said calls is a fascinating coincidence.
Of course in LiveView they are not deferred unless you use <.live_component />, at which point many useful guarantees mysteriously disappear…
Something like JSX is the best solution I can imagine to the template problem, but I assume what you’re referring to here is something like the “render props” pattern and I agree that is absolutely disgusting. Though still not as messy as named slots because at least it’s clearly communicated they’re flat. Unlike in the OP here where that reality is only discovered downstream of an error message.
I’m tempted to say if nobody has come up with a solution at this point it’s not gonna happen, but maybe that’s a defeatist attitude