Idiomatic way to insert incoming turbo-stream elements (sorted, not prepend/append)

I am having some difficulty figuring out the best way to place an element (from an incoming turbo-stream) in a specific spot in a list of previous elements. All the examples either simply prepend or append.

My situation:

I have a list of elements sorted by a ‘start’ value. The initial list is sorted by the server. The user has control over the start value, thus adding a new element, should insert the element properly sorted, but without additional magic they can only be added to the start or end of the list (or is there more to learn here?). Elements can also come in through a websocket stream.

I was initially thinking there was some way to catch the turbo:before-stream-render event and reorder there, but I cannot find the right way to do this (how do I get them element? or newBody?). Then I explored having a controller for each element, with a Value called start, this provides the ‘startValueChanged’ method that could be a good hook. From there I could trigger a re-order in the parent element. This feels very wrong though as it reorders the initial list over and over again for each (already ordered list). But it does work :slight_smile:

Another way would be to dispatch an event to the parent controller on connect of the child (would be easier with: Add a convenientce method for dispatching DOM events inside a controller by adammiribyan · Pull Request #302 · hotwired/stimulus · GitHub), and using a small timeout that you keep cancelling in the parent, and then only sort at the end. But it feels slightly convoluted…

My main question is: what is the idiomatic way?

Back in the UJS days, I would simply return the sorted list after a create, with the new item already in order with the previous list, and replace the entire list. Unless your list is extremely long. This is still probably simpler and faster than trying to merge a single item into a particular spot.

Walter

Thank you for your reply :slight_smile: I also considered this, but found the combination with websockets a bit odd, causing a flurry of web requests for a simple sorting operation.

I ended up going for the event/timer combo, but would love to hear other options too as this seems to be ‘visually’ noticeable.

# ItemController.js
import { Controller } from "stimulus"

export default class extends Controller {
  static values = { start: Number }

  startValueChanged() {
    document.dispatchEvent(new Event("items:requestSort"))
  }
}

# ItemsController.js
import { Controller } from "stimulus"

var sortTimer = null

export default class extends Controller {
  static targets = ["item"]

  connect() {
    document.addEventListener('items:requestSort', this.triggerSort.bind(this))
  }

  disconnect() {
    clearTimeout(sortTimer)
  }

  // Set a timer that will trigger sorting of the list, or cancel the current one
  // and wait some more. This mechanism prevents sorting the list multiple times
  // without need.
  triggerSort(event) {
    clearTimeout(sortTimer)
    sortTimer = setTimeout(this.sort.bind(this), 50)
  }

  // Sort all the underlying items by the `data-start` attribute.
  sort() {
    this.itemTargets
      .sort(({ dataset: { itemStartValue: a } }, { dataset: { itemStartValue: b } }) => a - b)
      .forEach((item) => this.element.appendChild(item))
  }
}

I fixed the visual effects, by adding an data-item-sorted-value="false" to each server rendered item and added a CSS rule .item[data-item-sorted-value="false"] that hides unsorted items, that gets set to true after it has been sorted at least once. I changed the sort method to this:

sort() {
    this.itemTargets
      .sort(({ dataset: { itemStartValue: a } }, { dataset: { itemStartValue: b } }) => a - b)
      .forEach((item) => {
        item.dataset.itemSortedValue = true
        this.element.appendChild(item)
      })
  }

I also only dispatched the event for elements coming in with the data-item-sorted-value=false, this allowed me to skip sorting elements that the server already sorted:

static values = { sorted: Boolean, start: Number }

startValueChanged() {
    if (!this.sortedValue) {
      document.dispatchEvent(new Event("items:requestSort"))
    }
  }

Why not with CSS? It seems a perfect fit for flexbox order property!

Oh wow. That is much better. I need to read up on my CSS. Thanks!