From f865e3f55944676ba51d550493cb66a8d22ab1a0 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 31 May 2025 13:21:46 +0100 Subject: [PATCH] Unify todomvc and todomvc-native examples (#4228) Signed-off-by: Nico Burns --- examples/assets/todomvc-native.css | 379 ----------------------------- examples/assets/todomvc.css | 5 + examples/todomvc-native.rs | 269 -------------------- 3 files changed, 5 insertions(+), 648 deletions(-) delete mode 100644 examples/assets/todomvc-native.css delete mode 100644 examples/todomvc-native.rs diff --git a/examples/assets/todomvc-native.css b/examples/assets/todomvc-native.css deleted file mode 100644 index 2a25a627e..000000000 --- a/examples/assets/todomvc-native.css +++ /dev/null @@ -1,379 +0,0 @@ -html, -body, pre { - margin: 0; - padding: 0; -} - -button { - margin: 0; - padding: 0; - border: 0; - background: none; - font-size: 100%; - vertical-align: baseline; - font-family: inherit; - font-weight: inherit; - color: inherit; - -webkit-appearance: none; - appearance: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; - line-height: 1.4em; - background: #f5f5f5; - color: #4d4d4d; - min-width: 230px; - max-width: 550px; - margin: 0 auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-weight: 300; -} - -:focus { - outline: 0; -} - -.hidden { - display: none; -} - -.todoapp { - background: #fff; - margin: 130px 0 40px 0; - position: relative; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); -} - -.todoapp input::-webkit-input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::-moz-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp h1 { - position: absolute; - top: -155px; - width: 100%; - font-size: 100px; - font-weight: 100; - text-align: center; - color: rgba(175, 47, 47, 1.0); - -webkit-text-rendering: optimizeLegibility; - -moz-text-rendering: optimizeLegibility; - text-rendering: optimizeLegibility; -} - -.new-todo, -.edit { - position: relative; - margin: 0; - width: 100%; - font-size: 24px; - font-family: inherit; - font-weight: inherit; - line-height: 1.4em; - border: 0; - color: inherit; - padding: 6px; - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); - box-sizing: border-box; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.new-todo { - padding: 16px 16px 16px 60px; - border: none; - background: rgba(0, 0, 0, 0.003); - box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); -} - -.main { - position: relative; - z-index: 2; - border-top: 1px solid #e6e6e6; -} - -.toggle-all { - text-align: center; - border: none; - /* Mobile Safari */ - opacity: 0; - position: absolute; -} - -.toggle-all+label { - width: 60px; - height: 34px; - font-size: 0; - position: absolute; - top: -52px; - left: -13px; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} - -.toggle-all+label:before { - content: '❯'; - font-size: 22px; - color: #e6e6e6; - padding: 10px 27px 10px 27px; -} - -.toggle-all:checked+label:before { - color: #737373; -} - -.todo-list { - margin: 0; - padding: 0; - list-style: none; -} - -.todo-list li { - position: relative; - font-size: 24px; - border-bottom: 1px solid #ededed; -} - -.todo-list li:last-child { - border-bottom: none; -} - -.todo-list li.editing { - border-bottom: none; - padding: 0; -} - -.todo-list li.editing .edit { - display: block; - width: 506px; - padding: 12px 16px; - margin: 0 0 0 43px; -} - -.todo-list li.editing .view { - display: none; -} - -.todo-list li .toggle { - text-align: center; - width: 40px; - /* auto, since non-WebKit browsers doesn't support input styling */ - height: auto; - position: absolute; - top: 0; - bottom: 0; - margin: auto 0; - border: none; - /* Mobile Safari */ - -webkit-appearance: none; - appearance: none; -} - -.todo-list li .toggle { - opacity: 0; -} - -.todo-list li .toggle+label { - /* - Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 - IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ - */ - background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); - background-repeat: no-repeat; - background-position: center left; -} - -.todo-list li .toggle:checked+label { - background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); -} - -.todo-list li label { - word-break: break-all; - padding: 15px 15px 15px 60px; - display: block; - line-height: 1.2; - transition: color 0.4s; -} - -.todo-list li.completed label { - color: #d9d9d9; - text-decoration: line-through; -} - -.todo-list li .destroy { - display: none; - position: absolute; - top: 0; - right: 10px; - bottom: 0; - width: 40px; - height: 40px; - margin: auto 0; - font-size: 30px; - color: #cc9a9a; - margin-bottom: 11px; - transition: color 0.2s ease-out; -} - -.todo-list li .destroy:hover { - color: #af5b5e; -} - -.todo-list li .destroy:after { - content: '×'; -} - -.todo-list li:hover .destroy { - display: block; -} - -.todo-list li .edit { - display: none; -} - -.todo-list li.editing:last-child { - margin-bottom: -1px; -} - -.footer { - color: #777; - padding: 10px 15px; - height: 20px; - text-align: center; - border-top: 1px solid #e6e6e6; - display: flex; - justify-content: space-between; - align-items: center; -} - -.footer:before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 50px; - overflow: hidden; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); -} - -.todo-count { - float: left; - text-align: left; - white-space-collapse: preserve; -} - -.todo-count strong { - font-weight: 300; -} - -.filters { - margin: 0; - padding: 0; - list-style: none; - position: absolute; - right: 0; - left: 0; -} - -.filters li { - display: inline-block; -} - -.filters li a { - display: inline-block; - color: inherit; - margin: 3px; - padding: 3px 7px; - text-decoration: none; - border: 1px solid transparent; - border-radius: 3px; -} - -.filters li a:hover { - border-color: rgba(175, 47, 47, 0.1); -} - -.filters li a.selected { - border-color: rgba(175, 47, 47, 0.2); -} - -.clear-completed, -html .clear-completed:active { - float: right; - position: relative; - line-height: 20px; - text-decoration: none; - cursor: pointer; - display: inline-block; -} - -.clear-completed:hover { - text-decoration: underline; -} - -.info { - margin: 65px auto 0; - color: #bfbfbf; - font-size: 10px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-align: center; -} - -.info p { - line-height: 1; -} - -.info a { - color: inherit; - text-decoration: none; - font-weight: 400; -} - -.info a:hover { - text-decoration: underline; -} - - -/* - Hack to remove background from Mobile Safari. - Can't use it globally since it destroys checkboxes in Firefox -*/ - -@media screen and (-webkit-min-device-pixel-ratio:0) { - .toggle-all, - .todo-list li .toggle { - background: none; - } - .todo-list li .toggle { - height: 40px; - } -} - -@media (max-width: 430px) { - .footer { - height: 50px; - } - .filters { - bottom: 10px; - } -} diff --git a/examples/assets/todomvc.css b/examples/assets/todomvc.css index b8fc4b6aa..bf86bdceb 100644 --- a/examples/assets/todomvc.css +++ b/examples/assets/todomvc.css @@ -255,6 +255,9 @@ body { } .footer { + display: flex; + justify-content: space-between; + align-items: center; color: #777; padding: 10px 15px; height: 20px; @@ -276,6 +279,7 @@ body { .todo-count { float: left; text-align: left; + white-space-collapse: preserve; } .todo-count strong { @@ -296,6 +300,7 @@ body { } .filters li a { + display: inline-block; color: inherit; margin: 3px; padding: 3px 7px; diff --git a/examples/todomvc-native.rs b/examples/todomvc-native.rs deleted file mode 100644 index 21b106d2e..000000000 --- a/examples/todomvc-native.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! The typical TodoMVC app, implemented in Dioxus. - -use dioxus::prelude::*; -use std::collections::HashMap; - -const STYLE: Asset = asset!("/examples/assets/todomvc-native.css"); - -fn main() { - dioxus::launch(app); -} - -#[derive(PartialEq, Eq, Clone, Copy)] -enum FilterState { - All, - Active, - Completed, -} - -#[derive(Debug, PartialEq, Eq)] -struct TodoItem { - id: u32, - checked: bool, - contents: String, -} - -fn app() -> Element { - // We store the todos in a HashMap in a Signal. - // Each key is the id of the todo, and the value is the todo itself. - let mut todos = use_signal(HashMap::::new); - - let filter = use_signal(|| FilterState::All); - - // We use a simple memoized signal to calculate the number of active todos. - // Whenever the todos change, the active_todo_count will be recalculated. - let active_todo_count = - use_memo(move || todos.read().values().filter(|item| !item.checked).count()); - - // We use a memoized signal to filter the todos based on the current filter state. - // Whenever the todos or filter change, the filtered_todos will be recalculated. - // Note that we're only storing the IDs of the todos, not the todos themselves. - let filtered_todos = use_memo(move || { - let mut filtered_todos = todos - .read() - .iter() - .filter(|(_, item)| match filter() { - FilterState::All => true, - FilterState::Active => !item.checked, - FilterState::Completed => item.checked, - }) - .map(|f| *f.0) - .collect::>(); - - filtered_todos.sort_unstable(); - - filtered_todos - }); - - // Toggle all the todos to the opposite of the current state. - // If all todos are checked, uncheck them all. If any are unchecked, check them all. - let toggle_all = move |_| { - let check = active_todo_count() != 0; - for (_, item) in todos.write().iter_mut() { - item.checked = check; - } - }; - - rsx! { - document::Link { rel: "stylesheet", href: STYLE } - body { - section { class: "todoapp", - TodoHeader { todos } - section { class: "main", - if !todos.read().is_empty() { - input { - id: "toggle-all", - class: "toggle-all", - r#type: "checkbox", - onchange: toggle_all, - checked: active_todo_count() == 0 - } - label { r#for: "toggle-all" } - } - - // Render the todos using the filtered_todos signal - // We pass the ID into the TodoEntry component so it can access the todo from the todos signal. - // Since we store the todos in a signal too, we also need to send down the todo list - ul { class: "todo-list", - for id in filtered_todos() { - TodoEntry { key: "{id}", id, todos } - } - } - - // We only show the footer if there are todos. - if !todos.read().is_empty() { - ListFooter { active_todo_count, todos, filter } - } - } - } - - // A simple info footer - footer { class: "info", - p { "Double-click to edit a todo" } - p { - "Created by " - a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" } - } - p { - "Part of " - a { href: "http://todomvc.com", "TodoMVC" } - } - } - } - } -} - -#[component] -fn TodoHeader(mut todos: Signal>) -> Element { - let mut draft = use_signal(|| "".to_string()); - let mut todo_id = use_signal(|| 0); - - let onkeydown = move |evt: KeyboardEvent| { - if evt.key() == Key::Enter && !draft.read().is_empty() { - let id = todo_id(); - let todo = TodoItem { - id, - checked: false, - contents: draft.to_string(), - }; - todos.write().insert(id, todo); - todo_id += 1; - draft.set("".to_string()); - evt.prevent_default(); - } - }; - - rsx! { - header { class: "header", - h1 { "todos" } - input { - class: "new-todo", - r#type: "text", - placeholder: "What needs to be done?", - value: "{draft}", - autofocus: "true", - oninput: move |evt| draft.set(evt.value()), - onkeydown - } - } - } -} - -/// A single todo entry -/// This takes the ID of the todo and the todos signal as props -/// We can use these together to memoize the todo contents and checked state -#[component] -fn TodoEntry(mut todos: Signal>, id: u32) -> Element { - let mut is_editing = use_signal(|| false); - - // To avoid re-rendering this component when the todo list changes, we isolate our reads to memos - // This way, the component will only re-render when the contents of the todo change, or when the editing state changes. - // This does involve taking a local clone of the todo contents, but it allows us to prevent this component from re-rendering - let checked = use_memo(move || todos.read().get(&id).unwrap().checked); - let contents = use_memo(move || todos.read().get(&id).unwrap().contents.clone()); - - rsx! { - li { - // Dioxus lets you use if statements in rsx to conditionally render attributes - // These will get merged into a single class attribute - class: if checked() { "completed" }, - class: if is_editing() { "editing" }, - - // Some basic controls for the todo - div { class: "view", - input { - class: "toggle", - r#type: "checkbox", - id: "cbg-{id}", - checked: "{checked}", - oninput: move |evt| todos.write().get_mut(&id).unwrap().checked = evt.checked() - } - label { - r#for: "cbg-{id}", - onclick: move |evt| { - is_editing.set(true); - evt.prevent_default() - }, - "{contents}" - } - button { - class: "destroy", - onclick: move |evt| { - todos.write().remove(&id); - evt.prevent_default(); - }, - } - } - - // Only render the actual input if we're editing - if is_editing() { - input { - class: "edit", - r#type: "text", - value: "{contents}", - oninput: move |evt| todos.write().get_mut(&id).unwrap().contents = evt.value(), - autofocus: "true", - onfocusout: move |_| is_editing.set(false), - onkeydown: move |evt| { - if matches!(evt.key(), Key::Enter | Key::Escape | Key::Tab) { - evt.prevent_default(); - is_editing.set(false); - } - } - } - } - } - } -} - -#[component] -fn ListFooter( - mut todos: Signal>, - active_todo_count: ReadOnlySignal, - mut filter: Signal, -) -> Element { - // We use a memoized signal to calculate whether we should show the "Clear completed" button. - // This will recompute whenever the todos change, and if the value is true, the button will be shown. - let show_clear_completed = use_memo(move || todos.read().values().any(|todo| todo.checked)); - - rsx! { - footer { class: "footer", - span { class: "todo-count", - strong { "{active_todo_count} " } - span { - match active_todo_count() { - 1 => "item", - _ => "items", - }, - " left" - } - } - ul { class: "filters", - for (state , state_text , url) in [ - (FilterState::All, "All", "#/"), - (FilterState::Active, "Active", "#/active"), - (FilterState::Completed, "Completed", "#/completed"), - ] { - li { - a { - href: url, - class: if filter() == state { "selected" }, - onclick: move |evt| { - filter.set(state); - evt.prevent_default(); - }, - {state_text} - } - } - } - } - if show_clear_completed() { - button { - class: "clear-completed", - onclick: move |_| todos.write().retain(|_, todo| !todo.checked), - "Clear completed" - } - } - } - } -}