use crate::cmd::Cmd;
use crate::core::Element;
pub trait Component: Sized {
type Props: Clone + Default;
type Msg;
type Model;
fn init(props: &Self::Props) -> (Self::Model, Cmd);
fn update(model: &mut Self::Model, msg: Self::Msg) -> Cmd;
fn view(model: &Self::Model, props: &Self::Props) -> Element;
#[allow(unused_variables)]
fn should_render(
model: &Self::Model,
old_props: &Self::Props,
new_props: &Self::Props,
) -> bool {
true
}
#[allow(unused_variables)]
fn unmount(model: &Self::Model) {}
#[allow(unused_variables)]
fn on_mount(model: &Self::Model) -> Cmd {
Cmd::none()
}
}
pub trait StatelessComponent: Sized {
type Props: Clone + Default;
fn render(props: &Self::Props) -> Element;
#[allow(unused_variables)]
fn should_render(old_props: &Self::Props, new_props: &Self::Props) -> bool {
true
}
}
impl<T: StatelessComponent> Component for T {
type Props = <T as StatelessComponent>::Props;
type Msg = (); type Model = ();
fn init(_props: &Self::Props) -> (Self::Model, Cmd) {
((), Cmd::none())
}
fn update(_model: &mut Self::Model, _msg: Self::Msg) -> Cmd {
Cmd::none()
}
fn view(_model: &Self::Model, props: &Self::Props) -> Element {
T::render(props)
}
fn should_render(
_model: &Self::Model,
old_props: &Self::Props,
new_props: &Self::Props,
) -> bool {
T::should_render(old_props, new_props)
}
}
pub struct ComponentInstance<C: Component> {
pub model: C::Model,
pub props: C::Props,
pending_cmds: Vec<Cmd>,
}
impl<C: Component> ComponentInstance<C> {
pub fn new(props: C::Props) -> Self {
let (model, cmd) = C::init(&props);
let mut instance = Self {
model,
props,
pending_cmds: Vec::new(),
};
if !cmd.is_none() {
instance.pending_cmds.push(cmd);
}
let mount_cmd = C::on_mount(&instance.model);
if !mount_cmd.is_none() {
instance.pending_cmds.push(mount_cmd);
}
instance
}
pub fn send(&mut self, msg: C::Msg) {
let cmd = C::update(&mut self.model, msg);
if !cmd.is_none() {
self.pending_cmds.push(cmd);
}
}
pub fn update_props(&mut self, new_props: C::Props) -> bool {
let should_render = C::should_render(&self.model, &self.props, &new_props);
self.props = new_props;
should_render
}
pub fn view(&self) -> Element {
C::view(&self.model, &self.props)
}
pub fn take_cmds(&mut self) -> Vec<Cmd> {
std::mem::take(&mut self.pending_cmds)
}
pub fn has_pending_cmds(&self) -> bool {
!self.pending_cmds.is_empty()
}
pub fn unmount(self) {
C::unmount(&self.model);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Default)]
struct TestProps {
value: i32,
}
enum TestMsg {
Increment,
Set(i32),
}
struct TestModel {
count: i32,
}
struct TestComponent;
impl Component for TestComponent {
type Props = TestProps;
type Msg = TestMsg;
type Model = TestModel;
fn init(props: &Self::Props) -> (Self::Model, Cmd) {
(TestModel { count: props.value }, Cmd::none())
}
fn update(model: &mut Self::Model, msg: Self::Msg) -> Cmd {
match msg {
TestMsg::Increment => model.count += 1,
TestMsg::Set(v) => model.count = v,
}
Cmd::none()
}
fn view(model: &Self::Model, _props: &Self::Props) -> Element {
Element::text(format!("Count: {}", model.count))
}
fn should_render(
_model: &Self::Model,
old_props: &Self::Props,
new_props: &Self::Props,
) -> bool {
old_props.value != new_props.value
}
}
#[test]
fn test_component_init() {
let props = TestProps { value: 42 };
let (model, cmd) = TestComponent::init(&props);
assert_eq!(model.count, 42);
assert!(cmd.is_none());
}
#[test]
fn test_component_update() {
let props = TestProps { value: 0 };
let (mut model, _) = TestComponent::init(&props);
TestComponent::update(&mut model, TestMsg::Increment);
assert_eq!(model.count, 1);
TestComponent::update(&mut model, TestMsg::Set(100));
assert_eq!(model.count, 100);
}
#[test]
fn test_component_instance() {
let props = TestProps { value: 10 };
let mut instance = ComponentInstance::<TestComponent>::new(props);
assert_eq!(instance.model.count, 10);
instance.send(TestMsg::Increment);
assert_eq!(instance.model.count, 11);
instance.send(TestMsg::Set(50));
assert_eq!(instance.model.count, 50);
}
#[test]
fn test_component_instance_props_update() {
let props = TestProps { value: 10 };
let mut instance = ComponentInstance::<TestComponent>::new(props);
let should_render = instance.update_props(TestProps { value: 10 });
assert!(!should_render);
let should_render = instance.update_props(TestProps { value: 20 });
assert!(should_render);
}
#[derive(Clone, Default)]
struct StatelessProps {
text: String,
}
struct StatelessTest;
impl StatelessComponent for StatelessTest {
type Props = StatelessProps;
fn render(props: &Self::Props) -> Element {
Element::text(props.text.clone())
}
}
#[test]
fn test_stateless_component() {
let props = StatelessProps {
text: "Hello".to_string(),
};
let element = StatelessTest::render(&props);
let _ = element;
}
#[test]
fn test_stateless_as_component() {
let props = StatelessProps {
text: "Test".to_string(),
};
let (model, cmd) = <StatelessTest as Component>::init(&props);
assert_eq!(model, ());
assert!(cmd.is_none());
let mut model = ();
let cmd = <StatelessTest as Component>::update(&mut model, ());
assert!(cmd.is_none());
}
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Clone, Default)]
struct LifecycleProps {
mounted: Arc<AtomicBool>,
unmounted: Arc<AtomicBool>,
}
struct LifecycleModel {
mounted: Arc<AtomicBool>,
unmounted: Arc<AtomicBool>,
}
struct LifecycleComponent;
impl Component for LifecycleComponent {
type Props = LifecycleProps;
type Msg = ();
type Model = LifecycleModel;
fn init(props: &Self::Props) -> (Self::Model, Cmd) {
(
LifecycleModel {
mounted: props.mounted.clone(),
unmounted: props.unmounted.clone(),
},
Cmd::none(),
)
}
fn update(_model: &mut Self::Model, _msg: Self::Msg) -> Cmd {
Cmd::none()
}
fn view(_model: &Self::Model, _props: &Self::Props) -> Element {
Element::text("")
}
fn on_mount(model: &Self::Model) -> Cmd {
model.mounted.store(true, Ordering::SeqCst);
Cmd::none()
}
fn unmount(model: &Self::Model) {
model.unmounted.store(true, Ordering::SeqCst);
}
}
#[test]
fn test_lifecycle_hooks() {
let mounted = Arc::new(AtomicBool::new(false));
let unmounted = Arc::new(AtomicBool::new(false));
let props = LifecycleProps {
mounted: mounted.clone(),
unmounted: unmounted.clone(),
};
let instance = ComponentInstance::<LifecycleComponent>::new(props);
assert!(mounted.load(Ordering::SeqCst), "on_mount should be called");
assert!(
!unmounted.load(Ordering::SeqCst),
"unmount should not be called yet"
);
instance.unmount();
assert!(
unmounted.load(Ordering::SeqCst),
"unmount should be called after unmount()"
);
}
}