Skip to main content

musli_web/
yew023.rs

1//! Integration with yew `0.23.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`]: yew023::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 yew023 as yew;
36//! use yew::prelude::*;
37//! use musli_web::web03::prelude::*;
38//! use musli_web::yew023::{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 [`yew023`] with a struct component which owns the
74//! service itself:
75//!
76//! [`yew023`]: crate::yew023
77//!
78//! ```
79//! # extern crate yew023 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!(yew023);