1
0

todomvcsingle.rs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. //! Example: TODOVMC - One file
  2. //! ---------------------------
  3. //! This example shows how to build a one-file TODO MVC app with Dioxus and Recoil.
  4. //! This project is confined to a single file to showcase the suggested patterns
  5. //! for building a small but mighty UI with Dioxus without worrying about project structure.
  6. //!
  7. //! If you want an example on recommended project structure, check out the TodoMVC folder
  8. //!
  9. //! Here, we show to use Dioxus' Recoil state management solution to simplify app logic
  10. #![allow(non_snake_case)]
  11. use dioxus_web::dioxus::prelude::*;
  12. use recoil::*;
  13. use std::collections::HashMap;
  14. use uuid::Uuid;
  15. const APP_STYLE: &'static str = include_str!("./todomvc/style.css");
  16. fn main() {
  17. wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App));
  18. }
  19. // Declare our global app state
  20. const TODO_LIST: Atom<HashMap<Uuid, TodoItem>> = atom(|_| Default::default());
  21. const FILTER: Atom<FilterState> = atom(|_| FilterState::All);
  22. const TODOS_LEFT: selector<usize> = selector(|api| api.get(&TODO_LIST).len());
  23. // Implement a simple abstraction over sets/gets of multiple atoms
  24. struct TodoManager(RecoilApi);
  25. impl TodoManager {
  26. fn add_todo(&self, contents: String) {
  27. let item = TodoItem {
  28. checked: false,
  29. contents,
  30. id: Uuid::new_v4(),
  31. };
  32. self.0.modify(&TODO_LIST, move |list| {
  33. list.insert(item.id, item);
  34. });
  35. }
  36. fn remove_todo(&self, id: &Uuid) {
  37. self.0.modify(&TODO_LIST, move |list| {
  38. list.remove(id);
  39. })
  40. }
  41. fn select_all_todos(&self) {
  42. self.0.modify(&TODO_LIST, move |list| {
  43. for item in list.values_mut() {
  44. item.checked = true;
  45. }
  46. })
  47. }
  48. fn toggle_todo(&self, id: &Uuid) {
  49. self.0.modify(&TODO_LIST, move |list| {
  50. list.get_mut(id).map(|item| item.checked = !item.checked)
  51. });
  52. }
  53. fn clear_completed(&self) {
  54. self.0.modify(&TODO_LIST, move |list| {
  55. *list = list.drain().filter(|(_, item)| !item.checked).collect();
  56. })
  57. }
  58. fn set_filter(&self, filter: &FilterState) {
  59. self.0.modify(&FILTER, move |f| *f = *filter);
  60. }
  61. }
  62. #[derive(PartialEq, Clone, Copy)]
  63. pub enum FilterState {
  64. All,
  65. Active,
  66. Completed,
  67. }
  68. #[derive(Debug, PartialEq, Clone)]
  69. pub struct TodoItem {
  70. pub id: Uuid,
  71. pub checked: bool,
  72. pub contents: String,
  73. }
  74. fn App(ctx: Context, props: &()) -> DomTree {
  75. use_init_recoil_root(ctx);
  76. rsx! { in ctx,
  77. div { id: "app", style { "{APP_STYLE}" }
  78. TodoList {}
  79. Footer {}
  80. }
  81. }
  82. }
  83. pub fn TodoList(ctx: Context, props: &()) -> DomTree {
  84. let draft = use_state_new(&ctx, || "".to_string());
  85. let todos = use_recoil_value(&ctx, &TODO_LIST);
  86. let filter = use_recoil_value(&ctx, &FILTER);
  87. let todolist = todos
  88. .values()
  89. .filter(|item| match filter {
  90. FilterState::All => true,
  91. FilterState::Active => !item.checked,
  92. FilterState::Completed => item.checked,
  93. })
  94. .map(|item| {
  95. rsx!(TodoEntry {
  96. key: "{order}",
  97. id: item.id,
  98. })
  99. });
  100. rsx! { in ctx,
  101. div {
  102. header {
  103. class: "header"
  104. h1 {"todos"}
  105. input {
  106. class: "new-todo"
  107. placeholder: "What needs to be done?"
  108. value: "{draft}"
  109. oninput: move |evt| draft.set(evt.value)
  110. }
  111. }
  112. {todolist}
  113. // rsx! accepts optionals, so we suggest the `then` method in place of ternary
  114. {(!todos.is_empty()).then(|| rsx!( FilterToggles {}) )}
  115. }
  116. }
  117. }
  118. #[derive(PartialEq, Props)]
  119. pub struct TodoEntryProps {
  120. id: Uuid,
  121. }
  122. pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree {
  123. let (is_editing, set_is_editing) = use_state(&ctx, || false);
  124. let todo = use_recoil_value(&ctx, &TODO_LIST).get(&props.id).unwrap();
  125. ctx.render(rsx! (
  126. li {
  127. "{todo.id}"
  128. input {
  129. class: "toggle"
  130. type: "checkbox"
  131. "{todo.checked}"
  132. }
  133. {is_editing.then(|| rsx!(
  134. input {
  135. value: "{todo.contents}"
  136. }
  137. ))}
  138. }
  139. ))
  140. }
  141. pub fn FilterToggles(ctx: Context, props: &()) -> DomTree {
  142. let reducer = use_recoil_context::<TodoManager>(ctx);
  143. let items_left = use_selector(ctx, &TODOS_LEFT);
  144. let toggles = [
  145. ("All", "", FilterState::All),
  146. ("Active", "active", FilterState::Active),
  147. ("Completed", "completed", FilterState::Completed),
  148. ]
  149. .iter()
  150. .map(|(name, path, filter)| {
  151. rsx!(
  152. li {
  153. class: "{name}"
  154. a {
  155. href: "{path}",
  156. onclick: move |_| reducer.set_filter(&filter),
  157. "{name}"
  158. }
  159. }
  160. )
  161. });
  162. let item_text = match items_left {
  163. 1 => "item",
  164. _ => "items",
  165. };
  166. rsx! { in ctx,
  167. footer {
  168. span {
  169. strong {"{items_left}"}
  170. span {"{item_text} left"}
  171. }
  172. ul {
  173. class: "filters"
  174. {toggles}
  175. }
  176. }
  177. }
  178. }
  179. pub fn Footer(ctx: Context, props: &()) -> DomTree {
  180. rsx! { in ctx,
  181. footer {
  182. class: "info"
  183. p {"Double-click to edit a todo"}
  184. p {
  185. "Created by "
  186. a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }
  187. }
  188. p {
  189. "Part of "
  190. a { "TodoMVC", href: "http://todomvc.com" }
  191. }
  192. }
  193. }
  194. }