-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Stores: Externally tracked state and reactive collections #4483
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
112 commits
Select commit
Hold shift + click to select a range
d3d7ec3
map signals mutably
ealmloff 1f3ceab
boxed signals
ealmloff 9a2eed3
remove mutable borrow type from boxed writable
ealmloff f13d655
improve map mut example
ealmloff 4be3d17
restore boxed_mut helper
ealmloff 01c9d0e
make boxed readable/writable types copy
ealmloff b608234
Move away from the ReadOnlySignal alias in examples
ealmloff 2b5ef8e
automatically convert Write
ealmloff 714fa5f
Fix some tests
ealmloff 195b0bd
ReadSignal and WriteSignal
ealmloff bcfa52c
move read only signal alias
ealmloff 94f2b58
make maps copy
ealmloff abb58b2
store prototype
ealmloff 08cf20b
implement derive macro
ealmloff 9b39c34
Handle generics in the derive macro
ealmloff af67e9e
Make Selector boxable
ealmloff d089c52
derive boxed conversion
ealmloff 9fda03c
Get rid of store type
ealmloff 83f6ec4
Remove hardcoded type from derive macro
ealmloff 0437501
selector -> store
ealmloff 23beada
Refactor store implementation
ealmloff c58ab37
more vec methods
ealmloff 76e6151
todo mvc store example
ealmloff f2029a9
Implement Storable for HashMap
ealmloff 7c974fd
fix write
ealmloff 274c35e
restore sorting logic in todomvc
ealmloff 0a066fa
derive readable and writable
ealmloff 1c200eb
improve todomvc store example
ealmloff 793e0f0
Merge branch 'remote-origin' into signal-map-mut
ealmloff be769bc
Result store
ealmloff 502043a
Option store
ealmloff 5d82568
collect dead subscribers
ealmloff 8f09fc1
fix clippy and formatting
ealmloff 62ec4c7
recognize some common foreign types automatically
ealmloff b08078e
fix foreign stores in props
ealmloff 2c9757c
slice and array selectors
ealmloff 83dea63
clean up store example
ealmloff d4d7ec1
writable is child owned
ealmloff 51f5ecc
box selector
ealmloff 26dfce5
read and write to builtins
ealmloff 7734244
re-use the proc macro in the derive macro
ealmloff b0ad9eb
Switch to a extension trait based approach
ealmloff 70553c2
switch to extension traits in the derive macro
ealmloff fdd7126
transposed method
ealmloff 286e444
derive enums
ealmloff 4139d42
Expose Store::new
ealmloff ee6070b
remove stores from the prelude
ealmloff 102ec32
More consistent naming scheme for SelectorScope methods
ealmloff f884a0b
document hashmap methods
ealmloff db77061
document index extension
ealmloff baaa911
document option ext
ealmloff ad69234
document result ext
ealmloff c01b958
document slice extension
ealmloff d609664
document the vec extension
ealmloff c42e1c1
make more methods borrow the store mutably
ealmloff 732b5c8
Move to impls to have higher priority over the builtin write extensio…
ealmloff 1662fa5
fix deadlock
ealmloff 7336aac
finish stores documentation
ealmloff 8fc362b
Document the store macro
ealmloff 71933f0
cleanup docs and organization
ealmloff ce90970
finish derive docs
ealmloff 1f49036
fix typo
ealmloff bdf216c
fix clippy
ealmloff a69407e
fix doc tests
ealmloff 619ad79
fix derive macro with generics
ealmloff 98befc9
fix clippy
ealmloff 31afbbf
test and fix empty structs
ealmloff 83f28a3
clean up todomvc store example
ealmloff cb12564
default to a boxed store writer to make it easier to type when provid…
ealmloff 864b061
fix cargo doc
ealmloff 0a4e939
fix clippy in store example
ealmloff c2480c4
Merge branch 'remote-origin' into stores
ealmloff b34ae85
relax generational box lifetime bounds
ealmloff 6dee9be
relax bounds
ealmloff 2fe7b7a
relax signal bounds
ealmloff 6d54702
fix compilation
ealmloff b098f5c
read store
ealmloff 5df1ad6
relax bounds on default store impls
ealmloff 2d5fd8e
fix enum derive
ealmloff 8479be4
Convert to readsignal and writesignal automatically
ealmloff a18ee31
simplify type matching logic
ealmloff 5345adb
add write store
ealmloff c54c296
unsized stores
ealmloff f8d81c0
shrink the path vec type
ealmloff b394024
use more concrete types
ealmloff 45e725d
remove opaque types
ealmloff 13a76e8
remove copy bounds
ealmloff 5f9c8d9
use lens as generic name everywhere
ealmloff 72deed9
more consistent borrow semantics
ealmloff 750348a
use a concrete type for the signal metadata and mention references in…
ealmloff 607a3c3
improve todomvc store docs
ealmloff 3bb878d
document store derive macro internals
ealmloff 506d46c
refactor macro
ealmloff 92f44e4
improve selector node documentation
ealmloff 1b13504
fix clippy
ealmloff 931b435
add btreemap
ealmloff 0922661
fix doc tests
ealmloff 3dbfb08
more option methods
ealmloff aed4792
fix doc
ealmloff b339de6
fix clippy
ealmloff 06d84d5
add a bunch more result methods
ealmloff bdb4d35
add a bunch of bounds to the iterators
ealmloff e0135dc
fix clippy
ealmloff 3d1467a
remove option from subscribers
ealmloff 7b0a718
add store macro to generate extension traits
ealmloff 72a836a
fix store example
ealmloff 2ae33d2
fix deadlock
ealmloff 8eadeb3
fix docs
ealmloff f100832
fix doc tests
ealmloff fae8d9f
fix clippy
ealmloff 3022c70
add stores to public api
jkelleyrtp 4984d36
add store macro, and then update todomvc_store example
jkelleyrtp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
//! The typical TodoMVC app, implemented in Dioxus with stores. Stores let us | ||
//! share nested reactive state between components. They let us keep our todomvc | ||
//! state in a single struct without wrapping every type in a signal while still | ||
//! maintaining fine grained reactivity. | ||
|
||
use dioxus::prelude::*; | ||
use std::{collections::HashMap, vec}; | ||
|
||
const STYLE: Asset = asset!("/examples/assets/todomvc.css"); | ||
|
||
/// Deriving the store macro on a struct will automatically generate an extension trait | ||
/// for Store<TodoState> with method to zoom into the fields of the struct. | ||
/// | ||
/// For this struct, the macro derives the following methods for Store<TodoState>: | ||
/// - `todos(self) -> Store<HashMap<u32, TodoItem>, _>` | ||
/// - `filter(self) -> Store<FilterState, _>` | ||
#[derive(Store, PartialEq, Clone, Debug)] | ||
struct TodoState { | ||
todos: HashMap<u32, TodoItem>, | ||
filter: FilterState, | ||
} | ||
|
||
// We can also add custom methods to the store by using the `store` attribute on an impl block. | ||
// The store attribute turns the impl block into an extension trait for Store<TodoState>. | ||
// Methods that take &self will automatically get a bound that Lens: Readable<Target = TodoState> | ||
// Methods that take &mut self will automatically get a bound that Lens: Writable<Target = TodoState> | ||
#[store] | ||
impl<Lens> Store<TodoState, Lens> { | ||
fn active_items(&self) -> Vec<u32> { | ||
let filter = self.filter().cloned(); | ||
let mut active_ids: Vec<u32> = self | ||
.todos() | ||
.iter() | ||
.filter_map(|(id, item)| item.active(filter).then_some(id)) | ||
.collect(); | ||
active_ids.sort_unstable(); | ||
active_ids | ||
} | ||
|
||
fn incomplete_count(&self) -> usize { | ||
self.todos() | ||
.values() | ||
.filter(|item| item.incomplete()) | ||
.count() | ||
} | ||
|
||
fn toggle_all(&mut self) { | ||
let check = self.incomplete_count() != 0; | ||
for item in self.todos().values() { | ||
item.checked().set(check); | ||
} | ||
} | ||
|
||
fn has_todos(&self) -> bool { | ||
!self.todos().is_empty() | ||
} | ||
} | ||
|
||
#[derive(PartialEq, Eq, Clone, Copy, Debug)] | ||
enum FilterState { | ||
All, | ||
Active, | ||
Completed, | ||
} | ||
|
||
#[derive(Store, PartialEq, Clone, Debug)] | ||
struct TodoItem { | ||
checked: bool, | ||
contents: String, | ||
} | ||
|
||
impl TodoItem { | ||
fn new(contents: impl ToString) -> Self { | ||
Self { | ||
checked: false, | ||
contents: contents.to_string(), | ||
} | ||
} | ||
} | ||
|
||
#[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 main() { | ||
dioxus::launch(app); | ||
} | ||
|
||
fn app() -> Element { | ||
// We store the state of our todo list in a store to use throughout the app. | ||
let mut todos = use_store(|| TodoState { | ||
todos: HashMap::new(), | ||
filter: FilterState::All, | ||
}); | ||
|
||
// We use a simple memo 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.incomplete_count()); | ||
|
||
// We use a memo 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 || todos.active_items()); | ||
|
||
// 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 |_| { | ||
todos.toggle_all(); | ||
}; | ||
|
||
rsx! { | ||
document::Link { rel: "stylesheet", href: STYLE } | ||
section { class: "todoapp", | ||
TodoHeader { todos } | ||
section { class: "main", | ||
if todos.has_todos() { | ||
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 memo | ||
// We pass the ID along with the hashmap into the TodoEntry component so it can access the todo from the todos store. | ||
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.has_todos() { | ||
ListFooter { active_todo_count, todos } | ||
} | ||
} | ||
} | ||
|
||
// 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: Store<TodoState>) -> 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::new(draft.take()); | ||
todos.todos().insert(id, todo); | ||
todo_id += 1; | ||
} | ||
}; | ||
|
||
rsx! { | ||
header { class: "header", | ||
h1 { "todos" } | ||
input { | ||
class: "new-todo", | ||
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 store as props | ||
/// We can use these together to memoize the todo contents and checked state | ||
#[component] | ||
fn TodoEntry(mut todos: Store<TodoState>, id: u32) -> Element { | ||
let mut is_editing = use_signal(|| false); | ||
|
||
// When we get an item out of the store, it will only subscribe to that specific item. | ||
// Since we only get the single todo item, the component will only rerender when that item changes. | ||
let entry = todos.todos().get(id).unwrap(); | ||
let checked = entry.checked(); | ||
let contents = entry.contents(); | ||
|
||
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| entry.checked().set(evt.checked()) | ||
} | ||
label { | ||
r#for: "cbg-{id}", | ||
ondoubleclick: move |_| is_editing.set(true), | ||
onclick: |evt| evt.prevent_default(), | ||
"{contents}" | ||
} | ||
button { | ||
class: "destroy", | ||
onclick: move |evt| { | ||
evt.prevent_default(); | ||
todos.todos().remove(&id); | ||
}, | ||
} | ||
} | ||
|
||
// Only render the actual input if we're editing | ||
if is_editing() { | ||
input { | ||
class: "edit", | ||
value: "{contents}", | ||
oninput: move |evt| entry.contents().set(evt.value()), | ||
autofocus: "true", | ||
onfocusout: move |_| is_editing.set(false), | ||
onkeydown: move |evt| { | ||
match evt.key() { | ||
Key::Enter | Key::Escape | Key::Tab => is_editing.set(false), | ||
_ => {} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[component] | ||
fn ListFooter(mut todos: Store<TodoState>, active_todo_count: ReadSignal<usize>) -> Element { | ||
// We use a memo to calculate whether we should show the "Clear completed" button. | ||
// This will recompute whenever the number of todos change or the checked state of an existing | ||
// todo changes | ||
let show_clear_completed = use_memo(move || todos.todos().values().any(|todo| todo.complete())); | ||
let mut filter = todos.filter(); | ||
|
||
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| { | ||
evt.prevent_default(); | ||
filter.set(state) | ||
}, | ||
{state_text} | ||
} | ||
} | ||
} | ||
} | ||
if show_clear_completed() { | ||
button { | ||
class: "clear-completed", | ||
onclick: move |_| todos.todos().retain(|_, todo| !todo.checked), | ||
"Clear completed" | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.