English

リスト

Vec のフィールドはリストです。each はブラウザで項目ごとに要素を描画し、リストの文はリクエストを送らずにリストを変更します。

リストを描画する

each は、リストと、項目 1 つ分の要素を組み立てる関数を受け取ります。この関数はテンプレートを作るためにサーバーで 1 回だけ実行され、ブラウザがそのとき描画している項目を表す Item を受け取ります。項目のフィールドは item.at(field) で、位置は item.index() で、項目そのものは item.value() で読みます。

use hypersig::html::{each, each_keyed, li, span, ul};
use hypersig::Signals;
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Track {
    id: i32,
    title: String,
}

#[derive(Signals, Serialize, Deserialize)]
struct Playlist {
    tracks: Vec<Track>,
    tags: Vec<String>,
}

// Matched to their elements by position.
let tags = ul::<Playlist>().child(each(Playlist::tags(), |tag| li().bind_text(tag)));

// Matched by id, so an item keeps its element when the list is reordered.
let tracks = ul::<Playlist>().child(each_keyed(
    Playlist::tracks(),
    |track| track.at(Track::id()),
    |track| {
        li().child(span().bind_text((track.index() + 1).to_text()))
            .child(span().bind_text(track.at(Track::title())))
    },
));

each は位置で項目と要素を対応させます。each_keyed はキーで対応させるので、リストを並べ替えたり前の項目を削除したりしても項目は自分の要素を保ち、フォーカスや入力欄の状態も項目と一緒に移ります。

リストを変更する

use hypersig::html::{button, div, p};
use hypersig::{Expr, Signals};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize, Clone)]
struct Task {
    title: String,
    done: bool,
    priority: i32,
}

#[derive(Signals, Serialize, Deserialize)]
struct Board {
    tasks: Vec<Task>,
}

let tasks = Board::tasks();
let new_task = Task { title: "Untitled".into(), done: false, priority: 0 };

let toolbar = div::<Board>()
    .child(button().on_click(tasks.push(Expr::lit(&new_task))).text("Add"))
    .child(button().on_click(tasks.set_all(Task::done(), true)).text("Finish all"))
    .child(button().on_click(tasks.retain(|task| !task.at(Task::done()))).text("Clear done"))
    .child(button().on_click(tasks.sort_by(|task| task.at(Task::priority()))).text("Sort"))
    .child(p().bind_text(tasks.count(|task| task.at(Task::done())).to_text()))
    .child(p().show(tasks.is_empty()).text("Nothing to do."));
種類リストのシグナルのメソッド
文push、append、extend、remove_at、clear、retain、swap、sort_by、set_all、update_where
式len、is_empty、nth、map、count、sum_of、max_of、min_of
1 つの項目にitem.remove()、item.at(field) の set

項目のシグナルはブラウザの中だけにある

項目のフィールドは Sig<S, T, Local> で、そのパスはブラウザで項目ごとに解決されます。patch メソッドがないので、サーバーは位置を指定して 1 つの項目のフィールドを書き換えられません。サーバーはリストのフィールドの差分でリスト全体を置き換えるか、ページの一部をフラグメントとして描画し直します。これは サーバーとのやり取り で説明します。

ToDo リスト

このデモは Expr::lit(&blank).with(...) で新しい項目を作ります。この式は定数の構造体を複製し、2 つのフィールドを式で置き換えます。未完了の項目の数はリストを使った式です。

実行結果

todos.rs
use hypersig::html::{div, each_keyed, form, input, label, li, p, span, ul};
use hypersig::style::Style;
use hypersig::style::pattern::{hstack, stack};
use hypersig::style::value::{AlignItems, Display, JustifyContent, rem};
use hypersig::{Expr, Page, Signals, Stmt};
use hypersig_ui::{ButtonSize, ButtonVariant, TextField, button, submit_button};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Todo {
    id: i32,
    title: String,
    done: bool,
}

#[derive(Signals, Serialize, Deserialize)]
pub struct Todos {
    draft: String,
    next_id: i32,
    items: Vec<Todo>,
}

pub fn page() -> Page<Todos> {
    let draft = Todos::draft();
    let next_id = Todos::next_id();
    let items = Todos::items();
    let blank = Todo {
        id: 0,
        title: String::new(),
        done: false,
    };
    let add = items
        .push(
            Expr::lit(&blank)
                .with(Todo::id(), next_id)
                .with(Todo::title(), draft.trim()),
        )
        .then(next_id.set(next_id + 1))
        .then(draft.set(""));

    let initial = Todos {
        draft: String::new(),
        next_id: 2,
        items: vec![
            Todo {
                id: 0,
                title: "ガイドを読む"
                    .into(),
                done: true,
            },
            Todo {
                id: 1,
                title: "ページを書く"
                    .into(),
                done: false,
            },
        ],
    };
    Page::new(
        "やること",
        initial,
    )
    .child(
        div()
            .css(stack(rem(1)).max_width(rem(24)))
            .child(
                form()
                    .on_submit(Stmt::when(!draft.trim().is_empty(), add))
                    .css(hstack(rem(0.5)).align_items(AlignItems::End))
                    .child(div().css(Style::new().flex_grow(1.0)).child(TextField::new(
                        "新しい項目",
                        draft,
                    )))
                    .child(
                        submit_button(ButtonVariant::Default, ButtonSize::Default).text("追加"),
                    ),
            )
            .child(ul().css(stack(rem(0.25))).child(each_keyed(
                items,
                |todo| todo.at(Todo::id()),
                |todo| {
                    li().css(hstack(rem(0.5)).justify_content(JustifyContent::SpaceBetween))
                        .child(
                            label()
                                .css(hstack(rem(0.5)))
                                .child(input().attr("type", "checkbox").bind(todo.at(Todo::done())))
                                .child(span().bind_text(todo.at(Todo::title()))),
                        )
                        .child(
                            button(ButtonVariant::Ghost, ButtonSize::Sm)
                                .on_click(todo.remove())
                                .text("削除"),
                        )
                },
            )))
            .child(
                div()
                    .css(
                        Style::new()
                            .display(Display::Flex)
                            .justify_content(JustifyContent::SpaceBetween)
                            .align_items(AlignItems::Center),
                    )
                    .child(
                        p().bind_text(items.count(|todo| !todo.at(Todo::done())).to_text().concat(
                            " 件が未完了",
                        )),
                    )
                    .child(
                        button(ButtonVariant::Outline, ButtonSize::Sm)
                            .on_click(items.retain(|todo| !todo.at(Todo::done())))
                            .text("完了した項目を消す"),
                    ),
            ),
    )
}