karbon_framework/livewire/
live_handler.rs1use axum::extract::ws::{Message, WebSocket};
2use axum::response::{Html, IntoResponse, Response};
3use futures::{SinkExt, StreamExt};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7use super::live_component::LiveComponent;
8
9#[derive(Debug, Deserialize)]
11struct ClientEvent {
12 event: String,
13 #[serde(default)]
14 params: HashMap<String, String>,
15}
16
17#[derive(Debug, Serialize)]
19struct ServerPatch {
20 html: String,
21}
22
23pub fn live_render(component: impl LiveComponent, ws_url: &str) -> Response {
44 let html = component.render();
45
46 let page = format!(
47 r#"<div id="lw-root">{html}</div>
48<script>{CLIENT_JS}</script>
49<script>LiveWire.connect("{ws_url}");</script>"#
50 );
51
52 Html(page).into_response()
53}
54
55pub async fn live_socket(socket: WebSocket, mut component: impl LiveComponent) {
60 component.mount().await;
61
62 let (mut tx, mut rx) = socket.split();
63
64 let html = component.render();
66 let patch = serde_json::to_string(&ServerPatch { html }).unwrap_or_default();
67 let _ = tx.send(Message::Text(patch.into())).await;
68
69 while let Some(Ok(msg)) = rx.next().await {
70 let text = match msg {
71 Message::Text(t) => t.to_string(),
72 Message::Close(_) => break,
73 _ => continue,
74 };
75
76 let Ok(event) = serde_json::from_str::<ClientEvent>(&text) else {
77 continue;
78 };
79
80 component.handle_event(&event.event, &event.params).await;
81
82 let html = component.render();
83 let patch = serde_json::to_string(&ServerPatch { html }).unwrap_or_default();
84 if tx.send(Message::Text(patch.into())).await.is_err() {
85 break;
86 }
87 }
88}
89
90const CLIENT_JS: &str = r#"
93const LiveWire = {
94 ws: null,
95 connect(url) {
96 const wsUrl = (location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + url;
97 this.ws = new WebSocket(wsUrl);
98 this.ws.onmessage = (e) => {
99 try {
100 const patch = JSON.parse(e.data);
101 if (patch.html) {
102 document.getElementById('lw-root').innerHTML = patch.html;
103 this.bind();
104 }
105 } catch {}
106 };
107 this.ws.onclose = () => setTimeout(() => this.connect(url), 2000);
108 this.bind();
109 },
110 bind() {
111 document.querySelectorAll('[lw-click]').forEach(el => {
112 if (el._lw) return;
113 el._lw = true;
114 el.addEventListener('click', () => {
115 const event = el.getAttribute('lw-click');
116 const params = {};
117 for (const attr of el.attributes) {
118 if (attr.name.startsWith('lw-param-')) {
119 params[attr.name.slice(9)] = attr.value;
120 }
121 }
122 this.send(event, params);
123 });
124 });
125 document.querySelectorAll('[lw-submit]').forEach(el => {
126 if (el._lw) return;
127 el._lw = true;
128 el.addEventListener('submit', (e) => {
129 e.preventDefault();
130 const event = el.getAttribute('lw-submit');
131 const params = {};
132 const formData = new FormData(el);
133 for (const [k, v] of formData) params[k] = v;
134 this.send(event, params);
135 });
136 });
137 document.querySelectorAll('[lw-input]').forEach(el => {
138 if (el._lw) return;
139 el._lw = true;
140 el.addEventListener('input', () => {
141 const event = el.getAttribute('lw-input');
142 this.send(event, { value: el.value });
143 });
144 });
145 },
146 send(event, params) {
147 if (this.ws && this.ws.readyState === 1) {
148 this.ws.send(JSON.stringify({ event, params: params || {} }));
149 }
150 }
151};
152"#;