mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-01-23 09:09:21 -05:00
commit
ddbc1c1db5
17 changed files with 938 additions and 2 deletions
|
@ -27,6 +27,7 @@ lazy_static = "1"
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
"CssStyleDeclaration",
|
"CssStyleDeclaration",
|
||||||
|
"CloseEvent",
|
||||||
"CustomEvent",
|
"CustomEvent",
|
||||||
"CustomEventInit",
|
"CustomEventInit",
|
||||||
"DomRectReadOnly",
|
"DomRectReadOnly",
|
||||||
|
@ -63,6 +64,7 @@ features = [
|
||||||
docs = []
|
docs = []
|
||||||
math = ["num"]
|
math = ["num"]
|
||||||
storage = ["serde", "serde_json", "web-sys/StorageEvent"]
|
storage = ["serde", "serde_json", "web-sys/StorageEvent"]
|
||||||
|
websocket = ["web-sys/BinaryType", "web-sys/WebSocket"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|
|
@ -57,7 +57,7 @@ cargo test --all-features
|
||||||
First you need to install
|
First you need to install
|
||||||
|
|
||||||
```shell
|
```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
|
To build the book go in your terminal into the docs/book folder
|
||||||
|
|
|
@ -11,6 +11,10 @@
|
||||||
- [use_session_storage](storage/use_session_storage.md)
|
- [use_session_storage](storage/use_session_storage.md)
|
||||||
- [use_storage](storage/use_storage.md)
|
- [use_storage](storage/use_storage.md)
|
||||||
|
|
||||||
|
# @WebSocket
|
||||||
|
|
||||||
|
- [use_websocket](websocket/use_websocket.md)
|
||||||
|
|
||||||
# Elements
|
# Elements
|
||||||
|
|
||||||
- [use_active_element](elements/use_active_element.md)
|
- [use_active_element](elements/use_active_element.md)
|
||||||
|
@ -68,4 +72,4 @@
|
||||||
- [use_floor](math/use_floor.md)
|
- [use_floor](math/use_floor.md)
|
||||||
- [use_max](math/use_max.md)
|
- [use_max](math/use_max.md)
|
||||||
- [use_min](math/use_min.md)
|
- [use_min](math/use_min.md)
|
||||||
- [use_round](math/use_round.md)
|
- [use_round](math/use_round.md)
|
||||||
|
|
3
docs/book/src/websocket/use_websocket.md
Normal file
3
docs/book/src/websocket/use_websocket.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# use_websocket
|
||||||
|
|
||||||
|
<!-- cmdrun python3 ../extract_doc_comment.py websocket/use_websocket websocket -->
|
|
@ -29,6 +29,7 @@ members = [
|
||||||
"use_scroll",
|
"use_scroll",
|
||||||
"use_storage",
|
"use_storage",
|
||||||
"use_throttle_fn",
|
"use_throttle_fn",
|
||||||
|
"use_websocket",
|
||||||
"use_window_focus",
|
"use_window_focus",
|
||||||
"use_window_scroll",
|
"use_window_scroll",
|
||||||
"watch_debounced",
|
"watch_debounced",
|
||||||
|
|
16
examples/use_websocket/Cargo.toml
Normal file
16
examples/use_websocket/Cargo.toml
Normal file
|
@ -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"
|
22
examples/use_websocket/README.md
Normal file
22
examples/use_websocket/README.md
Normal file
|
@ -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
|
||||||
|
```
|
2
examples/use_websocket/Trunk.toml
Normal file
2
examples/use_websocket/Trunk.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
public_url = "/demo/"
|
7
examples/use_websocket/index.html
Normal file
7
examples/use_websocket/index.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link data-trunk rel="css" href="style/output.css">
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
3
examples/use_websocket/input.css
Normal file
3
examples/use_websocket/input.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
2
examples/use_websocket/rust-toolchain.toml
Normal file
2
examples/use_websocket/rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
211
examples/use_websocket/src/main.rs
Normal file
211
examples/use_websocket/src/main.rs
Normal file
|
@ -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<Vec<String>>, 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<u8>| {
|
||||||
|
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,
|
||||||
|
<div class="container">
|
||||||
|
<div class="flex flex-col lg:flex-row gap-4">
|
||||||
|
<div class="w-full lg:w-1/2">
|
||||||
|
<h1 class="text-xl lg:text-4xl mb-2">"use_websocket"</h1>
|
||||||
|
<p>"status: " {status}</p>
|
||||||
|
<button on:click=send_message disabled=move || !connected()>"Send"</button>
|
||||||
|
<button on:click=send_byte_message disabled=move || !connected()>"Send bytes"</button>
|
||||||
|
<button on:click=open_connection disabled=connected>"Open"</button>
|
||||||
|
<button on:click=close_connection disabled=move || !connected()>"Close"</button>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h3 class="text-2xl mr-2">"History"</h3>
|
||||||
|
<button on:click=move |_| set_history(vec![]) disabled=move || history.get().len() <= 0>"Clear"</button>
|
||||||
|
</div>
|
||||||
|
<For
|
||||||
|
each=move || history.get().into_iter().enumerate()
|
||||||
|
key=|(index, _)| *index
|
||||||
|
view=move |cx, (_, message)| {
|
||||||
|
view! {cx, <div>{message}</div> }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="w-full lg:w-1/2">
|
||||||
|
<h1 class="text-xl lg:text-4xl mb-2">"use_websocket_with_options"</h1>
|
||||||
|
<p>"status: " {status2}</p>
|
||||||
|
<button on:click=open_connection2 disabled={connected2}>"Connect"</button>
|
||||||
|
<button on:click=close_connection2 disabled=move || !connected2()>"Close"</button>
|
||||||
|
<button on:click=send_message2 disabled=move || !connected2()>"Send"</button>
|
||||||
|
<button on:click=send_byte_message2 disabled=move || !connected2()>"Send Bytes"</button>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h3 class="text-2xl mr-2">"History"</h3>
|
||||||
|
<button on:click=move |_| set_history2(vec![]) disabled=move || history2.get().len() <= 0>"Clear"</button>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<For
|
||||||
|
each=move || history2.get().into_iter().enumerate()
|
||||||
|
key=|(index, _)| *index
|
||||||
|
view=move |cx, (_, message)| {
|
||||||
|
view! {cx, <li>{message}</li> }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
_ = console_log::init_with_level(log::Level::Info);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
mount_to(demo_or_body(), |cx| {
|
||||||
|
view! { cx, <Demo /> }
|
||||||
|
})
|
||||||
|
}
|
210
examples/use_websocket/style/output.css
Normal file
210
examples/use_websocket/style/output.css
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
13
examples/use_websocket/tailwind.config.js
Normal file
13
examples/use_websocket/tailwind.config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: {
|
||||||
|
files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false,
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ pub mod math;
|
||||||
#[cfg(feature = "storage")]
|
#[cfg(feature = "storage")]
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
#[cfg(feature = "websocket")]
|
||||||
|
pub mod websocket;
|
||||||
|
|
||||||
#[cfg(web_sys_unstable_apis)]
|
#[cfg(web_sys_unstable_apis)]
|
||||||
mod use_element_size;
|
mod use_element_size;
|
||||||
|
|
3
src/websocket/mod.rs
Normal file
3
src/websocket/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod use_websocket;
|
||||||
|
|
||||||
|
pub use use_websocket::*;
|
435
src/websocket/use_websocket.rs
Normal file
435
src/websocket/use_websocket.rs
Normal file
|
@ -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,
|
||||||
|
/// <div>
|
||||||
|
/// <p>"status: " {status}</p>
|
||||||
|
/// button on:click=send_message disabled=move || !connected()>"Send"</button>
|
||||||
|
/// <button on:click=send_byte_message disabled=move || !connected()>"Send bytes"</button>
|
||||||
|
/// <button on:click=open_connection disabled=connected>"Open"</button>
|
||||||
|
/// <button on:click=close_connection disabled=move || !connected()>"Close"</button>
|
||||||
|
/// <p>"Receive message: " {format! {"{:?}", message}}</p>
|
||||||
|
/// <p>"Receive byte message: " {format! {"{:?}", message_bytes}}</p>
|
||||||
|
/// </div>
|
||||||
|
/// }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
// #[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<u8>) + 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<u8>) + 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<Option<WebSocket>> = store_value(cx, None);
|
||||||
|
|
||||||
|
let on_open_ref = store_value(cx, options.on_open);
|
||||||
|
let on_message_ref = store_value(cx, options.on_message);
|
||||||
|
let on_message_bytes_ref = store_value(cx, options.on_message_bytes);
|
||||||
|
let on_error_ref = store_value(cx, options.on_error);
|
||||||
|
let on_close_ref = store_value(cx, options.on_close);
|
||||||
|
|
||||||
|
let reconnect_limit = options.reconnect_limit.unwrap_or(3);
|
||||||
|
let reconnect_interval = options.reconnect_interval.unwrap_or(3 * 1000);
|
||||||
|
|
||||||
|
let reconnect_timer_ref: StoredValue<Option<TimeoutHandle>> = store_value(cx, None);
|
||||||
|
let manual = options.manual;
|
||||||
|
let protocols = options.protocols;
|
||||||
|
|
||||||
|
let reconnect_times_ref: StoredValue<u64> = store_value(cx, 0);
|
||||||
|
let unmounted_ref = store_value(cx, false);
|
||||||
|
|
||||||
|
let connect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None);
|
||||||
|
|
||||||
|
let reconnect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None);
|
||||||
|
reconnect_ref.set_value({
|
||||||
|
let ws = ws_ref.get_value();
|
||||||
|
Some(Rc::new(move || {
|
||||||
|
if reconnect_times_ref.get_value() < reconnect_limit
|
||||||
|
&& ws
|
||||||
|
.clone()
|
||||||
|
.map_or(false, |ws: WebSocket| ws.ready_state() != WebSocket::OPEN)
|
||||||
|
{
|
||||||
|
reconnect_timer_ref.set_value(
|
||||||
|
set_timeout_with_handle(
|
||||||
|
move || {
|
||||||
|
if let Some(connect) = connect_ref.get_value() {
|
||||||
|
connect();
|
||||||
|
reconnect_times_ref.update_value(|current| *current += 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Duration::from_millis(reconnect_interval),
|
||||||
|
)
|
||||||
|
.ok(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
connect_ref.set_value({
|
||||||
|
let ws = ws_ref.get_value();
|
||||||
|
let url = url.clone();
|
||||||
|
|
||||||
|
Some(Rc::new(move || {
|
||||||
|
reconnect_timer_ref.set_value(None);
|
||||||
|
{
|
||||||
|
if let Some(web_socket) = &ws {
|
||||||
|
let _ = web_socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let web_socket = {
|
||||||
|
protocols.as_ref().map_or_else(
|
||||||
|
|| WebSocket::new(&url).unwrap_throw(),
|
||||||
|
|protocols| {
|
||||||
|
let array = protocols
|
||||||
|
.iter()
|
||||||
|
.map(|p| JsValue::from(p.clone()))
|
||||||
|
.collect::<Array>();
|
||||||
|
WebSocket::new_with_str_sequence(&url, &JsValue::from(&array))
|
||||||
|
.unwrap_throw()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
web_socket.set_binary_type(BinaryType::Arraybuffer);
|
||||||
|
set_ready_state.set(UseWebSocketReadyState::Connecting);
|
||||||
|
|
||||||
|
// onopen handler
|
||||||
|
{
|
||||||
|
let onopen_closure = Closure::wrap(Box::new(move |e: Event| {
|
||||||
|
if unmounted_ref.get_value() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback = on_open_ref.get_value();
|
||||||
|
callback(e);
|
||||||
|
|
||||||
|
set_ready_state.set(UseWebSocketReadyState::Open);
|
||||||
|
}) as Box<dyn FnMut(Event)>);
|
||||||
|
web_socket.set_onopen(Some(onopen_closure.as_ref().unchecked_ref()));
|
||||||
|
// Forget the closure to keep it alive
|
||||||
|
onopen_closure.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// onmessage handler
|
||||||
|
{
|
||||||
|
let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||||
|
if unmounted_ref.get_value() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.data().dyn_into::<js_sys::ArrayBuffer>().map_or_else(
|
||||||
|
|_| {
|
||||||
|
e.data().dyn_into::<js_sys::JsString>().map_or_else(
|
||||||
|
|_| {
|
||||||
|
unreachable!("message event, received Unknown: {:?}", e.data());
|
||||||
|
},
|
||||||
|
|txt| {
|
||||||
|
let txt = String::from(&txt);
|
||||||
|
let callback = on_message_ref.get_value();
|
||||||
|
callback(txt.clone());
|
||||||
|
|
||||||
|
set_message.set(Some(txt.clone()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|array_buffer| {
|
||||||
|
let array = js_sys::Uint8Array::new(&array_buffer);
|
||||||
|
let array = array.to_vec();
|
||||||
|
let callback = on_message_bytes_ref.get_value();
|
||||||
|
callback(array.clone());
|
||||||
|
|
||||||
|
set_message_bytes.set(Some(array));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
as Box<dyn FnMut(MessageEvent)>);
|
||||||
|
web_socket.set_onmessage(Some(onmessage_closure.as_ref().unchecked_ref()));
|
||||||
|
onmessage_closure.forget();
|
||||||
|
}
|
||||||
|
// onerror handler
|
||||||
|
{
|
||||||
|
let onerror_closure = Closure::wrap(Box::new(move |e: Event| {
|
||||||
|
if unmounted_ref.get_value() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reconnect) = &reconnect_ref.get_value() {
|
||||||
|
reconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback = on_error_ref.get_value();
|
||||||
|
callback(e);
|
||||||
|
|
||||||
|
set_ready_state.set(UseWebSocketReadyState::Closed);
|
||||||
|
}) as Box<dyn FnMut(Event)>);
|
||||||
|
web_socket.set_onerror(Some(onerror_closure.as_ref().unchecked_ref()));
|
||||||
|
onerror_closure.forget();
|
||||||
|
}
|
||||||
|
// onclose handler
|
||||||
|
{
|
||||||
|
let onclose_closure = Closure::wrap(Box::new(move |e: CloseEvent| {
|
||||||
|
if unmounted_ref.get_value() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reconnect) = &reconnect_ref.get_value() {
|
||||||
|
reconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback = on_close_ref.get_value();
|
||||||
|
callback(e);
|
||||||
|
|
||||||
|
set_ready_state.set(UseWebSocketReadyState::Closed);
|
||||||
|
})
|
||||||
|
as Box<dyn FnMut(CloseEvent)>);
|
||||||
|
web_socket.set_onclose(Some(onclose_closure.as_ref().unchecked_ref()));
|
||||||
|
onclose_closure.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
ws_ref.set_value(Some(web_socket));
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send text (String)
|
||||||
|
let send = {
|
||||||
|
Box::new(move |data: String| {
|
||||||
|
if ready_state.get() == UseWebSocketReadyState::Open {
|
||||||
|
if let Some(web_socket) = ws_ref.get_value() {
|
||||||
|
let _ = web_socket.send_with_str(&data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send bytes
|
||||||
|
let send_bytes = {
|
||||||
|
move |data: Vec<u8>| {
|
||||||
|
if ready_state.get() == UseWebSocketReadyState::Open {
|
||||||
|
if let Some(web_socket) = ws_ref.get_value() {
|
||||||
|
let _ = web_socket.send_with_u8_array(&data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open connection
|
||||||
|
let open = {
|
||||||
|
move || {
|
||||||
|
reconnect_times_ref.set_value(0);
|
||||||
|
if let Some(connect) = connect_ref.get_value() {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close connection
|
||||||
|
let close = {
|
||||||
|
reconnect_timer_ref.set_value(None);
|
||||||
|
move || {
|
||||||
|
reconnect_times_ref.set_value(reconnect_limit);
|
||||||
|
if let Some(web_socket) = ws_ref.get_value() {
|
||||||
|
let _ = web_socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open connection (not called if option `manual` is true)
|
||||||
|
{
|
||||||
|
let open = open.clone();
|
||||||
|
create_effect(cx, move |_| {
|
||||||
|
if !manual {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
|| ()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up (unmount)
|
||||||
|
{
|
||||||
|
let close = close.clone();
|
||||||
|
on_cleanup(cx, move || {
|
||||||
|
unmounted_ref.set_value(true);
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
UseWebsocketReturn {
|
||||||
|
ready_state,
|
||||||
|
message,
|
||||||
|
message_bytes,
|
||||||
|
ws: ws_ref.get_value(),
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
send,
|
||||||
|
send_bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current state of the `WebSocket` connection.
|
||||||
|
// #[doc(cfg(feature = "websocket"))]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum UseWebSocketReadyState {
|
||||||
|
Connecting,
|
||||||
|
Open,
|
||||||
|
Closing,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for UseWebSocketReadyState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
UseWebSocketReadyState::Connecting => write!(f, "Connecting"),
|
||||||
|
UseWebSocketReadyState::Open => write!(f, "Open"),
|
||||||
|
UseWebSocketReadyState::Closing => write!(f, "Closing"),
|
||||||
|
UseWebSocketReadyState::Closed => write!(f, "Closed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for [`use_websocket_with_options`].
|
||||||
|
// #[doc(cfg(feature = "websocket"))]
|
||||||
|
#[derive(DefaultBuilder)]
|
||||||
|
pub struct UseWebSocketOptions {
|
||||||
|
/// `WebSocket` connect callback.
|
||||||
|
on_open: Box<dyn CloneableFnWithArg<Event> + 'static>,
|
||||||
|
/// `WebSocket` message callback for text.
|
||||||
|
on_message: Box<dyn CloneableFnWithArg<String> + 'static>,
|
||||||
|
/// `WebSocket` message callback for binary.
|
||||||
|
on_message_bytes: Box<dyn CloneableFnWithArg<Vec<u8>> + 'static>,
|
||||||
|
/// `WebSocket` error callback.
|
||||||
|
on_error: Box<dyn CloneableFnWithArg<Event> + 'static>,
|
||||||
|
/// `WebSocket` close callback.
|
||||||
|
on_close: Box<dyn CloneableFnWithArg<CloseEvent> + 'static>,
|
||||||
|
/// Retry times.
|
||||||
|
reconnect_limit: Option<u64>,
|
||||||
|
/// Retry interval(ms).
|
||||||
|
reconnect_interval: Option<u64>,
|
||||||
|
/// Manually starts connection
|
||||||
|
manual: bool,
|
||||||
|
/// Sub protocols
|
||||||
|
protocols: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UseWebSocketOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
on_open: Box::new(|_| {}),
|
||||||
|
on_message: Box::new(|_| {}),
|
||||||
|
on_message_bytes: Box::new(|_| {}),
|
||||||
|
on_error: Box::new(|_| {}),
|
||||||
|
on_close: Box::new(|_| {}),
|
||||||
|
reconnect_limit: Some(3),
|
||||||
|
reconnect_interval: Some(3 * 1000),
|
||||||
|
manual: false,
|
||||||
|
protocols: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return type of [`use_websocket`].
|
||||||
|
// #[doc(cfg(feature = "websocket"))]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UseWebsocketReturn<OpenFn, CloseFn, SendFn, SendBytesFn>
|
||||||
|
where
|
||||||
|
OpenFn: Fn() + Clone + 'static,
|
||||||
|
CloseFn: Fn() + Clone + 'static,
|
||||||
|
SendFn: Fn(String) + Clone + 'static,
|
||||||
|
SendBytesFn: Fn(Vec<u8>) + Clone + 'static,
|
||||||
|
{
|
||||||
|
/// The current state of the `WebSocket` connection.
|
||||||
|
pub ready_state: ReadSignal<UseWebSocketReadyState>,
|
||||||
|
/// Latest text message received from `WebSocket`.
|
||||||
|
pub message: ReadSignal<Option<String>>,
|
||||||
|
/// Latest binary message received from `WebSocket`.
|
||||||
|
pub message_bytes: ReadSignal<Option<Vec<u8>>>,
|
||||||
|
/// The `WebSocket` instance.
|
||||||
|
pub ws: Option<WebSocket>,
|
||||||
|
/// Opens the `WebSocket` connection
|
||||||
|
pub open: OpenFn,
|
||||||
|
/// Closes the `WebSocket` connection
|
||||||
|
pub close: CloseFn,
|
||||||
|
/// Sends `text` (string) based data
|
||||||
|
pub send: SendFn,
|
||||||
|
/// Sends binary data
|
||||||
|
pub send_bytes: SendBytesFn,
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue