shelly-liveview 0.6.0

Core runtime primitives for Shelly LiveView.
Documentation
use crate::{Context, Event, Html, LiveResult};
use std::fmt;

/// Stable server-owned identifier for a LiveComponent.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(String);

impl ComponentId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for ComponentId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl From<&str> for ComponentId {
    fn from(value: &str) -> Self {
        Self::new(value)
    }
}

impl From<String> for ComponentId {
    fn from(value: String) -> Self {
        Self::new(value)
    }
}

/// Render result for one component after a scoped event.
#[derive(Debug, Clone, PartialEq)]
pub struct ComponentRender {
    id: ComponentId,
    html: Html,
}

impl ComponentRender {
    pub fn new(id: impl Into<ComponentId>, html: Html) -> Self {
        Self {
            id: id.into(),
            html,
        }
    }

    pub fn id(&self) -> &ComponentId {
        &self.id
    }

    pub fn html(&self) -> &Html {
        &self.html
    }

    pub fn into_parts(self) -> (ComponentId, Html) {
        (self.id, self.html)
    }
}

/// Trait implemented by server-owned child components.
///
/// Components own a smaller state boundary than a full LiveView. Parent views
/// decide how children are stored and rendered, while the session runtime can
/// patch a component target independently after a scoped event.
pub trait LiveComponent: Send + 'static {
    /// Stable id that maps to the component root DOM node.
    fn id(&self) -> ComponentId;

    /// Called by parent views when they initialize child component state.
    fn mount(&mut self, _ctx: &mut Context) -> LiveResult {
        Ok(())
    }

    /// Called by parent views when a scoped event targets this component.
    fn handle_event(&mut self, _event: Event, _ctx: &mut Context) -> LiveResult {
        Ok(())
    }

    /// Render this component root HTML.
    fn render(&self) -> Html;
}

#[cfg(test)]
mod tests {
    use super::{ComponentId, ComponentRender, LiveComponent};
    use crate::{Context, Event, Html};

    #[derive(Default)]
    struct StubComponent;

    impl LiveComponent for StubComponent {
        fn id(&self) -> ComponentId {
            ComponentId::new("stub")
        }

        fn render(&self) -> Html {
            Html::new("<div>stub</div>")
        }
    }

    #[test]
    fn component_id_and_render_parts_round_trip() {
        let render = ComponentRender::new("item-1", Html::new("<p>x</p>"));
        assert_eq!(render.id().as_str(), "item-1");
        assert_eq!(render.id().to_string(), "item-1");
        assert_eq!(render.html().as_str(), "<p>x</p>");

        let (id, html) = render.into_parts();
        assert_eq!(id.as_str(), "item-1");
        assert_eq!(html.as_str(), "<p>x</p>");

        let from_string = ComponentId::from(String::from("item-2"));
        assert_eq!(from_string.as_str(), "item-2");
    }

    #[test]
    fn live_component_default_callbacks_return_ok() {
        let mut component = StubComponent;
        let mut ctx = Context::new("root");
        assert!(component.mount(&mut ctx).is_ok());
        assert!(component.handle_event(Event::new("noop"), &mut ctx).is_ok());
        assert_eq!(component.id().as_str(), "stub");
        assert_eq!(component.render().as_str(), "<div>stub</div>");
    }
}