todomvcsingle.rs 5.7 KB

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