How We Implemented Find-On-Page With Infinite Scroll

Within Streak (a CRM built into Gmail), our main view is a giant table of all your leads (or boxes as we call them). We never wanted a user to have to page through this, instead we wanted them to be able to just scroll their entire list (they can filter if they want).

In a previous technical post we demonstrated how we could create a table that was performant even while displaying hundreds of thousands of rows. The main idea was you only render a subset of the table on screen at any given time. Then when the user scrolls we adjust what rows are visible on screen and move them around.

This brings up another interesting problem — if the entire table isn’t actually on the page, how does a user perform a CTRL/CMD+F to bring up a search box and jump between the different results. The built in browser ‘Find” function just wouldn’t work.

What follows is a technical discussion on how we made it happen.

Most webpages don’t need to implement their own form of search in a page as the browser handles that for you. However, the way our table view works we only actually render the rows that you see and simply reuse those rows and change their contents. This means that the browser find-in-page won’t work since the box contents you’re looking for won’t actually be on the page.

To understand how we implemented our solution you first have to get a broader overview of the way the spreadsheet is implemented.

From the diagram you can see that we used a Model-View-Controller-ish approach. The actual HTML renderers are the searchBoxView and spreadsheetView javascript files. These have references to the corresponding controllers and models.

To understand how the system works I’ll run through a very simple use case: clicking on a cell to make it active.

  1. click event gets handled by the spreadsheetView
  2. spreadsheetView extracts the row and column information encoded in the cell’s HTML attributes
  3. spreadsheetView calls a function into the spreadsheetController saying “activate the cell at this coordinate”
  4. spreadsheetController updates the virtualCursorModel to set the cursor position to the active cell coordinate
  5. virtualCursorModel emits an event that something has changed
  6. spreadsheetView handles the cursor event and updates the cursor position/rendering
  7. spreadsheetController calls activate on the spreadsheetModel
  8. spreadsheetModel does whatever “activate” means for that coordinate position, and when finished emits an event that something has changed
  9. spreadsheetView handles the event from spreadsheetModel and updates the table rendering accordingly
  10. when spreadsheetView is rendering the visible cells it asks both virtualCursorModel and spreadsheetModel what are the contents and state of this cell

Previously we didn’t have a fleshed out cursor data structure, so adding the virtualCursorModel class introduced steps 4,5,6 and modifies step 10. While the update and render loop gets a bit more complicated it also makes for a fantastically versatile system that let us implement search in an elegant way, and support other features in the future.

When you kick off a search, here are the steps:

  1. searchBoxView gets keypress event and gets current input text
  2. calls new search into searchController
  3. searchController iterates through the data of the spreadsheet adding matching coordinates to the result set
  4. searchController updates “highlighted” coordinate set in the virtualCursorModel
  5. virtualCursorModel updates its set of cursor information and emits updated event
  6. spreadsheetView handles cursor updated event and updates the rendered HTML

This design will allow us to later do things like highlight what other users viewing the pipeline are focusing on at the moment. The idea being that cells and the virtual cursor(s) are mostly dumb elements that get updated by other smarter components.

Hopefully you will find this feature helpful, let us know how it performs.