todomvc.rs 5.5 KB

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