todomvcsingle.rs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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_core as dioxus;
  12. use dioxus_web::dioxus::prelude::*;
  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: AtomHashMap<Uuid, TodoItem> = |_| {};
  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(cx: Context<()>) -> VNode {
  71. let draft = use_state(cx, || "".to_string());
  72. let todos = use_read(&cx, &TODO_LIST);
  73. let filter = use_read(&cx, &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 cx,
  88. div {
  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(cx: Context, props: &TodoEntryProps) -> VNode {
  110. let (is_editing, set_is_editing) = use_state_classic(cx, || false);
  111. let todo = use_read(&cx, &TODO_LIST).get(&cx.id).unwrap();
  112. cx.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. }
  128. pub fn FilterToggles(cx: Context<()>) -> VNode {
  129. let reducer = TodoManager(use_recoil_api(cx));
  130. let items_left = use_read(cx, &TODOS_LEFT);
  131. let item_text = match items_left {
  132. 1 => "item",
  133. _ => "items",
  134. };
  135. let toggles = rsx! {
  136. ul {
  137. class: "filters"
  138. li { class: "All", a { href: "", onclick: move |_| reducer.set_filter(&FilterState::All), "All" }}
  139. li { class: "Active", a { href: "active", onclick: move |_| reducer.set_filter(&FilterState::Active), "Active" }}
  140. li { class: "Completed", a { href: "completed", onclick: move |_| reducer.set_filter(&FilterState::Completed), "Completed" }}
  141. }
  142. };
  143. rsx! { in cx,
  144. footer {
  145. span {
  146. strong {"{items_left}"}
  147. span { "{item_text} left" }
  148. }
  149. {toggles}
  150. }
  151. }
  152. }
  153. pub fn Footer(cx: Context<()>) -> VNode {
  154. rsx! { in cx,
  155. footer { class: "info"
  156. p {"Double-click to edit a todo"}
  157. p {
  158. "Created by "
  159. a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }
  160. }
  161. p {
  162. "Part of "
  163. a { "TodoMVC", href: "http://todomvc.com" }
  164. }
  165. }
  166. }
  167. }
  168. const APP_STYLE: &'static str = include_str!("./todomvc/style.css");
  169. fn App(cx: Context<()>) -> VNode {
  170. use_init_recoil_root(cx, |_| {});
  171. rsx! { in cx,
  172. div { id: "app"
  173. TodoList {}
  174. Footer {}
  175. }
  176. }
  177. }
  178. fn main() {
  179. wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App));
  180. }