From 9a38ad4f13748909b53bdfa76c81238598e1b9f0 Mon Sep 17 00:00:00 2001 From: Ken Micklas Date: Tue, 11 Jun 2024 20:34:03 +0100 Subject: [PATCH] Implement local state --- README.md | 2 +- examples/tutorial/src/main.rs | 37 ++++++++++++++++++- ravel-web/src/lib.rs | 4 ++- ravel/src/lib.rs | 2 ++ ravel/src/local.rs | 68 +++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 ravel/src/local.rs diff --git a/README.md b/README.md index 38a78bf..3998379 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ trunk serve examples/todomvc/index.html ### Features -- [ ] Local state +- [x] Local state - [ ] Memoization - [ ] `async` actions - [ ] Support "message"/"reducer" architecture rather than direct model mutation diff --git a/examples/tutorial/src/main.rs b/examples/tutorial/src/main.rs index e4f909c..7ef4e27 100644 --- a/examples/tutorial/src/main.rs +++ b/examples/tutorial/src/main.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use ravel::with; +use ravel::{with, with_local}; use ravel_web::{ any, attr, collections::{btree_map, slice}, @@ -17,6 +17,7 @@ use web_sys::{ }; /// Our model type contains the global state of the application. +#[derive(Default)] struct Model { count: usize, message: String, @@ -112,6 +113,39 @@ fn events() -> View!(Model) { ) } +/// Sometimes, we might not want to store all state in our global [`Model`]. +/// +/// This is useful when it would be tedious to write down uninteresting state +/// types, or when you want to encapsulate the behavior of a reusable component. +/// However, it generally increases complexity and makes testing harder. +fn local_state() -> View!(Model) { + with_local( + // We provide an initialization callback, which is only run when the + // component is constructed for the first time. + || 0, + |cx, local_count| { + // Inside the body, we have a reference to the current local state. + cx.build(( + el::h2("Local state"), + el::p(("Local count: ", display(*local_count))), + el::p(el::button(( + "Increment local count", + // Although we have a reference to the current value, we + // cannot mutate it, or store it in an event handler (which + // must remain `'static`). + // + // Instead, [`with_local`] changes our state type to be a + // tuple which has both the outer state ([`Model`]) and our + // local state type. + on_(event::Click, move |(_model, local_count): &mut _| { + *local_count += 1; + }), + ))), + )) + }, + ) +} + /// All of our views so far have had a static structure. Sometimes, we need to /// swap out or hide various components. fn dynamic_view(model: &Model) -> View!(Model, '_) { @@ -198,6 +232,7 @@ fn tutorial(model: &Model) -> View!(Model, '_) { basic_html(), state(model), events(), + local_state(), dynamic_view(model), lists(model), ) diff --git a/ravel-web/src/lib.rs b/ravel-web/src/lib.rs index 3593fdf..d5c392d 100644 --- a/ravel-web/src/lib.rs +++ b/ravel-web/src/lib.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use atomic_waker::AtomicWaker; use dom::Position; -use ravel::{Builder, Cx, CxRep}; +use ravel::{Builder, Cx, CxRep, WithLocalState}; mod any; pub mod attr; @@ -44,6 +44,8 @@ pub struct RebuildCx<'cx> { /// A marker trait for the [`ravel::State`] types of a [`trait@View`]. pub trait ViewMarker {} +impl ViewMarker for WithLocalState {} + macro_rules! tuple_state { ($($a:ident),*) => { #[allow(non_camel_case_types)] diff --git a/ravel/src/lib.rs b/ravel/src/lib.rs index 0248938..5481546 100644 --- a/ravel/src/lib.rs +++ b/ravel/src/lib.rs @@ -8,8 +8,10 @@ use std::{marker::PhantomData, mem::MaybeUninit}; use paste::paste; mod any; +mod local; pub use any::*; +pub use local::*; /// A dummy type which typically represents a "backend". pub trait CxRep { diff --git a/ravel/src/local.rs b/ravel/src/local.rs new file mode 100644 index 0000000..91d976e --- /dev/null +++ b/ravel/src/local.rs @@ -0,0 +1,68 @@ +use std::marker::PhantomData; + +use crate::{with, Builder, Cx, CxRep, State, Token}; + +/// A [`Builder`] created from [`with_local`]. +pub struct WithLocal { + init: Init, + f: F, + phantom: PhantomData, +} + +impl Builder for WithLocal +where + Init: FnOnce() -> T, + F: FnOnce(Cx, &T) -> Token, +{ + type State = WithLocalState; + + fn build(self, cx: R::BuildCx<'_>) -> Self::State { + let value = (self.init)(); + let inner = with(|cx| (self.f)(cx, &value)).build(cx); + WithLocalState { value, inner } + } + + fn rebuild(self, cx: R::RebuildCx<'_>, state: &mut Self::State) { + with(|cx| (self.f)(cx, &mut state.value)).rebuild(cx, &mut state.inner) + } +} + +/// The state of a [`WithLocal`]. +pub struct WithLocalState { + value: T, + inner: S, +} + +impl State + for WithLocalState +where + S: State<(Output, T)>, +{ + fn run(&mut self, output: &mut Output) { + let mut data = + (std::mem::take(output), std::mem::take(&mut self.value)); + self.inner.run(&mut data); + (*output, self.value) = data; + } +} + +/// Creates a [`Builder`] which has access to a local state value. +/// +/// The `init` callback determines the inital value of the local state, and will +/// only be run when the component is initially built. +/// +/// Like [`with`], `f` must call [`Cx::build`] to return a [`Token`]. +pub fn with_local( + init: Init, + f: F, +) -> WithLocal +where + Init: FnOnce() -> T, + F: FnOnce(Cx, &T) -> Token, +{ + WithLocal { + init, + f, + phantom: PhantomData, + } +}