todomvc.rs 5.7 KB

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