sycamore_state/
lib.rs

1// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com)
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14use std::{marker::PhantomData, rc::Rc};
15
16use sycamore::prelude::*;
17
18/// Trait that maps a message and an original state value to a new value.
19/// Implementors of this trait can implement all of their state management
20/// logic in one place.
21pub trait MessageMapper<Msg, Val> {
22    fn map<'ctx>(&self, cx: Scope<'ctx>, msg: Msg, original: &'ctx Signal<Val>);
23}
24
25/// Provides the necessary wiring for a centralized state handling
26/// mechanism. The API guides you along handling the lifetimes properly
27/// as well as registering all the state management logic in one place.
28pub struct Handler<'ctx, D, T, Msg>
29where
30    D: MessageMapper<Msg, T>,
31{
32    signal: &'ctx Signal<T>,
33    dispatcher: &'ctx D,
34    _phantom: PhantomData<Msg>,
35}
36
37impl<'ctx, D, T, Msg> Handler<'ctx, D, T, Msg>
38where
39    D: MessageMapper<Msg, T>,
40{
41    /// Constructs a new Handler with a lifetime anchored to the provided Scope.
42    /// You will usually construct this in your top level scope and then
43    /// pass the handlers down into your components.
44    pub fn new(cx: Scope<'ctx>, initial: T, dispatcher: D) -> &'ctx Self {
45        let signal = create_signal(cx, initial);
46        let dispatcher = create_ref(cx, dispatcher);
47        create_ref(
48            cx,
49            Self {
50                signal,
51                dispatcher,
52                _phantom: PhantomData,
53            },
54        )
55    }
56
57    /// Directly handle a state message without requiring a binding.
58    pub fn dispatch(&'ctx self, cx: Scope<'ctx>, msg: Msg) {
59        self.dispatcher.map(cx, msg, self.signal)
60    }
61
62    /// Provides a ReadSignal handle for the contained Signal implementation.
63    pub fn read_signal(&'ctx self) -> &'ctx ReadSignal<T> {
64        self.signal
65    }
66
67    /// Binds a triggering signal and associated message mapping function as
68    /// a state update for this Handler instance. This uses [`create_effect`]
69    /// so be aware that it will fire at least once when you first call it. It
70    /// is really easy to introduce an infinite signal update loop.
71    pub fn bind_trigger<F, Val>(
72        &'ctx self,
73        cx: Scope<'ctx>,
74        trigger: &'ctx ReadSignal<Val>,
75        message_fn: F,
76    ) where
77        F: Fn(Rc<Val>) -> Msg + 'ctx,
78    {
79        create_effect(cx, move || self.dispatch(cx, message_fn(trigger.get())));
80    }
81
82    /// Helper method to get a memoized value derived from the contained
83    /// state for this Handler. The provided handler only notifies subscribers
84    /// If the state has actually been updated.
85    pub fn get_selector<F, Val>(
86        &'ctx self,
87        cx: Scope<'ctx>,
88        selector_factory: F,
89    ) -> &'ctx ReadSignal<Val>
90    where
91        F: Fn(&'ctx ReadSignal<T>) -> Val + 'ctx,
92        Val: PartialEq,
93    {
94        create_selector(cx, move || selector_factory(self.signal))
95    }
96
97    // Helper method to get a non reactive value from the state.
98    pub fn get_value<F, Val>(&'ctx self, getter_factory: F) -> Val
99    where
100        F: Fn(&'ctx ReadSignal<T>) -> Val + 'ctx,
101    {
102        getter_factory(self.signal)
103    }
104}
105
106#[cfg(test)]
107mod tests;