leptos-use 0.15.7

Collection of essential Leptos utilities inspired by React-Use / VueUse / SolidJS-USE
Documentation
use crate::sendwrap_fn;
use crate::{
    js, use_event_listener, use_event_listener_with_options, use_supported, UseEventListenerOptions,
};
use codee::{CodecError, Decoder, Encoder};
use leptos::ev::messageerror;
use leptos::prelude::*;
use thiserror::Error;
use wasm_bindgen::JsValue;

/// Reactive [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel).
///
/// Closes a broadcast channel automatically when the component is cleaned up.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_broadcast_channel)
///
/// ## Usage
///
/// The BroadcastChannel interface represents a named channel that any browsing context of a given origin can subscribe to. It allows communication between different documents (in different windows, tabs, frames, or iframes) of the same origin.
///
/// Messages are broadcasted via a message event fired at all BroadcastChannel objects listening to the channel.
///
/// ```
/// # use leptos::prelude::*;
/// # use leptos_use::{use_broadcast_channel, UseBroadcastChannelReturn};
/// # use codee::string::FromToStringCodec;
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let UseBroadcastChannelReturn {
///     is_supported,
///     message,
///     post,
///     error,
///     close,
///     ..
/// } = use_broadcast_channel::<bool, FromToStringCodec>("some-channel-name");
///
/// post(&true);
///
/// close();
/// #
/// # view! { }
/// # }
/// ```
///
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
/// binary codec wrapped in `Base64`.
///
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
/// > available and what feature flags they require.
///
/// ```
/// # use leptos::prelude::*;
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::use_broadcast_channel;
/// # use codee::string::JsonSerdeCodec;
/// #
/// // Data sent in JSON must implement Serialize, Deserialize:
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
/// pub struct MyState {
///     pub playing_lego: bool,
///     pub everything_is_awesome: String,
/// }
///
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// use_broadcast_channel::<MyState, JsonSerdeCodec>("everyting-is-awesome");
/// # view! { }
/// # }
/// ```
///
/// ## SendWrapped Return
///
/// The returned closures `post` and `close` are sendwrapped functions. They can
/// only be called from the same thread that called `use_broadcast_channel`.
pub fn use_broadcast_channel<T, C>(
    name: &str,
) -> UseBroadcastChannelReturn<
    T,
    impl Fn(&T) + Clone + Send + Sync,
    impl Fn() + Clone + Send + Sync,
    C,
>
where
    T: Send + Sync,
    C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str> + Send + Sync,
    <C as Encoder<T>>::Error: Send + Sync,
    <C as Decoder<T>>::Error: Send + Sync,
{
    let is_supported = use_supported(|| js!("BroadcastChannel" in &window()));

    let (is_closed, set_closed) = signal(false);
    let (channel, set_channel) = signal_local(None::<web_sys::BroadcastChannel>);
    let (message, set_message) = signal(None::<T>);
    let (error, set_error) = signal_local(
        None::<UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>>,
    );

    let post = {
        sendwrap_fn!(move |data: &T| {
            if let Some(channel) = channel.get_untracked() {
                match C::encode(data) {
                    Ok(msg) => {
                        channel
                            .post_message(&msg.into())
                            .map_err(|err| {
                                set_error.set(Some(UseBroadcastChannelError::PostMessage(err)))
                            })
                            .ok();
                    }
                    Err(err) => {
                        set_error.set(Some(UseBroadcastChannelError::Codec(CodecError::Encode(
                            err,
                        ))));
                    }
                }
            }
        })
    };

    let close = {
        sendwrap_fn!(move || {
            if let Some(channel) = channel.get_untracked() {
                channel.close();
            }
            set_closed.set(true);
        })
    };

    if is_supported.get_untracked() {
        let channel_val = web_sys::BroadcastChannel::new(name).ok();
        set_channel.set(channel_val.clone());

        if let Some(channel) = channel_val {
            let _ = use_event_listener_with_options(
                channel.clone(),
                leptos::ev::message,
                move |event| {
                    if let Some(data) = event.data().as_string() {
                        match C::decode(&data) {
                            Ok(msg) => {
                                set_message.set(Some(msg));
                            }
                            Err(err) => set_error.set(Some(UseBroadcastChannelError::Codec(
                                CodecError::Decode(err),
                            ))),
                        }
                    } else {
                        set_error.set(Some(UseBroadcastChannelError::ValueNotString));
                    }
                },
                UseEventListenerOptions::default().passive(true),
            );

            let _ = use_event_listener_with_options(
                channel.clone(),
                messageerror,
                move |event| {
                    set_error.set(Some(UseBroadcastChannelError::MessageEvent(event)));
                },
                UseEventListenerOptions::default().passive(true),
            );

            let _ = use_event_listener(channel, leptos::ev::close, move |_| set_closed.set(true));
        }
    }

    on_cleanup({
        let close = close.clone();

        move || {
            close();
        }
    });

    UseBroadcastChannelReturn {
        is_supported,
        channel: channel.into(),
        message: message.into(),
        post,
        close,
        error: error.into(),
        is_closed: is_closed.into(),
    }
}

/// Return type of [`use_broadcast_channel`].
pub struct UseBroadcastChannelReturn<T, PFn, CFn, C>
where
    T: Send + Sync + 'static,
    PFn: Fn(&T) + Clone,
    CFn: Fn() + Clone,
    C: Encoder<T> + Decoder<T> + Send + Sync,
{
    /// `true` if this browser supports `BroadcastChannel`s.
    pub is_supported: Signal<bool>,

    /// The broadcast channel that is wrapped by this function
    pub channel: Signal<Option<web_sys::BroadcastChannel>, LocalStorage>,

    /// Latest message received from the channel
    pub message: Signal<Option<T>>,

    /// Sends a message through the channel
    pub post: PFn,

    /// Closes the channel
    pub close: CFn,

    /// Latest error as reported by the `messageerror` event.
    pub error: Signal<Option<ErrorType<T, C>>, LocalStorage>,

    /// Wether the channel is closed
    pub is_closed: Signal<bool>,
}

type ErrorType<T, C> = UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>;

#[derive(Debug, Error)]
pub enum UseBroadcastChannelError<E, D> {
    #[error("failed to post message")]
    PostMessage(JsValue),
    #[error("channel message error")]
    MessageEvent(web_sys::MessageEvent),
    #[error("failed to (de)encode value")]
    Codec(CodecError<E, D>),
    #[error("received value is not a string")]
    ValueNotString,
}