todomvc.rs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. #![allow(non_snake_case)]
  2. use dioxus::prelude::*;
  3. use dioxus_elements::input_data::keyboard_types::Key;
  4. fn main() {
  5. dioxus_desktop::launch(app);
  6. }
  7. #[derive(PartialEq, Eq)]
  8. pub enum FilterState {
  9. All,
  10. Active,
  11. Completed,
  12. }
  13. #[derive(Debug, PartialEq, Eq, Clone)]
  14. pub struct TodoItem {
  15. pub id: u32,
  16. pub checked: bool,
  17. pub contents: String,
  18. }
  19. pub fn app(cx: Scope<()>) -> Element {
  20. let todos = use_state(&cx, im_rc::HashMap::<u32, TodoItem>::default);
  21. let filter = use_state(&cx, || FilterState::All);
  22. let draft = use_state(&cx, || "".to_string());
  23. let todo_id = use_state(&cx, || 0);
  24. // Filter the todos based on the filter state
  25. let mut filtered_todos = todos
  26. .iter()
  27. .filter(|(_, item)| match **filter {
  28. FilterState::All => true,
  29. FilterState::Active => !item.checked,
  30. FilterState::Completed => item.checked,
  31. })
  32. .map(|f| *f.0)
  33. .collect::<Vec<_>>();
  34. filtered_todos.sort_unstable();
  35. let show_clear_completed = todos.values().any(|todo| todo.checked);
  36. let items_left = filtered_todos.len();
  37. let item_text = match items_left {
  38. 1 => "item",
  39. _ => "items",
  40. };
  41. cx.render(rsx!{
  42. section { class: "todoapp",
  43. style { include_str!("./assets/todomvc.css") }
  44. div {
  45. header { class: "header",
  46. h1 {"todos"}
  47. input {
  48. class: "new-todo",
  49. placeholder: "What needs to be done?",
  50. value: "{draft}",
  51. autofocus: "true",
  52. oninput: move |evt| {
  53. println!("calling oninput");
  54. draft.set(evt.value.clone());
  55. },
  56. onkeydown: move |evt| {
  57. if evt.key() == Key::Enter && !draft.is_empty() {
  58. todos.make_mut().insert(
  59. **todo_id,
  60. TodoItem {
  61. id: **todo_id,
  62. checked: false,
  63. contents: draft.to_string(),
  64. },
  65. );
  66. *todo_id.make_mut() += 1;
  67. draft.set("".to_string());
  68. }
  69. }
  70. }
  71. }
  72. ul { class: "todo-list",
  73. filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id, todos: todos }))
  74. }
  75. (!todos.is_empty()).then(|| rsx!(
  76. footer { class: "footer",
  77. span { class: "todo-count",
  78. strong {"{items_left} "}
  79. span {"{item_text} left"}
  80. }
  81. ul { class: "filters",
  82. li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }}
  83. li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }}
  84. li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }}
  85. }
  86. show_clear_completed.then(|| rsx!(
  87. button {
  88. class: "clear-completed",
  89. onclick: move |_| todos.make_mut().retain(|_, todo| !todo.checked),
  90. "Clear completed"
  91. }
  92. ))
  93. }
  94. ))
  95. }
  96. }
  97. footer { class: "info",
  98. p {"Double-click to edit a todo"}
  99. p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
  100. p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
  101. }
  102. })
  103. }
  104. #[derive(Props)]
  105. pub struct TodoEntryProps<'a> {
  106. todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
  107. id: u32,
  108. }
  109. pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
  110. let is_editing = use_state(&cx, || false);
  111. let todos = cx.props.todos.get();
  112. let todo = &todos[&cx.props.id];
  113. let completed = if todo.checked { "completed" } else { "" };
  114. let editing = if **is_editing { "editing" } else { "" };
  115. println!("rendering todo entry");
  116. cx.render(rsx!{
  117. li {
  118. class: "{completed} {editing}",
  119. div { class: "view",
  120. input {
  121. class: "toggle",
  122. r#type: "checkbox",
  123. id: "cbg-{todo.id}",
  124. checked: "{todo.checked}",
  125. oninput: move |evt| {
  126. cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
  127. }
  128. }
  129. label {
  130. r#for: "cbg-{todo.id}",
  131. onclick: move |_| is_editing.set(true),
  132. prevent_default: "onclick",
  133. "{todo.contents}"
  134. }
  135. }
  136. is_editing.then(|| rsx!{
  137. input {
  138. class: "edit",
  139. value: "{todo.contents}",
  140. oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value.clone(),
  141. autofocus: "true",
  142. onfocusout: move |_| is_editing.set(false),
  143. onkeydown: move |evt| {
  144. match evt.key() {
  145. Key::Enter | Key::Escape | Key::Tab => is_editing.set(false),
  146. _ => {}
  147. }
  148. },
  149. }
  150. })
  151. }
  152. })
  153. }