diff --git a/Cargo.toml b/Cargo.toml
index 80cc532..f6db118 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ lazy_static = "1"
version = "0.3"
features = [
"CssStyleDeclaration",
+ "CloseEvent",
"CustomEvent",
"CustomEventInit",
"DomRectReadOnly",
@@ -63,6 +64,7 @@ features = [
docs = []
math = ["num"]
storage = ["serde", "serde_json", "web-sys/StorageEvent"]
+websocket = ["web-sys/BinaryType", "web-sys/WebSocket"]
[package.metadata.docs.rs]
all-features = true
diff --git a/README.md b/README.md
index 8d97917..43d2f36 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ cargo test --all-features
First you need to install
```shell
-cargo install mdbook-cmdrun trunk
+cargo install mdbook mdbook-cmdrun trunk
```
To build the book go in your terminal into the docs/book folder
diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md
index 9941910..8e38c3d 100644
--- a/docs/book/src/SUMMARY.md
+++ b/docs/book/src/SUMMARY.md
@@ -11,6 +11,10 @@
- [use_session_storage](storage/use_session_storage.md)
- [use_storage](storage/use_storage.md)
+# @WebSocket
+
+- [use_websocket](websocket/use_websocket.md)
+
# Elements
- [use_active_element](elements/use_active_element.md)
@@ -68,4 +72,4 @@
- [use_floor](math/use_floor.md)
- [use_max](math/use_max.md)
- [use_min](math/use_min.md)
-- [use_round](math/use_round.md)
\ No newline at end of file
+- [use_round](math/use_round.md)
diff --git a/docs/book/src/websocket/use_websocket.md b/docs/book/src/websocket/use_websocket.md
new file mode 100644
index 0000000..389c51c
--- /dev/null
+++ b/docs/book/src/websocket/use_websocket.md
@@ -0,0 +1,3 @@
+# use_websocket
+
+
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index 3e8bfcd..3374a1a 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -29,6 +29,7 @@ members = [
"use_scroll",
"use_storage",
"use_throttle_fn",
+ "use_websocket",
"use_window_focus",
"use_window_scroll",
"watch_debounced",
diff --git a/examples/use_websocket/Cargo.toml b/examples/use_websocket/Cargo.toml
new file mode 100644
index 0000000..0b2c27b
--- /dev/null
+++ b/examples/use_websocket/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "use_websocket"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.4", features = ["nightly", "csr"] }
+console_error_panic_hook = "0.1"
+console_log = "1"
+log = "0.4"
+leptos-use = { path = "../..", features = ["docs", "websocket"] }
+web-sys = "0.3"
+
+[dev-dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-test = "0.3.0"
diff --git a/examples/use_websocket/README.md b/examples/use_websocket/README.md
new file mode 100644
index 0000000..7030fa4
--- /dev/null
+++ b/examples/use_websocket/README.md
@@ -0,0 +1,22 @@
+A simple example for `use_websocket`.
+
+If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
+as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
+
+```bash
+cargo install trunk
+rustup toolchain install nightly
+rustup target add wasm32-unknown-unknown
+```
+
+Then, open two terminals. In the first one, run:
+
+```
+npx tailwindcss -i ./input.css -o ./style/output.css --watch
+```
+
+In the second one, run:
+
+```bash
+trunk serve --open
+```
diff --git a/examples/use_websocket/Trunk.toml b/examples/use_websocket/Trunk.toml
new file mode 100644
index 0000000..962e040
--- /dev/null
+++ b/examples/use_websocket/Trunk.toml
@@ -0,0 +1,2 @@
+[build]
+public_url = "/demo/"
diff --git a/examples/use_websocket/index.html b/examples/use_websocket/index.html
new file mode 100644
index 0000000..ae249a6
--- /dev/null
+++ b/examples/use_websocket/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/examples/use_websocket/input.css b/examples/use_websocket/input.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/examples/use_websocket/input.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/examples/use_websocket/rust-toolchain.toml b/examples/use_websocket/rust-toolchain.toml
new file mode 100644
index 0000000..5d56faf
--- /dev/null
+++ b/examples/use_websocket/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs
new file mode 100644
index 0000000..e5a6eb7
--- /dev/null
+++ b/examples/use_websocket/src/main.rs
@@ -0,0 +1,211 @@
+use leptos::*;
+use leptos_use::docs::demo_or_body;
+use leptos_use::websocket::*;
+
+use web_sys::{CloseEvent, Event};
+
+#[component]
+fn Demo(cx: Scope) -> impl IntoView {
+ let (history, set_history) = create_signal(cx, vec![]);
+
+ fn update_history(&history: &WriteSignal>, message: String) {
+ let _ = &history.update(|history: &mut Vec<_>| history.push(message));
+ }
+ // ----------------------------
+ // use_websocket
+ // ----------------------------
+
+ let UseWebsocketReturn {
+ ready_state,
+ message,
+ message_bytes,
+ send,
+ send_bytes,
+ open,
+ close,
+ ..
+ } = use_websocket(cx, "wss://echo.websocket.events/".to_string());
+
+ let send_message = move |_| {
+ let m = "Hello, world!".to_string();
+ send(m.clone());
+ set_history.update(|history: &mut Vec<_>| history.push(format! {"[send]: {:?}", m}));
+ };
+
+ let send_byte_message = move |_| {
+ let m = b"Hello, world!\r\n".to_vec();
+ send_bytes(m.clone());
+ set_history.update(|history: &mut Vec<_>| history.push(format! {"[send_bytes]: {:?}", m}));
+ };
+
+ let status = move || ready_state().to_string();
+
+ let connected = move || ready_state.get() == UseWebSocketReadyState::Open;
+
+ let open_connection = move |_| {
+ open();
+ };
+ let close_connection = move |_| {
+ close();
+ };
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message.get() {
+ update_history(&set_history, format! {"[message]: {:?}", m});
+ };
+ });
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message_bytes.get() {
+ update_history(&set_history, format! {"[message_bytes]: {:?}", m});
+ };
+ });
+
+ // ----------------------------
+ // use_websocket_with_options
+ // ----------------------------
+
+ let (history2, set_history2) = create_signal(cx, vec![]);
+
+ let on_open_callback = move |e: Event| {
+ set_history2.update(|history: &mut Vec<_>| {
+ history.push(format! {"[onopen]: event {:?}", e.type_()})
+ });
+ };
+
+ let on_close_callback = move |e: CloseEvent| {
+ set_history2.update(|history: &mut Vec<_>| {
+ history.push(format! {"[onclose]: event {:?}", e.type_()})
+ });
+ };
+
+ let on_error_callback = move |e: Event| {
+ set_history2.update(|history: &mut Vec<_>| {
+ history.push(format! {"[onerror]: event {:?}", e.type_()})
+ });
+ };
+
+ let on_message_callback = move |m: String| {
+ set_history2.update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m}));
+ };
+
+ let on_message_bytes_callback = move |m: Vec| {
+ set_history2
+ .update(|history: &mut Vec<_>| history.push(format! {"[onmessage_bytes]: {:?}", m}));
+ };
+
+ let UseWebsocketReturn {
+ ready_state: ready_state2,
+ send: send2,
+ send_bytes: send_bytes2,
+ open: open2,
+ close: close2,
+ message: message2,
+ message_bytes: message_bytes2,
+ ..
+ } = use_websocket_with_options(
+ cx,
+ "wss://echo.websocket.events/".to_string(),
+ UseWebSocketOptions::default()
+ .manual(true)
+ .on_open(on_open_callback.clone())
+ .on_close(on_close_callback.clone())
+ .on_error(on_error_callback.clone())
+ .on_message(on_message_callback.clone())
+ .on_message_bytes(on_message_bytes_callback.clone()),
+ );
+
+ let open_connection2 = move |_| {
+ open2();
+ };
+ let close_connection2 = move |_| {
+ close2();
+ };
+
+ let send_message2 = move |_| {
+ let message = "Hello, use_leptos!".to_string();
+ send2(message.clone());
+ update_history(&set_history2, format! {"[send]: {:?}", message});
+ };
+
+ let send_byte_message2 = move |_| {
+ let m = b"Hello, world!\r\n".to_vec();
+ send_bytes2(m.clone());
+ update_history(&set_history2, format! {"[send_bytes]: {:?}", m});
+ };
+
+ let status2 = move || ready_state2.get().to_string();
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message2.get() {
+ update_history(&set_history2, format! {"[message]: {:?}", m});
+ };
+ });
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message_bytes2.get() {
+ update_history(&set_history2, format! {"[message_bytes]: {:?}", m});
+ };
+ });
+
+ let connected2 = move || ready_state2.get() == UseWebSocketReadyState::Open;
+
+ view! { cx,
+
+
+
+
"use_websocket"
+
"status: " {status}
+
+
+
+
+
+
"History"
+
+
+
{message} }
+ }
+ />
+
+
+
+
"use_websocket_with_options"
+
"status: " {status2}
+
+
+
+
+
+
"History"
+
+
+
+
+
+
+
+
+ }
+}
+
+fn main() {
+ _ = console_log::init_with_level(log::Level::Info);
+ console_error_panic_hook::set_once();
+
+ mount_to(demo_or_body(), |cx| {
+ view! { cx, }
+ })
+}
diff --git a/examples/use_websocket/style/output.css b/examples/use_websocket/style/output.css
new file mode 100644
index 0000000..c5dc82c
--- /dev/null
+++ b/examples/use_websocket/style/output.css
@@ -0,0 +1,210 @@
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia:
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia:
+}
+
+.container {
+ width: 100%
+}
+
+@media (min-width: 640px) {
+ .container {
+ max-width: 640px
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 768px
+ }
+}
+
+@media (min-width: 1024px) {
+ .container {
+ max-width: 1024px
+ }
+}
+
+@media (min-width: 1280px) {
+ .container {
+ max-width: 1280px
+ }
+}
+
+@media (min-width: 1536px) {
+ .container {
+ max-width: 1536px
+ }
+}
+
+.static {
+ position: static
+}
+
+.mb-2 {
+ margin-bottom: 0.5rem
+}
+
+.mr-2 {
+ margin-right: 0.5rem
+}
+
+.flex {
+ display: flex
+}
+
+.w-full {
+ width: 100%
+}
+
+.flex-col {
+ flex-direction: column
+}
+
+.items-center {
+ align-items: center
+}
+
+.gap-4 {
+ gap: 1rem
+}
+
+.text-2xl {
+ font-size: 1.5rem;
+ line-height: 2rem
+}
+
+.text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem
+}
+
+.text-\[--brand-color\] {
+ color: var(--brand-color)
+}
+
+.text-green-600 {
+ --tw-text-opacity: 1;
+ color: rgb(22 163 74 / var(--tw-text-opacity))
+}
+
+.opacity-75 {
+ opacity: 0.75
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:text-green-500 {
+ --tw-text-opacity: 1;
+ color: rgb(34 197 94 / var(--tw-text-opacity))
+ }
+}
+
+@media (min-width: 1024px) {
+ .lg\:w-1\/2 {
+ width: 50%
+ }
+
+ .lg\:flex-row {
+ flex-direction: row
+ }
+
+ .lg\:text-4xl {
+ font-size: 2.25rem;
+ line-height: 2.5rem
+ }
+}
diff --git a/examples/use_websocket/tailwind.config.js b/examples/use_websocket/tailwind.config.js
new file mode 100644
index 0000000..c5f4159
--- /dev/null
+++ b/examples/use_websocket/tailwind.config.js
@@ -0,0 +1,13 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: {
+ files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
+ },
+ theme: {
+ extend: {},
+ },
+ corePlugins: {
+ preflight: false,
+ },
+ plugins: [],
+}
diff --git a/src/lib.rs b/src/lib.rs
index 9134db4..23e7d23 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,6 +9,8 @@ pub mod math;
#[cfg(feature = "storage")]
pub mod storage;
pub mod utils;
+#[cfg(feature = "websocket")]
+pub mod websocket;
#[cfg(web_sys_unstable_apis)]
mod use_element_size;
diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs
new file mode 100644
index 0000000..ef6e33b
--- /dev/null
+++ b/src/websocket/mod.rs
@@ -0,0 +1,3 @@
+mod use_websocket;
+
+pub use use_websocket::*;
diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs
new file mode 100644
index 0000000..14b002b
--- /dev/null
+++ b/src/websocket/use_websocket.rs
@@ -0,0 +1,435 @@
+use leptos::{leptos_dom::helpers::TimeoutHandle, *};
+
+use core::fmt;
+use std::rc::Rc;
+use std::time::Duration;
+
+use default_struct_builder::DefaultBuilder;
+use js_sys::Array;
+use wasm_bindgen::{prelude::*, JsCast, JsValue};
+use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
+
+use crate::utils::CloneableFnWithArg;
+
+/// Creating and managing a [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connection.
+///
+/// ## Demo
+///
+/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_websocket)
+///
+/// ## Usage
+///
+/// ```
+/// # use leptos::*;
+/// # use leptos_use::websocket::*;
+/// #
+/// # #[component]
+/// # fn Demo(cx: Scope) -> impl IntoView {
+/// let UseWebsocketReturn {
+/// ready_state,
+/// message,
+/// message_bytes,
+/// send,
+/// send_bytes,
+/// open,
+/// close,
+/// ..
+/// } = use_websocket(cx, "wss://echo.websocket.events/".to_string());
+///
+/// let send_message = move |_| {
+/// let m = "Hello, world!".to_string();
+/// send(m.clone());
+/// };
+///
+/// let send_byte_message = move |_| {
+/// let m = b"Hello, world!\r\n".to_vec();
+/// send_bytes(m.clone());
+/// };
+///
+/// let status = move || ready_state().to_string();
+///
+/// let connected = move || ready_state.get() == UseWebSocketReadyState::Open;
+///
+/// let open_connection = move |_| {
+/// open();
+/// };
+///
+/// let close_connection = move |_| {
+/// close();
+/// };
+///
+/// view! { cx,
+///
+///
"status: " {status}
+/// button on:click=send_message disabled=move || !connected()>"Send"
+///
+///
+///
+///
"Receive message: " {format! {"{:?}", message}}
+///
"Receive byte message: " {format! {"{:?}", message_bytes}}
+///
+/// }
+/// # }
+/// ```
+// #[doc(cfg(feature = "websocket"))]
+pub fn use_websocket(
+ cx: Scope,
+ url: String,
+) -> UseWebsocketReturn<
+ impl Fn() + Clone + 'static,
+ impl Fn() + Clone + 'static,
+ impl Fn(String) + Clone + 'static,
+ impl Fn(Vec) + Clone + 'static,
+> {
+ use_websocket_with_options(cx, url, UseWebSocketOptions::default())
+}
+
+/// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use.
+// #[doc(cfg(feature = "websocket"))]
+pub fn use_websocket_with_options(
+ cx: Scope,
+ url: String,
+ options: UseWebSocketOptions,
+) -> UseWebsocketReturn<
+ impl Fn() + Clone + 'static,
+ impl Fn() + Clone + 'static,
+ impl Fn(String) + Clone + 'static,
+ impl Fn(Vec) + Clone,
+> {
+ let (ready_state, set_ready_state) = create_signal(cx, UseWebSocketReadyState::Closed);
+ let (message, set_message) = create_signal(cx, None);
+ let (message_bytes, set_message_bytes) = create_signal(cx, None);
+ let ws_ref: StoredValue