Showing
3 changed files
with
262 additions
and
1 deletions
mogwai-list/Cargo.toml
0 → 100644
| 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" |
mogwai-list/src/lib.rs
0 → 100644
| 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