use dioxus::prelude::*;
use dioxus_stores::*;
use std::collections::HashMap;
#[derive(Store, PartialEq, Clone, Debug)]
struct TodoState {
todos: HashMap<u32, TodoItem>,
filter: FilterState,
}
#[derive(Store, PartialEq, Clone, Debug)]
struct TodoItem {
checked: bool,
contents: String,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum FilterState {
All,
Active,
Completed,
}
// Custom methods for TodoState
#[store]
impl<Lens> Store<TodoState, Lens> {
fn active_items(&self) -> Vec<u32> {
let filter = self.filter().cloned();
self.todos()
.iter()
.filter_map(|(id, item)| {
item.active(filter).then_some(id)
})
.collect()
}
fn incomplete_count(&self) -> usize {
self.todos()
.values()
.filter(|item| !item.checked().cloned())
.count()
}
}
// Custom methods for TodoItem
#[store]
impl<Lens> Store<TodoItem, Lens> {
fn complete(&self) -> bool {
self.checked().cloned()
}
fn incomplete(&self) -> bool {
!self.complete()
}
fn active(&self, filter: FilterState) -> bool {
match filter {
FilterState::All => true,
FilterState::Active => self.incomplete(),
FilterState::Completed => self.complete(),
}
}
}
fn App() -> Element {
let mut todos = use_store(|| TodoState {
todos: HashMap::new(),
filter: FilterState::All,
});
let filtered_todos = use_memo(move || todos.active_items());
rsx! {
section { class: "todoapp",
TodoHeader { todos }
ul { class: "todo-list",
for id in filtered_todos() {
TodoEntry { key: "{id}", id, todos }
}
}
}
}
}
#[component]
fn TodoHeader(mut todos: Store<TodoState>) -> Element {
let mut draft = use_signal(String::new);
let mut todo_id = use_signal(|| 0);
let onkeydown = move |evt: KeyboardEvent| {
if evt.key() == Key::Enter && !draft().is_empty() {
let id = todo_id();
let item = TodoItem {
checked: false,
contents: draft.take(),
};
todos.todos().insert(id, item);
todo_id += 1;
}
};
rsx! {
header { class: "header",
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
oninput: move |e| draft.set(e.value()),
onkeydown
}
}
}
}
#[component]
fn TodoEntry(mut todos: Store<TodoState>, id: u32) -> Element {
let mut is_editing = use_signal(|| false);
// Only subscribes to this specific todo item
let entry = todos.todos().get(id).unwrap();
let checked = entry.checked();
let contents = entry.contents();
rsx! {
li {
class: if checked() { "completed" },
class: if is_editing() { "editing" },
div { class: "view",
input {
class: "toggle",
r#type: "checkbox",
checked: "{checked}",
oninput: move |e| entry.checked().set(e.checked())
}
label {
ondoubleclick: move |_| is_editing.set(true),
"{contents}"
}
button {
class: "destroy",
onclick: move |_| { todos.todos().remove(&id); }
}
}
if is_editing() {
input {
class: "edit",
value: "{contents}",
oninput: move |e| entry.contents().set(e.value()),
onfocusout: move |_| is_editing.set(false)
}
}
}
}
}