todomvcsingle.rs 5.6 KB

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