#[hemx::surface] pub mod ui {} #[cfg(feature = "client")] #[derive(Clone, Debug, Eq, PartialEq)] struct CardId(String); #[cfg(feature = "client")] struct ReorderCommand { card: CardId, input_kind: String, } #[cfg(feature = "client")] struct CardReordered { card: CardId, input_kind: String, } #[cfg(feature = "client")] struct BoardProjection { first: CardId, } #[cfg(feature = "client")] struct ProjectedReorder { card: CardId, before: Option, input_kind: String, } #[cfg(feature = "client")] impl ReorderCommand { fn from_client(event: hemx::wasm::ClientEvent) -> Self { Self { card: CardId( event .value .filter(|card| !card.is_empty()) .unwrap_or_else(|| "1".into()), ), input_kind: event.kind, } } fn decide(self) -> CardReordered { CardReordered { card: self.card, input_kind: self.input_kind, } } } #[cfg(feature = "client")] impl BoardProjection { fn restore(state: hemx::wasm::ClientState) -> Self { Self { first: CardId(state.encoded.split('|').next().unwrap_or("1").to_owned()), } } fn apply(self, event: CardReordered) -> ProjectedReorder { let before = (event.card != self.first).then_some(self.first); ProjectedReorder { card: event.card, before, input_kind: event.input_kind, } } } #[cfg(feature = "client")] #[hemx::handler(client)] pub fn reorder_card( event: hemx::wasm::ClientEvent, state: hemx::wasm::ClientState, ) -> impl hemx::IntoEffect { let projected = BoardProjection::restore(state).apply(ReorderCommand::from_client(event).decide()); let card = projected.card.0; let move_effect = match projected.before { Some(before) => ui::client_board::client_cards.move_before(card.clone(), before.0), None => ui::client_board::client_cards.move_to_end(card.clone()), }; vec![ move_effect, ui::client_board::client_notice.text(format!("Moved {card} with {}", projected.input_kind)), ] } #[cfg(all(feature = "fixture", not(target_arch = "wasm32")))] mod fixture { use super::ui; use hemplate::Hemplate; use hemx::Html; #[derive(Hemplate)] struct ClientBoard { cards: Vec, } #[derive(Hemplate)] struct ClientCard { id: u64, title: &'static str, } pub fn render() -> Html { ui::client_board::page(&ClientBoard { cards: vec![ ClientCard { id: 1, title: "First", }, ClientCard { id: 2, title: "Second", }, ], }) } } #[cfg(all(feature = "fixture", not(target_arch = "wasm32")))] pub fn render_client_fixture() -> hemx::Html { fixture::render() } #[cfg(test)] mod tests { use super::ui::{board, board_card}; use hemplate::Hemplate; use hemx::IntoEffect; use hemx_test::inspect; use scraper::{Html, Selector}; #[derive(Hemplate)] #[hemplate = "partials"] struct BoardColumns { columns: Vec, } #[allow(dead_code)] #[derive(Clone, Debug)] #[hemx::form("create_card")] struct CreateCard { title: String, column: String, } // req: examples/001 req: codegen/002 req: list/003 #[test] fn kanban_board_updates_generated_slot() { fn render_board() -> impl IntoEffect { board::board.put(&empty_board()) } let effect = inspect(render_board()); assert!(effect.updates_html(board::board)); } // req: html_safety/002 req: view/001 req: test/005 #[test] fn kanban_board_test_payload_is_rendered_by_a_hemplate_view() { let html = super::ui::page(&empty_board()); let document = Html::parse_fragment(html.as_str()); assert_eq!(document.select(&selector(".columns")).count(), 1); } fn empty_board() -> BoardColumns { // req: html_safety/002 req: view/001 BoardColumns { columns: Vec::new(), } } fn selector(value: &str) -> Selector { Selector::parse(value).expect("test selector parses") } // req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003 #[test] fn kanban_form_handler_is_checked_against_hemplate_form() { #[hemx::handler] fn create_card(_form: hemx::Form) -> impl IntoEffect { board::notice.text("queued") } let effect = inspect(create_card(CreateCard::FORM)); assert!(effect.updates_text(board::notice)); } // req: examples/001 req: form/002 req: codegen/003 #[test] fn kanban_template_exports_form_and_card_handles() { assert_ne!(board::create_card.id(), board_card::move_right.id()); assert_eq!( board::create_card_form.field("title").resource, board::create_card_form.id() ); } }