todomvcsingle.rs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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(RecoilContext);
  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. TODO_LIST.with(api).get()
  55. self.0.modify(&TODO_LIST, move |list| {
  56. *list = list.drain().filter(|(_, item)| !item.checked).collect();
  57. })
  58. }
  59. fn set_filter(&self, filter: &FilterState) {
  60. self.0.modify(&FILTER, move |f| *f = *filter);
  61. }
  62. }
  63. #[derive(PartialEq, Clone, Copy)]
  64. pub enum FilterState {
  65. All,
  66. Active,
  67. Completed,
  68. }
  69. #[derive(Debug, PartialEq, Clone)]
  70. pub struct TodoItem {
  71. pub id: Uuid,
  72. pub checked: bool,
  73. pub contents: String,
  74. }
  75. fn App(ctx: Context, props: &()) -> DomTree {
  76. init_recoil_root(ctx);
  77. rsx! { in ctx,
  78. div { id: "app", style { "{APP_STYLE}" }
  79. TodoList {}
  80. Footer {}
  81. }
  82. }
  83. }
  84. pub fn TodoList(ctx: Context, props: &()) -> DomTree {
  85. let draft = use_state_new(&ctx, || "".to_string());
  86. let todos = use_atom(&ctx, &TODO_LIST);
  87. let filter = use_atom(&ctx, &FILTER);
  88. let todolist = todos
  89. .values()
  90. .filter(|item| match filter {
  91. FilterState::All => true,
  92. FilterState::Active => !item.checked,
  93. FilterState::Completed => item.checked,
  94. })
  95. .map(|item| {
  96. rsx!(TodoEntry {
  97. key: "{order}",
  98. id: item.id,
  99. })
  100. });
  101. rsx! { in ctx,
  102. div {
  103. header {
  104. class: "header"
  105. h1 {"todos"}
  106. input {
  107. class: "new-todo"
  108. placeholder: "What needs to be done?"
  109. value: "{draft}"
  110. oninput: move |evt| draft.set(evt.value)
  111. }
  112. }
  113. {todolist}
  114. // rsx! accepts optionals, so we suggest the `then` method in place of ternary
  115. {(!todos.is_empty()).then(|| rsx!( FilterToggles {}) )}
  116. }
  117. }
  118. }
  119. #[derive(PartialEq, Props)]
  120. pub struct TodoEntryProps {
  121. id: Uuid,
  122. }
  123. pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree {
  124. let (is_editing, set_is_editing) = use_state(&ctx, || false);
  125. let todo = use_atom(&ctx, &TODO_LIST).get(&props.id).unwrap();
  126. // let todo = use_atom_family(&ctx, &TODOS, props.id);
  127. ctx.render(rsx! (
  128. li {
  129. "{todo.id}"
  130. input {
  131. class: "toggle"
  132. type: "checkbox"
  133. "{todo.checked}"
  134. }
  135. {is_editing.then(|| rsx!(
  136. input {
  137. value: "{todo.contents}"
  138. }
  139. ))}
  140. }
  141. ))
  142. }
  143. pub fn FilterToggles(ctx: Context, props: &()) -> DomTree {
  144. let reducer = use_recoil_context::<TodoManager>(ctx);
  145. let items_left = use_selector(ctx, &TODOS_LEFT);
  146. let toggles = [
  147. ("All", "", FilterState::All),
  148. ("Active", "active", FilterState::Active),
  149. ("Completed", "completed", FilterState::Completed),
  150. ]
  151. .iter()
  152. .map(|(name, path, filter)| {
  153. rsx!(
  154. li {
  155. class: "{name}"
  156. a {
  157. href: "{path}",
  158. onclick: move |_| reducer.set_filter(&filter),
  159. "{name}"
  160. }
  161. }
  162. )
  163. });
  164. let item_text = match items_left {
  165. 1 => "item",
  166. _ => "items",
  167. };
  168. rsx! { in ctx,
  169. footer {
  170. span {
  171. strong {"{items_left}"}
  172. span {"{item_text} left"}
  173. }
  174. ul {
  175. class: "filters"
  176. {toggles}
  177. }
  178. }
  179. }
  180. }
  181. pub fn Footer(ctx: Context, props: &()) -> DomTree {
  182. rsx! { in ctx,
  183. footer {
  184. class: "info"
  185. p {"Double-click to edit a todo"}
  186. p {
  187. "Created by "
  188. a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }
  189. }
  190. p {
  191. "Part of "
  192. a { "TodoMVC", href: "http://todomvc.com" }
  193. }
  194. }
  195. }
  196. }