Skip to main content

ServerTable

Struct ServerTable 

Source
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.

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>

Source

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.

Source

pub fn column(self, column: Column<S, T>) -> Self

Adds column after the columns added so far.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn text(self, text: TableText) -> Self

Replaces the words the table shows and names its controls with, English until set.

Source

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.

Source

pub fn render(self) -> El<S>

The filter field, the table, and the page buttons, in a <div>.

Trait Implementations§

Source§

impl<S, T> Debug for ServerTable<S, T>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<S: 'static, T: 'static> From<ServerTable<S, T>> for El<S>

Source§

fn from(table: ServerTable<S, T>) -> Self

Converts to this type from the input type.
Source§

impl<S: 'static, T: 'static> From<ServerTable<S, T>> for Node<S>

Source§

fn from(table: ServerTable<S, T>) -> Self

Converts to this type from the input type.

Auto Trait Implementations§

§

impl<S, T> !RefUnwindSafe for ServerTable<S, T>

§

impl<S, T> !Send for ServerTable<S, T>

§

impl<S, T> !Sync for ServerTable<S, T>

§

impl<S, T> !UnwindSafe for ServerTable<S, T>

§

impl<S, T> Freeze for ServerTable<S, T>

§

impl<S, T> Unpin for ServerTable<S, T>

§

impl<S, T> UnsafeUnpin for ServerTable<S, T>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.