Commit b6c247616fc516497aa8d2300aeea104107f56fc

Authored by Georg Hopp
1 parent 6fc16d0b

Add mogwai list example

1 1 [workspace]
2   -members = [ "server", "ui" ]
  2 +members = [ "server", "ui", "mogwai-list" ]
3 3
4 4 [profile.release]
5 5 lto = true
... ...
  1 +[package]
  2 +name = "mogwai-list"
  3 +version = "0.0.0"
  4 +authors = ["Georg Hopp <georg@steffers.org>"]
  5 +workspace = ".."
  6 +edition = "2018"
  7 +
  8 +[lib]
  9 +crate-type = ["cdylib", "rlib"]
  10 +
  11 +[features]
  12 +default = ["console_error_panic_hook"]
  13 +
  14 +[dependencies]
  15 +artshop-common = { path = "../common" }
  16 +katex = { version = "0.4", default-features = false, features = ["wasm-js"] }
  17 +pulldown-cmark = "0.9"
  18 +console_log = "^0.1"
  19 +log = "^0.4"
  20 +serde = { version = "^1.0", features = ["derive"] }
  21 +serde_json = "^1.0"
  22 +wasm-bindgen = "^0.2"
  23 +wasm-bindgen-futures = "^0.4"
  24 +
  25 +# The `console_error_panic_hook` crate provides better debugging of panics by
  26 +# logging them with `console.error`. This is great for development, but requires
  27 +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
  28 +# code size when deploying.
  29 +console_error_panic_hook = { version = "0.1.6", optional = true }
  30 +wee_alloc = { version = "0.4.2", optional = true }
  31 +js-sys = "^0.3"
  32 +
  33 +[dependencies.mogwai]
  34 +version = "^0.5"
  35 +
  36 +[dependencies.web-sys]
  37 +version = "^0.3"
  38 +features = [
  39 + "Document",
  40 + "DomParser",
  41 + "Headers",
  42 + "HtmlElement",
  43 + "HtmlInputElement",
  44 + "MouseEvent",
  45 + "Node",
  46 + "Request",
  47 + "RequestInit",
  48 + "RequestMode",
  49 + "Response",
  50 + "SupportedType",
  51 + "Window",
  52 +]
  53 +
  54 +[dev-dependencies]
  55 +wasm-bindgen-test = "0.2"
