Stream empty state

if you have infinite scrolling, then you necessarily have pagination for next/previous page, no? Then you can toggle an empty state assign as other have alluded to when you hit no results without having to check the existing stream contents.

2 Likes

Thinking out loud, this could be a good use case for the :first-child or :last-child pseudoselectors. This will let the browser determine how to display the empty list message based on whether there are any other streamed elements rendered.

I got an example working by adding the following line to the .table core component as the first <tr> before the streamed ones. You could also add it to the end and use the :first-child pseudoselector instead.

          <tr class="hidden last:table-row"><td>No Streamed Items</td></tr>
3 Likes

Yeah, you’re right, that shouldn’t be an issue, using something like the end_of_timeline? assign in the docs.

Oh nice idea! I tried to do something with the :empty pseudoselector, but it’s sensible to whitespace which makes it hard to have something generic.

I have no pagination at all on my articles index template. I tried to apply your fix, but the message is displayed after deleting a single item and even if there is at least 1 item left.

What does the code look like?

You would need something like this:

<ul>
  <li class="first:hidden">No articles</li>
  <li :for={article <- @streams.articles}>
    <%= article.body %>
  </li>
</ul>

Ah, no, I just modified the core_components.ex, function table/1 by adding a single line as you did in your GitHub repo:

<tr class="hidden last:table-row"><td>No Streamed Items</td></tr>`

In my index template the code to display items looks like that:

<.table
    id="articles"
    rows={@streams.articles}
    row_click={fn {_id, article} -> JS.navigate(~p"/articles/#{article}") end}
  >
    <:col :let={{_id, article}} label="Title"><%= article.title %></:col>
    <:col :let={{_id, article}} label="Content">
      <%= String.slice(article.content, 0, 200) %>
    </:col>
    <:action :let={{_id, article}}>
      <div class="sr-only">
        <.link navigate={~p"/articles/#{article}"}>Show</.link>
      </div>
      <.link patch={~p"/articles/#{article}/edit"}>Edit</.link>
    </:action>
    <:action :let={{id, article}}>
      <.link
        phx-click={JS.push("delete", value: %{id: article.id}) |> hide("##{id}")}
        data-confirm="Are you sure?"
      >
        Delete
      </.link>
    </:action>
  </.table>

I was redirected here from this thread :slight_smile:

I’m not @codeanpeace, I’m from that other thread and just butting in since we were talking there :sweat_smile:

The important bit to see is where you put it in the table component. Is it possible you put it outside the tbody or something or maybe have other modifications that would result in there being more than tr left even when there are no articles? I would inspect the DOM after deleting the last one and ensure there is only one tr left.

ops, my bad, I totally messed it up :sweat_smile:.
Here is how looks the table/1 function where I just added a single line like @codeanpeace did (inside tbody section):

<tbody
          id={@id}
          phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
          class="relative text-sm leading-6 border-t divide-y divide-zinc-100 border-zinc-200 text-zinc-700"
        >
        <tr class="hidden last:table-row"><td>No Streamed Items</td></tr>
          <tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
...

What I also noticed is that “No Streamed items” also get visible right after displaying a modal to create a new Item.

1 Like

And still does not work or it does work now?

Before deletion:


After deletion, without refreshing the page:

Weird. It seems like it’s somehow being re-ordered to the after stream’s trs… or something. Maybe try hidden only:table-row.

2 Likes

Yay, using <tr class="hidden only:table-row"><td>No Streamed Items</td></tr> worked like a charm :). Thanks a lot! But not as needed:

  • when an item is deleted, the table just displays the available items.
  • when deleting the last item in the table, you can see the table headers and no “No Streamed items”
  • when refreshing the page, you can still see the table headers and “No Streamed items”.

This is, frankly, not exactly the expected behavior:slightly_frowning_face:

Yep, that stands to reason since the CSS is based on the number of trs.

You can always use JS to hide if empty. If you don’t want to resort to JS you’ll have to try another method from this thread.

However, since you say you only have one page, you possibly don’t need a stream at all. If you hold all the articles in state then your original code (from the other thread) will work and this problem goes away. Streams are only necessary if you have very large amounts of data of an unknown size.

1 Like

TIL there’s an only-child pseudoselector – nice!

Yeah, when I first had the idea, I had <ul>/<ol> and <li> elements in mind where it would be much less of an issue or even desired behavior. I ended up applying the idea to the .table core component purely out of convenience since that was what was available in a newly generated Phoenix app.

1 Like

I also TIL’d looking into this issue even though it’s been supported as early as 17 years ago now (Firefox) according to caniuse :sweat_smile:

3 Likes

I have the same issue as you (not for a <table> though, but with a list of <div>), and there’s a strange behavior when removing an item.

I have the following code:

<main
  id="shift-list"
  phx-update="stream"
>
  <.empty icon="clock-off" class="hidden only:block">
    <:title><%= dgettext("events", "No shifts") %></:title>
    <:subtitle><%= dgettext("events", "There are no shifts for this event.") %></:subtitle>
  </.empty>

  <.live_component
    :for={{id, shift} <- @streams.shifts}
    id={id}
    shift={shift}
  />
</main>

Both components are basically <div>. It works fine when there’s only one item or more, my empty state div is hidden, and the items are visible underneath:

Then, when I delete the item, a new empty state is added:

And I think even more are added on each update of the list.

@chrismccord can we put non-stream stuff inside a phx-update="stream" container?

Actually, instead of using the stream (proposed by default when using a generator), I use a classic assign {:ok, assign(socket, :items, list_items())} as the list is not too large. But it looks a bit frustrating that such a classic task makes so much trouble :slight_smile:

1 Like

Since you are using the classic assign and assuming list_items() always returns a list, you could conditionally show/hide with something like this:

<.table :if={Enum.any?(@items)} ...>...</.table>
<span :if={Enum.empty?(@items)}>No Articles</span>
1 Like

Unless something has changed very recently, the generator is coded in a way that works with both a stream or a regular list. Perhaps the with line at the top of the table component that is confusing? The case version would look like this:

assigns =
  case assigns do
    %{rows: %Phoenix.LiveView.LiveStream{} ->
      assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)

    assigns ->
      assigns
  end

I was experimenting with a similar setup and I was having the same problem, adding an id to the empty placeholder block (in your case, the .empty component) fixed the issue