musli_web/yew022.rs
1//! Integration with yew `0.22.x`.
2//!
3//! [`Callback`] can be used wherever this crate takes a callback, and the
4//! [`ws::Handle`] of a service can be provided to every component in an
5//! application.
6//!
7//! # Providing the handle
8//!
9//! Build and connect the [`ws::Service`] in the root component and keep it for
10//! as long as the application runs. Wrap the component tree in a
11//! [`HandleProvider`] with its handle, and the components below it can get the
12//! handle without it being passed through their properties:
13//!
14//! * [`use_handle`] gets the handle in a function component.
15//! * [`use_request`] keeps a [`ws::Request`] for as long as the component is
16//! mounted, cancelling the previous one when a new one is stored.
17//! * [`use_requests`] keeps any number of [`ws::Request`]s in flight at once,
18//! and [`use_keyed_requests`] keeps at most one for each key.
19//! * [`use_channel`] opens a [`ws::Channel`] for as long as the component is
20//! mounted, and renders the component again whenever it opens or closes.
21//! * [`use_open`] follows whether the connection is open, so that what a
22//! component shows can be fetched again after a reconnect.
23//! * [`ContextExt::handle`] gets the handle in a struct component.
24//!
25//! The hooks and [`ContextExt::handle`] panic with a message naming
26//! [`HandleProvider`] if the component is not below one.
27//!
28//! [`Callback`]: yew022::Callback
29//! [`ws::Handle`]: crate::web::Handle
30//! [`ws::Service`]: crate::web::Service
31//! [`ws::Request`]: crate::web::Request
32//! [`ws::Channel`]: crate::web::Channel
33//!
34//! ```
35//! # extern crate yew022 as yew;
36//! use yew::prelude::*;
37//! use musli_web::web03::prelude::*;
38//! use musli_web::yew022::{HandleProvider, use_handle, use_open};
39//!
40//! #[function_component(App)]
41//! fn app() -> Html {
42//! // The service is built once and kept for as long as the app runs.
43//! let service = use_memo((), |_| {
44//! let service = ws::connect(ws::Connect::location("ws"))
45//! .on_error(|error| tracing::error!("WebSocket error: {error}"))
46//! .build();
47//!
48//! service.connect();
49//! service
50//! });
51//!
52//! html! {
53//! <HandleProvider context={service.handle().clone()}>
54//! <Status />
55//! </HandleProvider>
56//! }
57//! }
58//!
59//! #[function_component(Status)]
60//! fn status() -> Html {
61//! let handle = use_handle();
62//! let open = use_open();
63//!
64//! match open {
65//! Some(generation) => html!(<p>{format!("Connection #{generation} uses {:?}", handle.format())}</p>),
66//! None => html!(<p>{"Disconnected"}</p>),
67//! }
68//! }
69//! ```
70//!
71//! # Struct components
72//!
73//! This example uses [`yew022`] with a struct component which owns the
74//! service itself:
75//!
76//! [`yew022`]: crate::yew022
77//!
78//! ```
79//! # extern crate yew022 as yew;
80//! # extern crate web_sys03 as web_sys;
81//! use web_sys::HtmlInputElement;
82//! use yew::prelude::*;
83//! use musli_web::web03::prelude::*;
84//!
85//! mod api {
86//! use musli::{Decode, Encode};
87//! use musli_web::api;
88//!
89//! #[derive(Encode, Decode)]
90//! pub struct HelloRequest<'de> {
91//! pub message: &'de str,
92//! }
93//!
94//! #[derive(Encode, Decode)]
95//! pub struct HelloResponse<'de> {
96//! pub message: &'de str,
97//! }
98//!
99//! #[derive(Encode, Decode)]
100//! pub struct TickEvent<'de> {
101//! pub message: &'de str,
102//! pub tick: u32,
103//! }
104//!
105//! api::define! {
106//! pub type Hello;
107//!
108//! impl Endpoint for Hello {
109//! impl<'de> Request for HelloRequest<'de>;
110//! type Response<'de> = HelloResponse<'de>;
111//! }
112//!
113//! pub type Tick;
114//!
115//! impl Broadcast for Tick {
116//! impl<'de> Event for TickEvent<'de>;
117//! }
118//! }
119//! }
120//!
121//! enum Msg {
122//! Error(ws::Error),
123//! Change(String),
124//! Send,
125//! HelloResponse(Result<ws::Packet<api::Hello>, ws::Error>),
126//! Tick(Result<ws::Packet<api::Tick>, ws::Error>),
127//! }
128//!
129//! struct App {
130//! service: ws::Service,
131//! _listen: ws::Listener,
132//! request: ws::Request,
133//! text: String,
134//! tick: u32,
135//! responses: Vec<String>,
136//! }
137//!
138//! impl Component for App {
139//! type Message = Msg;
140//! type Properties = ();
141//!
142//! fn create(ctx: &Context<Self>) -> Self {
143//! let service = ws::connect(ws::Connect::location("ws"))
144//! .on_error(ctx.link().callback(Msg::Error))
145//! .build();
146//!
147//! service.connect();
148//!
149//! let listen = service.handle().on_broadcast(ctx.link().callback(Msg::Tick));
150//!
151//! Self {
152//! service,
153//! _listen: listen,
154//! request: ws::Request::new(),
155//! text: String::new(),
156//! tick: 0,
157//! responses: Vec::new(),
158//! }
159//! }
160//!
161//! fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
162//! match msg {
163//! Msg::Error(error) => {
164//! tracing::error!("WebSocket error: {:?}", error);
165//! false
166//! }
167//! Msg::Change(text) => {
168//! self.text = text;
169//! true
170//! }
171//! Msg::Send => {
172//! self.request = self
173//! .service
174//! .handle()
175//! .request()
176//! .body(api::HelloRequest {
177//! message: self.text.as_str(),
178//! })
179//! .on_packet(ctx.link().callback(Msg::HelloResponse))
180//! .send();
181//!
182//! self.text.clear();
183//! true
184//! }
185//! Msg::HelloResponse(Err(error)) => {
186//! tracing::error!("Request error: {:?}", error);
187//! false
188//! }
189//! Msg::HelloResponse(Ok(packet)) => {
190//! tracing::debug!("Got response");
191//!
192//! while !packet.is_empty() {
193//! let Ok(response) = packet.decode() else {
194//! break;
195//! };
196//!
197//! self.responses.push(response.message.to_owned());
198//! }
199//!
200//! true
201//! }
202//! Msg::Tick(Err(error)) => {
203//! tracing::error!("Tick error: {error}");
204//! false
205//! }
206//! Msg::Tick(Ok(packet)) => {
207//! tracing::debug!("Got tick");
208//!
209//! if let Ok(tick) = packet.decode_event() {
210//! self.tick = tick.tick;
211//! }
212//!
213//! true
214//! }
215//! }
216//! }
217//!
218//! fn view(&self, ctx: &Context<Self>) -> Html {
219//! let oninput = ctx.link().callback(|e: InputEvent| {
220//! let input = e.target_unchecked_into::<HtmlInputElement>();
221//! Msg::Change(input.value())
222//! });
223//!
224//! let onclick = ctx.link().callback(|_: MouseEvent| {
225//! Msg::Send
226//! });
227//!
228//! html! {
229//! <div class="container">
230//! <input type="text" {oninput} value={self.text.clone()} />
231//! <button {onclick}>{"Send Message"}</button>
232//! {for self.responses.iter().enumerate().map(|(index, response)| html!(<div>{format!("Response #{index}: {response}")}</div>))}
233//! <div>{format!("Global tick: {}", self.tick)}</div>
234//! </div>
235//! }
236//! }
237//! }
238//! ```
239
240yew_impl!(yew022);