This post deals with programmatically scrolling a table in a Shiny app (using the DT library) to a specific row.
I've been working on a Shiny app involving a number of tables, which are displayed using the R DT package. The tables include controls to let the user sort them by any column, which plays a role in today's post. Controls external to the table let the user insert, delete and edit records. The reason I use external controls and not the editing capabilities built into DT is that there are a number of limitations on edits that the application code enforces.
Before getting into the weeds, I need to establish a bit of terminology. The underlying data in any table has associated row numbers, just as in a spreadsheet. Those do not change when the user sorts the displayed table. I'll refer to that as the "row index", and to the position (1 if first, 2 if second, etc.) of a row in the displayed table as the "display index". If the seventeenth row of a table winds up at the top after sorting, it has row index 17 and display index 1. (Sorry if I'm belaboring the obvious.)
Here is the problem I ran into. Suppose that a user selects a row and edits it. When the user commits the change, DT updates the display, and the edited row may wander quite far in either direction and in particular leave the portion of the table being displayed. So I want the application to automatically scroll the table to the new location of the edited record. I initially assumed this would be easy. It was not. In fact, 20+ attempts with the Shiny Assistant coding AI and 10+ attempts with Claude AI failed to solve the problem (although they did produce some insights, and a bit of code described below).
We can split the problem into two parts: finding the new location of the record, and then scrolling to it. It turns out that the latter is the easier part. At least it's the easier part if you know JavaScript, which I do not. One of the bots (I forget which) provided me with the following code to embed in an R function. The arguments to the R function are outputID (the name of the table in the UI) and row (the row number to which to scroll).
js_code <- -="" 1="" 200="" 300="" addclass="" atatable="" behavior:="" block:="" center="" function="" highlight-row="" if="" js_code="" outputid="" pre="" row.="" row="" rownode.scrollintoview="" rownode="" runjs="" s="" scroll="" settimeout="" smooth="" sprintf="" table="" the="" to="" var="">->
I'm not positive, but I believe you need to load the shinyjs R library for this to work. (My app was already using it.)
On to what I originally thought would be the easier part: finding the target record. What the JS code is looking for in the second argument (row number) is actually what I'm calling the display index. My code knows the row index of the target record. Assume the output ID of the table is "A". After some digging, I discovered that input$A_rows_all contains the vector of row indices in display order (meaning the first entry is the row index of the record with display index 1 etc.). So if variable r contains the row index of the record we want, which(input$A_rows_all == r) will give us the display index of that row. Simple enough ... except that it did not work.
Poking around my code, I discovered that after the user committed a change the table display would update "immediately" but that input$A_rows_all retained its pre-change value. This turns out to be a synchronization issue -- DT updates the table and then updates the rows_all vector at it's own pace, while my function attempts to move immediately from the line committing the change to the line looking for the new display index.
The answer was to wrap the code doing the scrolling in an event observer: observeEvent(input$A_rows_all, { ... }) where "..." is the code that finds the target row and scrolls to it. This effectively pauses my code until DT has gotten around to updating the rows_all vector.