... ...
  1 +#![allow(unused_braces)]
  2 +use log::Level;
  3 +use mogwai::{futures, prelude::*};
  4 +use std::panic;
  5 +use wasm_bindgen::prelude::*;
  6 +
  7 +/// An item widget.
  8 +/// Keeps track of clicks.
  9 +#[derive(Clone, Debug)]
  10 +struct Item {
  11 + id: usize,
  12 + clicks: Model<u32>,
  13 +}
  14 +
  15 +/// An item's update messages.
  16 +#[derive(Clone)]
  17 +enum ItemMsg {
  18 + /// The user clicked
  19 + Click,
  20 + /// The user requested this item be removed
  21 + Remove,
  22 +}
  23 +
  24 +/// One item's logic loop.
  25 +async fn item_logic(
  26 + id: usize,
  27 + clicks: Model<u32>,
  28 + mut from_view: broadcast::Receiver<ItemMsg>,
  29 + to_list: broadcast::Sender<ListMsg>,
  30 +) {
  31 + loop {
  32 + match from_view.recv().await {
  33 + Ok(ItemMsg::Click) => {
  34 + clicks.visit_mut(|c| *c += 1).await;
  35 + }
  36 + Ok(ItemMsg::Remove) => {
  37 + to_list.broadcast(ListMsg::RemoveItem(id)).await.unwrap();
  38 + break;
  39 + }
  40 + Err(_) => break,
  41 + }
  42 + }
  43 + log::info!("item {} logic loop is done", id);
  44 +}
  45 +
  46 +// ANCHOR: item_view
  47 +fn item_view(
  48 + clicks: impl Stream<Item = u32> + Sendable,
  49 + to_logic: broadcast::Sender<ItemMsg>,
  50 +) -> ViewBuilder<Dom> {
  51 + builder! {
  52 + <li>
  53 + <button
  54 + style:cursor="pointer"
  55 + on:click=to_logic.sink().contra_map(|_| ItemMsg::Click)>
  56 + "Increment"
  57 + </button>
  58 + <button
  59 + style:cursor="pointer"
  60 + on:click=to_logic.sink().contra_map(|_| ItemMsg::Remove)>
  61 + "Remove"
  62 + </button>
  63 + " "
  64 + <span>
  65 + {
  66 + ("", clicks.map(|clicks| match clicks {
  67 + 1 => "1 click".to_string(),
  68 + n => format!("{} clicks", n),
  69 + }))
  70 + }
  71 + </span>
  72 + </li>
  73 + }
  74 +}
  75 +// ANCHOR_END: item_view
  76 +
  77 +/// Create a new item component.
  78 +fn item(id: usize, clicks: Model<u32>, to_list: broadcast::Sender<ListMsg>) -> Component<Dom> {
  79 + let (tx, rx) = broadcast::bounded(1);
  80 + Component::from(item_view(clicks.stream(), tx)).with_logic(item_logic(id, clicks, rx, to_list))
  81 +}
  82 +
  83 +#[derive(Clone)]
  84 +enum ListMsg {
  85 + /// Create a new item
  86 + NewItem,
  87 + /// Remove the item with the given id
  88 + RemoveItem(usize),
  89 +}
  90 +
  91 +// ANCHOR: list_logic_coms
  92 +/// Launch the logic loop of our list of items.
  93 +async fn list_logic(
  94 + input: broadcast::Receiver<ListMsg>,
  95 + tx_patch_children: mpmc::Sender<ListPatch<ViewBuilder<Dom>>>,
  96 +) {
  97 + // Set up our communication from items to this logic loop by
  98 + // * creating a list patch model
  99 + // * creating a channel to go from item to list logic (aka here)
  100 + // * creating a side-effect stream (for_each) that runs for each item patch
  101 + // * map patches of Item to patches of builders and send that to our view
  102 + // through tx_patch_children
  103 + let mut items: ListPatchModel<Item> = ListPatchModel::new();
  104 + let (to_list, from_items) = broadcast::bounded::<ListMsg>(1);
  105 + let to_list = to_list.clone();
  106 + let all_item_patches = items.stream().map(move |patch| {
  107 + log::info!("mapping patch for item: {:?}", patch);
  108 + let to_list = to_list.clone();
  109 + patch.map(move |Item { id, clicks }: Item| {
  110 + let to_list = to_list.clone();
  111 + let component = item(id, clicks, to_list);
  112 + let builder: ViewBuilder<Dom> = component.into();
  113 + builder
  114 + })
  115 + }).for_each(move |patch| {
  116 + let tx_patch_children = tx_patch_children.clone();
  117 + async move {
  118 + tx_patch_children.send(patch).await.unwrap();
  119 + }
  120 + });
  121 + mogwai::spawn(all_item_patches);
  122 + // ANCHOR_END: list_logic_coms
  123 + // ANCHOR: list_logic_loop
  124 + // Combine the input from our view with the input from our items
  125 + let mut input = futures::stream::select_all(vec![input, from_items]);
  126 + let mut next_id = 0;
  127 + loop {
  128 + match input.next().await {
  129 + Some(ListMsg::NewItem) => {
  130 + log::info!("creating a new item");
  131 + let item: Item = Item {
  132 + id: next_id,
  133 + clicks: Model::new(0),
  134 + };
  135 + next_id += 1;
  136 + // patch our items easily and _item_patch_stream's for_each runs automatically,
  137 + // keeping the list of item views in sync
  138 + items.list_patch_push(item);
  139 + }
  140 + Some(ListMsg::RemoveItem(id)) => {
  141 + log::info!("removing item: {}", id);
  142 + let mut may_index = None;
  143 + 'find_item_by_id: for (item, index) in items.read().await.iter().zip(0..) {
  144 + if item.id == id {
  145 + may_index = Some(index);
  146 + break 'find_item_by_id;
  147 + }
  148 + }
  149 +
  150 + if let Some(index) = may_index {
  151 + // patch our items to remove the item at the index
  152 + let _ = items.list_patch_remove(index);
  153 + }
  154 + }
  155 + _ => {
  156 + log::error!("Leaving list logic loop - this shouldn't happen");
  157 + break;
  158 + },
  159 + }
  160 + }
  161 + // ANCHOR_END: list_logic_loop
  162 +}
  163 +
  164 +// ANCHOR: list_view
  165 +fn list_view<T>(to_logic: broadcast::Sender<ListMsg>, children: T) -> ViewBuilder<Dom>
  166 +where
  167 + T: Stream<Item = ListPatch<ViewBuilder<Dom>>> + Sendable,
  168 +{
  169 + builder! {
  170 + <fieldset>
  171 + <legend>"A List of Gizmos"</legend>
  172 + <button style:cursor="pointer" on:click=to_logic.sink().contra_map(|_| ListMsg::NewItem)>
  173 + "Create a new item"
  174 + </button>
  175 + <fieldset>
  176 + <legend>"Items"</legend>
  177 + <ol patch:children=children>
  178 + </ol>
  179 + </fieldset>
  180 + </fieldset>
  181 + }
  182 +}
  183 +// ANCHOR_END: list_view
  184 +
  185 +/// Create our list component.
  186 +fn list() -> Component<Dom> {
  187 + let (logic_tx, logic_rx) = broadcast::bounded(1);
  188 + let (item_patch_tx, item_patch_rx) = mpmc::bounded(1);
  189 + Component::from(list_view(logic_tx, item_patch_rx))
  190 + .with_logic(list_logic(logic_rx, item_patch_tx))
  191 +}
  192 +
  193 +#[wasm_bindgen]
  194 +pub fn main(parent_id: Option<String>) -> Result<(), JsValue> {
  195 + panic::set_hook(Box::new(console_error_panic_hook::hook));
  196 + console_log::init_with_level(Level::Trace).unwrap();
  197 + let component = list();
  198 + let view = component.build().unwrap();
  199 +
  200 + if let Some(id) = parent_id {
  201 + let parent = mogwai::utils::document().get_element_by_id(&id).unwrap();
  202 + view.run_in_container(&parent)
  203 + } else {
  204 + view.run()
  205 + }
  206 +}
... ...
Please register or login to post a comment