pub struct ServerTable<S, T> { /* private fields */ }Expand description
A table whose rows the server filters, sorts, and pages, one page at a time.
The list signal holds only the rows shown. The filter field, a sortable
header, and the page buttons write the TableState and then run
load, which is a call to an action that receives the state. The
action answers with a patch of the rows and, for a paged table, of the
total. The browser counts no rows, so the browser’s work per change
grows with the rows of one page, however many the server holds.
ServerTable::sortable_columntakes a key the server reads back withTableState::sorting, so both sides name the column with the same enum.ServerTable::filterrunsloadonce typing pauses for 300 ms, and returns to the first page.ServerTable::paginateshows the range of rows and moves between pages, out of the total that the server writes.
What the server does with the state is the application’s code, which the compiler does not check against the table: it answers with the rows of the page the state names, and a total that counts every row that passes the filter. A page past the last row shows no rows until the visitor goes back.
use hypersig::{Action, Page, Patches, Segment, Signals};
use hypersig_ui::{Column, ServerTable, SortOrder, TableState};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize, Clone)]
struct User {
id: i32,
email: String,
}
#[derive(Signals, Serialize, Deserialize)]
struct Users {
users: Vec<User>,
total: i32,
table: TableState,
}
#[derive(Segment, Serialize, Deserialize)]
enum By {
Email,
}
const LOAD: Action<Users, TableState> = Action::post("/users");
/// The page of users `state` names, and how many pass its filter.
fn query(all: &[User], state: &TableState) -> (Vec<User>, i32) {
let mut rows: Vec<User> = all
.iter()
.filter(|user| user.email.contains(&state.query.to_lowercase()))
.cloned()
.collect();
if let Some((By::Email, order)) = state.sorting::<By>() {
rows.sort_by(|a, b| a.email.cmp(&b.email));
if order == SortOrder::Descending {
rows.reverse();
}
}
let total = i32::try_from(rows.len()).unwrap_or(i32::MAX);
let start = usize::try_from(state.page * 10).unwrap_or(0);
(rows.into_iter().skip(start).take(10).collect(), total)
}
async fn load((): (), state: TableState) -> Patches<Users> {
let all = vec![User { id: 1, email: "ada@example.com".into() }];
let (rows, total) = query(&all, &state);
Users::users().patch(rows).and(Users::total().patch(total)).into()
}
let table = ServerTable::new(
"Users",
Users::table(),
Users::users(),
|user| user.at(User::id()),
LOAD.call_with(Users::table()),
)
.filter("Filter emails")
.paginate(10, Users::total())
.sortable_column(Column::text("Email", User::email()), &By::Email);
let all = vec![User { id: 1, email: "ada@example.com".into() }];
let (users, total) = query(&all, &TableState::default());
let html = Page::new("Users", Users { users, total, table: TableState::default() })
.child(table)
.render();
assert!(html.contains("1–1 / 1"));Implementations§
Source§impl<S: 'static, T: 'static> ServerTable<S, T>
impl<S: 'static, T: 'static> ServerTable<S, T>
Sourcepub fn new<K: 'static, E: IntoExpr<S, K>>(
label: impl Into<String>,
state: Sig<S, TableState>,
rows: Sig<S, Vec<T>>,
key: impl FnOnce(Item<S, T>) -> E + 'static,
load: impl IntoStmt<S>,
) -> Self
pub fn new<K: 'static, E: IntoExpr<S, K>>( label: impl Into<String>, state: Sig<S, TableState>, rows: Sig<S, Vec<T>>, key: impl FnOnce(Item<S, T>) -> E + 'static, load: impl IntoStmt<S>, ) -> Self
An empty table named label, listing rows by key, which runs load to fetch rows.
state holds the filter text, the page, and the sort, and load
runs after the visitor changes one of them.
Sourcepub fn sortable_column<K: Segment>(self, column: Column<S, T>, key: &K) -> Self
pub fn sortable_column<K: Segment>(self, column: Column<S, T>, key: &K) -> Self
Adds column, whose header asks the server to sort by key, after the columns added so far.
The header writes key into the TableState, returns to the
first page, and runs load. The server reads the key back with
TableState::sorting. An enum with #[derive(Segment)] keeps the
keys of the columns and the server’s match in one type.
Sourcepub fn selectable<K: PartialEq + 'static, E: IntoExpr<S, K>>(
self,
selected: Sig<S, Vec<K>>,
key: impl Fn(Item<S, T>) -> E + 'static,
) -> Self
pub fn selectable<K: PartialEq + 'static, E: IntoExpr<S, K>>( self, selected: Sig<S, Vec<K>>, key: impl Fn(Item<S, T>) -> E + 'static, ) -> Self
Adds a first column of checkboxes that keep the key of each selected row in selected.
The keys live in a signal of the page, so a row selected on one page stays selected while the visitor moves to another, and the server receives the keys as an argument of an action. The header checkbox selects or clears the rows of the page shown.
Sourcepub fn expandable<K: PartialEq + 'static, E: IntoExpr<S, K>>(
self,
expanded: Sig<S, Vec<K>>,
key: impl Fn(Item<S, T>) -> E + 'static,
details: impl Fn(Item<S, T>) -> El<S> + 'static,
) -> Self
pub fn expandable<K: PartialEq + 'static, E: IntoExpr<S, K>>( self, expanded: Sig<S, Vec<K>>, key: impl Fn(Item<S, T>) -> E + 'static, details: impl Fn(Item<S, T>) -> El<S> + 'static, ) -> Self
Adds a first column of buttons that show details of a row under it, keeping the key of each open row in expanded.
The button carries aria-expanded, as the WAI-ARIA disclosure
pattern asks, and the details fill a row of their own under the row,
across every column. Each row and its details are a <tbody> of
their own, so the two move together when the rows are sorted.
Sourcepub fn filter(self, label: impl Into<String>) -> Self
pub fn filter(self, label: impl Into<String>) -> Self
Adds a field named label whose text the server filters the rows by.
The field sits above the table. Once typing pauses for 300 ms, the
table returns to its first page and runs load.
Sourcepub fn paginate(self, size: i32, total: Sig<S, i32>) -> Self
pub fn paginate(self, size: i32, total: Sig<S, i32>) -> Self
Shows pages of size rows out of total, with buttons for the previous and next page.
The server answers each load with at most size rows and writes
total, the number of rows that pass the filter. Below the table,
the range of rows shown is written as 11–20 / 42.
§Panics
Panics if size is less than 1.
Sourcepub fn text(self, text: TableText) -> Self
pub fn text(self, text: TableText) -> Self
Replaces the words the table shows and names its controls with, English until set.
Sourcepub fn max_height(self, height: Length) -> Self
pub fn max_height(self, height: Length) -> Self
Keeps the table at most height high, scrolling its rows under a header that stays in place.