Shpadoinkle-widgets offers a feature-rich table widget. It features:
The ability to fully customize the styling
Generic handling of turning tabular data into an HTML table
Generic handling of data filters
Generic handling of sorting data by any column (in ascending or descending order)
Built-in and customizable interactions with table headers to sort by a column
Generic handling of lazily loaded and lazily populated tables with infinite scroll
All of this is achieved using only a few hundred lines of code, located in the modules
A "lazily loaded table," for our purposes, is one where the data is not all loaded up front but rather is loaded as the user interacts with the table, using an infinite scrolling experience.
A "lazily populated table," for our purposes, is one where the DOM nodes for table rows are not necessarily created up front, but rather may be created on demand as the user scrolls. A lazily populated table may or may not be lazily loaded.
Both lazily loaded tables and lazily populated tables are handled using
Let’s start with the case of a regular non-lazy table. This case depends on a few concrete types:
Sort represents a sort order (ascending or descending).
SortCol represents a way of sorting the table. It consists of a column and a sort order.
Row are data families. The user of the table widget must instantiate each of these data families for their use case.
Column should be an enumerable data type with a finite number of cases, one for each column of the table.
Row should be a view model for a table row.
Row are parameterized by a single type
a. This type
a is a view model for a table, almost. The type
a will be supplemented with some data which the table widget uses to maintain its state, in order to create a full view model for a table. This is done by putting
a as an element of a tuple.
For a basic table widget, a view model consists of
a and a
SortCol a. A
SortCol a consists of a
Column a and a
Sort. So a view model for a basic table widget is a tuple
(a, SortCol a). For a lazy table widget, there is some more data that gets added onto the view model.
a must be an instance of the
Tabular type class. This type class explains to the table widget module how to render the table and create the basic user interactions. It has the following pieces:
A constraint family
Effect awhich takes a type of kind
* → *(in practice, a monad). This puts the constraints on the monad where event handlers run which those event handlers require in order to do their work. By default,
Effect a mis a constraint which requires that
toRows :: a → [Row a]which extracts the rows from the table data.
toFilter :: a → (Row a → Bool)which tells the table widget how to filter rows. This implies that the state of the table filters (if any) must be on
a. The default implementation of
toFilterdoes not filter out any rows.
toCellwhich tells the table widget how to render the contents of a cell.
sortTablewhich tells the table widget how to compare two rows to say which should come first based on the sort order.
ascendingIconwhich tells the table widget how to render an icon indicating that a certain column is the one we are sorting by and the sort is in ascending order. By default this is an arrow pointing upwards rendered in Unicode.
descendingIconwhich tells the table widget how to render an icon indicating that a certain column is the one we are sorting by and the sort is in descending order. By default this is an arrow pointing downwards rendered in Unicode.
handleSortwhich gives an action (a continuation) to perform when the user changes the sort order. By default it doesn’t do anyting. In that case, the table widget still knows how to re-sort the rows and it will do so.
The table widget requires some additional information to know how to render the table, which is provided in the form of a
Theme is a data type which is parameterized by
m (the monad where event handlers run) and
a (the table data type). The
Theme can be used to attach arbitrary properties to the
<td> elements generated by the table widget.
If you do not require any special properties on the aforementioned elements, then you can create a basic table widget using
Shpadoinkle.Widgets.Table.view, which will use a default theme that does not attach any properties to the aforementioned elements beyond those required to make the table widget function with its default behavior. This function expects an
a (the table data) and a sort order (a
SortCol a) and it gives you a view with the view model type
Html m (a, SortCol a).
If you do need to provide a non-default
Theme, then you can create a basic table widget using
Shpadoinkle.Widgets.Table.viewWith, which expects (in addition to an
a and a
SortCol a), a
Theme m a.
To create a lazy table, you will need to import both
Shpadoinkle.Widgets.Table.Lazy. The latter is a special application of the former.
The lazy table module introduces a subclass of
LazyTabular has a method,
countRows, which tells the lazy table widget how to know how many rows are in a table. For a basic table widget, we can count the rows by doing
length . toRows, but this does not necessarily work for a lazy table widget, because some of the rows might be on the backend (not yet fetched to the client side). It also may be beneficial to avoid computing
length . toRows on a lazily populated table where all the rows are in memory, because taking the length of a long list can be an expensive operation. Thus it is up to the user of the lazy table widget to say how to determine the number of rows.
The number of rows is used to set the size of the scroll bar. There may be a very empty row at the end of the table if the number of rows rendered in the DOM is less than the number output by
countRows. This is done in order to give the user an accurate general idea of how much data is in the table, so they know that if they scroll down to a certain depth, they may see data, and they can do so by dragging the scroll bar.
The lazy table widget wraps the table view model type
a in a data type
LazyTable a, in order to hold state which it needs in order to function. This
LazyTable data type does not escape into end user code. The
LazyTable data type can be regenerated given certain information passed to the lazy table widget along with certain information which must be stored on the end user’s view model. What information this is exactly depends on what kind of lazy table we are talking about.
A lazily populated table (not a lazy loading table) is constructed using the
lazyTable function, which outputs a view of type
Html m (b, CurrentScrollY). We will get back to
b momentarily. The additional information which the lazily populated table needs to put on the user’s view model is
CurrentScrollY, which tracks the number of pixels the user has scrolled down in the table.
A lazily loaded table (which may also be a lazily populated table) is constructed using the
lazyLoadingTable function, which outputs a view of type
Html m (b, CurrentScrollY, RowsLoaded). The additional information which the lazily loaded table needs to put on the user’s view model in addition to
RowsLoaded, which tracks the number of rows that have been loaded so far.
Both varieties of lazy table widget need the following data in addition to the data required by the basic table widget:
AssumedTableHeight, which is the number of pixels tall the whole table is. This is either the height of the table body, or the height of the container, depending on the setting of
AssumedRowHeight, which is the number of pixels tall a row of data is. This is assumed to be the same for all rows, so that the trick of extending the scroll bar can work.
LazyTableScrollConfig. This tells the widget whether you want the container of the table to scroll, or the body of the table. If you let the container scroll, then the table headers will scroll out of view. If you let the body scroll, the the table headers will not scroll out of view. In addition, the
LazyTableScrollConfigsupplies the table widget with a
Debouncelets the table widget debounce the scroll events at an approriate tempo (which you can find for your app using testing). Debouncing the scroll events is necessary in order to avoid performance problems when scrolling the table.
The container. For a lazily populated table, it is of type
Html m ((a, SortCol a), CurrentScrollY) → Html m ((b, SortCol a), CurrentScrollY). For a lazy loading table, it is of type
Html m ((b, SortCol a), CurrentScrollY, RowsLoaded). The container wraps around the
<table>element. If you do not need a container, then you can set it to
id. It is provided so that it can be the thing which scrolls in the lazy table interaction. This is where
bcomes in the type of the resulting view. The container is allowed to change the type of the view model, though it is not allowed to remove the data which the basic table widget and the lazy table widget require.
CurrentScrollYmust also be supplied.
For a lazy loading table, the following additional data must be supplied:
Paginatortells the table widget how to load more data.
Paginator a is a
forall m. ( Applicative m, Effect a m ) ⇒ a → SortCol a → Page → m a.
Page is a data type representing a page of data, consisting of an
Offset (a zero-based index into the whole list of rows, after applying the filters and sort order), and a
Length (a number of rows). Given the current table view model
a, and the sort order and a page, the
Paginator must return a new table view model which includes the data in the given
Page (as well as any data that is already there which is outside of the given
The lazy loading table widget handles knowing which pages of data to load when as the end user scrolls, without reloading data that is already there. When the end user changes the sort order, the widget knows to fetch a page of data which has an offset of zero and goes as far as the end user has scrolled. It is up to the user of the widget to load new data when the end user changes the filters, because the controls for any filters are not part of the table widget.