Skip to content

Commit

Permalink
Implement local state
Browse files Browse the repository at this point in the history
  • Loading branch information
kmicklas committed Jun 19, 2024
1 parent 94f0033 commit 9a38ad4
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 36 additions & 1 deletion examples/tutorial/src/main.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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,
Expand Down Expand Up @@ -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, '_) {
Expand Down Expand Up @@ -198,6 +232,7 @@ fn tutorial(model: &Model) -> View!(Model, '_) {
basic_html(),
state(model),
events(),
local_state(),
dynamic_view(model),
lists(model),
)
Expand Down
4 changes: 3 additions & 1 deletion ravel-web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,6 +44,8 @@ pub struct RebuildCx<'cx> {
/// A marker trait for the [`ravel::State`] types of a [`trait@View`].
pub trait ViewMarker {}

impl<T: 'static, S: ViewMarker> ViewMarker for WithLocalState<T, S> {}

macro_rules! tuple_state {
($($a:ident),*) => {
#[allow(non_camel_case_types)]
Expand Down
2 changes: 2 additions & 0 deletions ravel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
68 changes: 68 additions & 0 deletions ravel/src/local.rs
Original file line number Diff line number Diff line change
@@ -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, F, S> {
init: Init,
f: F,
phantom: PhantomData<S>,
}

impl<R: CxRep, T, Init, F, S> Builder<R> for WithLocal<Init, F, S>
where
Init: FnOnce() -> T,
F: FnOnce(Cx<S, R>, &T) -> Token<S>,
{
type State = WithLocalState<T, S>;

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<T, S> {
value: T,
inner: S,
}

impl<Output: Default, T: 'static + Default, S> State<Output>
for WithLocalState<T, S>
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<T, Init, F, S, R: CxRep>(
init: Init,
f: F,
) -> WithLocal<Init, F, S>
where
Init: FnOnce() -> T,
F: FnOnce(Cx<S, R>, &T) -> Token<S>,
{
WithLocal {
init,
f,
phantom: PhantomData,
}
}

0 comments on commit 9a38ad4

Please sign in to comment.