feat(api): streamline generated app authoring

Move the canonical examples toward generated component-root helpers, typed form decoding, async/state handler registration, and derive-driven app/component registry wiring. Tighten requirements and diagnostics for the server-first, selectorless authoring path.

Verified with cargo run -p slhx-xtask -- test, cargo check --workspace, redgate list, redgate refs, redgate health --strict, and git diff --check.

req: canonical/001

req: canonical/003

req: canonical/004

req: dx/002

req: derive_app/001

req: component/003

req: form/004

req: axum_integration/003
This commit is contained in:
slhx agent
2026-06-05 06:33:41 +02:00
parent eb6086616c
commit d4e865ef92
34 changed files with 4573 additions and 1156 deletions
+91 -17
View File
@@ -3,18 +3,89 @@
//! Most application code should depend on this crate, use the proc-macros from
//! here, and import generated resources through `#[slhx::surface]`.
pub use slhx_core::*;
use slhx_core::{Effect, KeyedSlot, SafeHtml, Slot};
pub use slhx_core::{
navigate, push, redirect, replace, Atom, ComponentRef, CssClass, CssClasses, EventName, Form,
FormContract, FormControlKind, FormError, FormField, FormModel, FormValue, FromForm,
GeneratedTarget, Handle, IntoEffect, ParamName,
};
pub use slhx_derive::{app, component, form, handler, surface};
pub fn render(view: &impl hemplate::Hemplate) -> SafeHtml {
let mut html = String::new();
view.render_into(&mut html)
.expect("hemplate view renders into slhx effect payload");
SafeHtml::trusted(html)
/// Advanced/raw slhx primitives used by generated code, integrations, and tests.
///
/// Beginner-facing application code should prefer generated targets, generated
/// handles/forms/classes, `Html`, `IntoEffect`, and tuple composition. req: dx/001 req: public_api/005
pub mod advanced {
pub use slhx_core::*;
}
/// Rendered, checked HTML produced by hemplate/slhx rendering helpers.
///
/// Raw trusted HTML construction remains an advanced boundary; beginner-facing
/// code should receive `Html` values from generated render helpers. req: public_api/005
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Html(SafeHtml);
impl Html {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn into_string(self) -> String {
self.0.into_string()
}
pub fn join(fragments: impl IntoIterator<Item = Html>) -> Self {
Self(SafeHtml::join(fragments.into_iter().map(Into::into)))
}
}
impl From<Html> for SafeHtml {
fn from(value: Html) -> Self {
value.0
}
}
impl AsRef<str> for Html {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl core::fmt::Display for Html {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[doc(hidden)]
pub fn render_html(view: &impl hemplate::Hemplate) -> SafeHtml {
pub mod __private {
use super::{Html, SafeHtml};
pub fn html_trusted(value: impl Into<String>) -> Html {
Html(SafeHtml::trusted(value))
}
}
pub fn render(view: &impl hemplate::Hemplate) -> Html {
let mut html = String::new();
view.render_into(&mut html)
.expect("hemplate view renders into slhx effect payload");
__private::html_trusted(html)
}
/// A hemplate partial that carries the stable key for a generated keyed target.
///
/// Generated keyed target helpers use this to keep ordinary handler code at the
/// level of `ui::row.replace(row)` instead of `ui::row.replace(row.id, &row)`.
/// req: canonical_authoring/003 req: codegen/002
pub trait KeyedPartial {
fn slhx_key(&self) -> String;
}
#[doc(hidden)]
pub fn render_html(view: &impl hemplate::Hemplate) -> Html {
render(view)
}
@@ -42,7 +113,7 @@ impl<T> RenderSlotExt for Slot<T> {
/// Compatibility shim for raw keyed slot rendering.
///
/// Prefer generated target objects such as `targets::row.append(key, &view)` so
/// Prefer generated target objects such as `targets::row.append(&view)` so
/// generated resource lowering stays attached to the view boundary. req: dx/006
#[doc(hidden)]
pub trait RenderKeyedSlotExt<K> {
@@ -90,12 +161,12 @@ where
}
pub mod prelude {
pub use slhx_core::{
navigate, push, redirect, replace, Atom, BuildFingerprint, ComponentRef, CssClass,
CssClasses, Effect, EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot,
ParamName, SafeHtml, Slot,
};
pub use crate::render;
pub use crate::Html;
pub use slhx_core::{
navigate, push, redirect, replace, Atom, ComponentRef, CssClass, CssClasses, EventName,
Form, FormModel, FormValue, Handle, IntoEffect, ParamName,
};
pub use slhx_derive::{app, component, form, handler, surface};
}
@@ -114,15 +185,18 @@ mod tests {
fn render_is_the_short_safe_html_helper() {
// req: dx/006 req: html_safety/002
assert_eq!(crate::render(&InlineView).as_str(), "<strong>ok</strong>");
assert_eq!(crate::render_html(&InlineView).as_str(), "<strong>ok</strong>");
assert_eq!(
crate::render_html(&InlineView).as_str(),
"<strong>ok</strong>"
);
}
#[test]
fn prelude_exports_safe_html_for_page_composition() {
// req: dx/006 req: html_safety/002
fn prelude_exports_html_for_page_composition() {
// req: dx/006 req: public_api/005 req: html_safety/002
use crate::prelude::*;
let html = SafeHtml::join([render(&InlineView)]);
let html = Html::join([render(&InlineView)]);
assert_eq!(html.as_str(), "<strong>ok</strong>");
}