Archived
feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
//! Server backend — SSR: render to HTML string, served by axum.
|
||||
//!
|
||||
//! This is the primary SSR backend. An axum handler calls `render_to_string()`
|
||||
//! on the component tree and returns the result as an HTTP response.
|
||||
//!
|
||||
//! The same component code runs server-side without any changes. Only the
|
||||
//! backend (chosen by `el.toml`) differs.
|
||||
|
||||
use crate::{EventHandler, PlatformBackend, PlatformError, PlatformNode, PlatformResult};
|
||||
|
||||
/// Server-side rendering backend.
|
||||
///
|
||||
/// Renders component trees to full HTML strings. No DOM, no browser APIs.
|
||||
/// An axum handler uses this backend to generate the initial page HTML.
|
||||
pub struct ServerBackend {
|
||||
/// Whether to emit hydration markers (`data-el-hydrate`) for client takeover.
|
||||
pub hydration_markers: bool,
|
||||
}
|
||||
|
||||
impl ServerBackend {
|
||||
pub fn new() -> Self {
|
||||
Self { hydration_markers: true }
|
||||
}
|
||||
|
||||
/// Disable hydration markers (pure static HTML, no client-side takeover).
|
||||
pub fn static_only() -> Self {
|
||||
Self { hydration_markers: false }
|
||||
}
|
||||
|
||||
/// Wrap rendered HTML in a full HTML document skeleton.
|
||||
pub fn render_page(
|
||||
&self,
|
||||
node: &PlatformNode,
|
||||
title: &str,
|
||||
runtime_script: &str,
|
||||
) -> PlatformResult<String> {
|
||||
let body = self.render_to_string(node)?;
|
||||
Ok(format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" data-el-ssr="true">
|
||||
{body}
|
||||
</div>
|
||||
<script type="module" src="{runtime_script}"></script>
|
||||
</body>
|
||||
</html>"#
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServerBackend {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformBackend for ServerBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
"server"
|
||||
}
|
||||
|
||||
fn create_element(&self, tag: &str) -> PlatformResult<PlatformNode> {
|
||||
Ok(PlatformNode::element(tag))
|
||||
}
|
||||
|
||||
fn create_text(&self, content: &str) -> PlatformResult<PlatformNode> {
|
||||
Ok(PlatformNode::text(content))
|
||||
}
|
||||
|
||||
fn set_attribute(
|
||||
&self,
|
||||
node: &mut PlatformNode,
|
||||
name: &str,
|
||||
value: &str,
|
||||
) -> PlatformResult<()> {
|
||||
node.attributes.retain(|a| a.name != name);
|
||||
node.attributes.push(crate::Attribute::new(name, value));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_attribute(&self, node: &mut PlatformNode, name: &str) -> PlatformResult<()> {
|
||||
node.attributes.retain(|a| a.name != name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_child(
|
||||
&self,
|
||||
parent: &mut PlatformNode,
|
||||
child: PlatformNode,
|
||||
) -> PlatformResult<()> {
|
||||
parent.children.push(child);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_child(&self, parent: &mut PlatformNode, child_index: usize) -> PlatformResult<()> {
|
||||
if child_index >= parent.children.len() {
|
||||
return Err(PlatformError::Render(format!(
|
||||
"server: child index {} out of bounds",
|
||||
child_index
|
||||
)));
|
||||
}
|
||||
parent.children.remove(child_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replace_child(
|
||||
&self,
|
||||
parent: &mut PlatformNode,
|
||||
index: usize,
|
||||
new_child: PlatformNode,
|
||||
) -> PlatformResult<()> {
|
||||
if index >= parent.children.len() {
|
||||
return Err(PlatformError::Render(format!(
|
||||
"server: replace_child index {} out of bounds",
|
||||
index
|
||||
)));
|
||||
}
|
||||
parent.children[index] = new_child;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bind_event(
|
||||
&self,
|
||||
node: &mut PlatformNode,
|
||||
event: &str,
|
||||
_handler: EventHandler,
|
||||
) -> PlatformResult<()> {
|
||||
// On the server, event handlers are emitted as data attributes.
|
||||
// The client-side hydration pass picks them up and binds real listeners.
|
||||
if self.hydration_markers {
|
||||
node.attributes
|
||||
.push(crate::Attribute::new(format!("data-el-{}", event), "hydrate"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_to_string(&self, node: &PlatformNode) -> PlatformResult<String> {
|
||||
Ok(node.to_html())
|
||||
}
|
||||
|
||||
fn mount(&self, _root: PlatformNode, _container_id: &str) -> PlatformResult<()> {
|
||||
// Server has no mount concept — rendering is one-shot.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn patch(&self, _old: &PlatformNode, _new: &PlatformNode) -> PlatformResult<()> {
|
||||
// Server rendering is stateless — no patch needed.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn supports_ssr(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user