karbon_framework/livewire/live_component.rs
1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4
5/// Escape a string for safe HTML output inside a LiveComponent's render().
6///
7/// ```ignore
8/// fn render(&self) -> String {
9/// format!("<span>{}</span>", html_escape(&self.user_input))
10/// }
11/// ```
12pub fn html_escape(s: &str) -> String {
13 s.replace('&', "&")
14 .replace('<', "<")
15 .replace('>', ">")
16 .replace('"', """)
17 .replace('\'', "'")
18}
19
20/// Trait for server-rendered live components.
21///
22/// A LiveComponent holds state, renders HTML, and handles events from the browser.
23/// When an event is received via WebSocket, `handle_event` is called, the state
24/// is updated, and `render` produces new HTML that is sent back to the client.
25///
26/// **SECURITY**: The HTML returned by `render()` is sent directly to the browser.
27/// Always use `html_escape()` for any user-provided content to prevent XSS.
28///
29/// ```ignore
30/// struct Counter {
31/// count: i32,
32/// }
33///
34/// impl LiveComponent for Counter {
35/// fn render(&self) -> String {
36/// format!(r#"
37/// <div>
38/// <span>{}</span>
39/// <button lw-click="increment">+</button>
40/// <button lw-click="decrement">-</button>
41/// </div>
42/// "#, self.count)
43/// }
44///
45/// fn handle_event<'a>(
46/// &'a mut self,
47/// event: &'a str,
48/// _params: &'a HashMap<String, String>,
49/// ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
50/// Box::pin(async move {
51/// match event {
52/// "increment" => self.count += 1,
53/// "decrement" => self.count -= 1,
54/// _ => {}
55/// }
56/// })
57/// }
58/// }
59/// ```
60pub trait LiveComponent: Send + Sync + 'static {
61 /// Render the component to an HTML string
62 fn render(&self) -> String;
63
64 /// Handle an event from the browser, mutating state as needed
65 fn handle_event<'a>(
66 &'a mut self,
67 event: &'a str,
68 params: &'a HashMap<String, String>,
69 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
70
71 /// Called once when the component is first mounted (optional)
72 fn mount(&mut self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
73 Box::pin(async {})
74 }
75}