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
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.
Thank you for your reply 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"))
}
}