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;