feat(build): generate keyed view commands
Add lower-aware generated append/prepend/replace helpers for keyed slots, move the v0 keyed todo example off the facade extension trait, and keep beginner examples from importing lower-level render shortcuts. req: dx/006 req: codegen/002 req: public_api/003 req: examples/003
This commit is contained in:
+2
-2
@@ -76,7 +76,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
|
|||||||
005 Error messages must explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”.
|
005 Error messages must explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”.
|
||||||
|
|
||||||
### req: dx/006
|
### req: dx/006
|
||||||
006 Generated resource methods are the preferred authoring API: `ui::put(slots::todo_list, &view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. The public facade and generated view modules expose `render(view)` for trusted hemplate-to-`SafeHtml` page and fragment composition, `put(slot, view)` for lower-aware slot HTML updates, `static_fragment(include_str!(...))` for prototype/static `.heml` fragments that need generated resource lowering as `SafeHtml`, plus `lower(html)` for callers that need the lowered string; `render_html(view)` and `lower_html(html)` remain explicit compatibility aliases. The beginner prelude should not expose lower-level slot render shortcuts that bypass generated lowering. These return `impl IntoEffect`, `SafeHtml`, or lowered HTML at the boundary. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star]
|
006 Generated resource commands are the preferred authoring API: `ui::put(slots::todo_list, &view)`, `ui::append(slots::card, key, &view)`, `slots::count.text(42)`, `atoms::user.set(user)`. The public facade and generated view modules expose `render(view)` for trusted hemplate-to-`SafeHtml` page and fragment composition, `put(slot, view)` for lower-aware slot HTML updates, `append/prepend/replace(keyed_slot, key, view)` for lower-aware keyed slot updates, `static_fragment(include_str!(...))` for prototype/static `.heml` fragments that need generated resource lowering as `SafeHtml`, plus `lower(html)` for callers that need the lowered string; `render_html(view)` and `lower_html(html)` remain explicit compatibility aliases. The beginner prelude should not expose lower-level slot render shortcuts that bypass generated lowering. These return `impl IntoEffect`, `SafeHtml`, or lowered HTML at the boundary. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star]
|
||||||
|
|
||||||
### req: dx/007
|
### req: dx/007
|
||||||
007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path.
|
007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path.
|
||||||
@@ -226,7 +226,7 @@ a `data-*` handle param is statically known or runtime-extracted.
|
|||||||
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star]
|
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star]
|
||||||
|
|
||||||
### req: codegen/002
|
### req: codegen/002
|
||||||
002 Generated module `slots` exposes ergonomic methods: `Slot<T>::render(value)`, `Slot<T>::text(value)`, `KeyedSlot<K,T>::append(key, value)`, `KeyedSlot<K,T>::prepend(key, value)`, `KeyedSlot<K,T>::replace(key, value)`, `KeyedSlot<K,T>::remove(key)`. Methods return `impl IntoEffect`.
|
002 Generated view modules expose ergonomic resource commands: `put(slot, value)`, `append(keyed_slot, key, value)`, `prepend(keyed_slot, key, value)`, `replace(keyed_slot, key, value)`, plus typed resource constants for direct text/remove operations. Commands return `impl IntoEffect` and preserve generated lowering.
|
||||||
|
|
||||||
### req: codegen/003
|
### req: codegen/003
|
||||||
003 Generated module `handles` exports typed constants: `Handle<I>` where `I` is `Form<T>`, a param type, or `()`. Users rarely reference handles directly; they are consumed by `#[slhx::handler]` for validation.
|
003 Generated module `handles` exports typed constants: `Handle<I>` where `I` is `Form<T>`, a param type, or `()`. Users rarely reference handles directly; they are consumed by `#[slhx::handler]` for validation.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ pub mod ui {}
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::ui::{auth, counter, notifications, page_swap, todos, wizard};
|
use super::ui::{auth, counter, notifications, page_swap, todos, wizard};
|
||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use slhx::{push, Effect, IntoEffect, NavigateMode, Payload, RenderKeyedSlotExt};
|
use slhx::{push, Effect, IntoEffect, NavigateMode, Payload};
|
||||||
use slhx_test::inspect;
|
use slhx_test::inspect;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -77,7 +77,8 @@ mod tests {
|
|||||||
fn todos_append_keyed_rows_from_form_input() {
|
fn todos_append_keyed_rows_from_form_input() {
|
||||||
fn add_todo(input: TodoInput) -> impl IntoEffect {
|
fn add_todo(input: TodoInput) -> impl IntoEffect {
|
||||||
let todo = Todo { id: 7, title: input.title };
|
let todo = Todo { id: 7, title: input.title };
|
||||||
todos::slots::todo_row.append(
|
todos::append(
|
||||||
|
todos::slots::todo_row,
|
||||||
todo.id.to_string(),
|
todo.id.to_string(),
|
||||||
&TodoRow { title: todo.title },
|
&TodoRow { title: todo.title },
|
||||||
)
|
)
|
||||||
|
|||||||
+33
-6
@@ -534,9 +534,27 @@ impl Resources {
|
|||||||
out.push_str(&format!("{pad} ::slhx::SafeHtml::trusted(lower(html))\n"));
|
out.push_str(&format!("{pad} ::slhx::SafeHtml::trusted(lower(html))\n"));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{ render(view) }}\n\n"));
|
out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{ render(view) }}\n\n"));
|
||||||
out.push_str(&format!("{pad}pub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::slhx::Effect {{\n"));
|
out.push_str(&format!("{pad}pub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect {{\n"));
|
||||||
out.push_str(&format!("{pad} slot.html(render(view))\n"));
|
out.push_str(&format!("{pad} slot.html(render(view))\n"));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}pub fn append<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect\n"));
|
||||||
|
out.push_str(&format!("{pad}where\n"));
|
||||||
|
out.push_str(&format!("{pad} K: ::std::string::ToString,\n"));
|
||||||
|
out.push_str(&format!("{pad}{{\n"));
|
||||||
|
out.push_str(&format!("{pad} slot.append_html(key, render(view))\n"));
|
||||||
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}pub fn prepend<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect\n"));
|
||||||
|
out.push_str(&format!("{pad}where\n"));
|
||||||
|
out.push_str(&format!("{pad} K: ::std::string::ToString,\n"));
|
||||||
|
out.push_str(&format!("{pad}{{\n"));
|
||||||
|
out.push_str(&format!("{pad} slot.prepend_html(key, render(view))\n"));
|
||||||
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}pub fn replace<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect\n"));
|
||||||
|
out.push_str(&format!("{pad}where\n"));
|
||||||
|
out.push_str(&format!("{pad} K: ::std::string::ToString,\n"));
|
||||||
|
out.push_str(&format!("{pad}{{\n"));
|
||||||
|
out.push_str(&format!("{pad} slot.replace_html(key, render(view))\n"));
|
||||||
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
self.push_lowering_table(out, component, indent, &table_name);
|
self.push_lowering_table(out, component, indent, &table_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1310,7 +1328,10 @@ mod tests {
|
|||||||
assert!(generated.contains("pub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("pub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("pub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::slhx::Effect"));
|
assert!(generated.contains("pub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
|
assert!(generated.contains("pub fn append<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
|
assert!(generated.contains("pub fn prepend<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
|
assert!(generated.contains("pub fn replace<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
assert!(generated.contains("data-slhx-slot"));
|
assert!(generated.contains("data-slhx-slot"));
|
||||||
assert!(generated.contains("data-slhx-form"));
|
assert!(generated.contains("data-slhx-form"));
|
||||||
assert!(generated.contains("data-sid"));
|
assert!(generated.contains("data-sid"));
|
||||||
@@ -1380,13 +1401,15 @@ mod tests {
|
|||||||
assert!(generated.contains("\npub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("\npub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("\npub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::slhx::Effect"));
|
assert!(generated.contains("\npub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
|
assert!(generated.contains("\npub fn append<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
assert!(generated.contains("pub mod todo"));
|
assert!(generated.contains("pub mod todo"));
|
||||||
assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")"));
|
assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")"));
|
||||||
assert!(generated.contains(" pub mod slots"));
|
assert!(generated.contains(" pub mod slots"));
|
||||||
assert!(generated.contains(" pub fn lower_html"));
|
assert!(generated.contains(" pub fn lower_html"));
|
||||||
assert!(generated.contains(" pub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
|
assert!(generated.contains(" pub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains(" pub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::slhx::Effect"));
|
assert!(generated.contains(" pub fn put<T>(slot: ::slhx::Slot<T>, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
|
assert!(generated.contains(" pub fn append<K, T>(slot: ::slhx::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> impl ::slhx::IntoEffect"));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
@@ -1488,10 +1511,12 @@ mod slhx {{
|
|||||||
#[derive(Clone, Copy)] pub struct BuildFingerprint;
|
#[derive(Clone, Copy)] pub struct BuildFingerprint;
|
||||||
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
||||||
pub struct Effect;
|
pub struct Effect;
|
||||||
|
pub trait IntoEffect {{}}
|
||||||
|
impl IntoEffect for Effect {{}}
|
||||||
#[derive(Clone, Copy)] pub struct Slot<T>(::std::marker::PhantomData<T>);
|
#[derive(Clone, Copy)] pub struct Slot<T>(::std::marker::PhantomData<T>);
|
||||||
impl<T> Slot<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} pub fn html(self, _: SafeHtml) -> Effect {{ Effect }} }}
|
impl<T> Slot<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} pub fn html(self, _: SafeHtml) -> Effect {{ Effect }} }}
|
||||||
#[derive(Clone, Copy)] pub struct KeyedSlot<K, T>(::std::marker::PhantomData<(K, T)>);
|
#[derive(Clone, Copy)] pub struct KeyedSlot<K, T>(::std::marker::PhantomData<(K, T)>);
|
||||||
impl<K, T> KeyedSlot<K, T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
|
impl<K, T> KeyedSlot<K, T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} pub fn append_html(self, _: K, _: SafeHtml) -> Effect {{ Effect }} pub fn prepend_html(self, _: K, _: SafeHtml) -> Effect {{ Effect }} pub fn replace_html(self, _: K, _: SafeHtml) -> Effect {{ Effect }} }}
|
||||||
#[derive(Clone, Copy)] pub struct Handle<T>(::std::marker::PhantomData<T>);
|
#[derive(Clone, Copy)] pub struct Handle<T>(::std::marker::PhantomData<T>);
|
||||||
impl<T> Handle<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
|
impl<T> Handle<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
|
||||||
#[derive(Clone, Copy)] pub struct Atom<T>(::std::marker::PhantomData<T>);
|
#[derive(Clone, Copy)] pub struct Atom<T>(::std::marker::PhantomData<T>);
|
||||||
@@ -1763,10 +1788,12 @@ mod slhx {{
|
|||||||
#[derive(Clone, Copy)] pub struct BuildFingerprint;
|
#[derive(Clone, Copy)] pub struct BuildFingerprint;
|
||||||
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
||||||
pub struct Effect;
|
pub struct Effect;
|
||||||
|
pub trait IntoEffect {{}}
|
||||||
|
impl IntoEffect for Effect {{}}
|
||||||
#[derive(Clone, Copy)] pub struct Slot<T>(::std::marker::PhantomData<T>);
|
#[derive(Clone, Copy)] pub struct Slot<T>(::std::marker::PhantomData<T>);
|
||||||
impl<T> Slot<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} pub fn html(self, _: SafeHtml) -> Effect {{ Effect }} }}
|
impl<T> Slot<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} pub fn html(self, _: SafeHtml) -> Effect {{ Effect }} }}
|
||||||
#[derive(Clone, Copy)] pub struct KeyedSlot<K, T>(::std::marker::PhantomData<(K, T)>);
|
#[derive(Clone, Copy)] pub struct KeyedSlot<K, T>(::std::marker::PhantomData<(K, T)>);
|
||||||
impl<K, T> KeyedSlot<K, T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
|
impl<K, T> KeyedSlot<K, T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} pub fn append_html(self, _: K, _: SafeHtml) -> Effect {{ Effect }} pub fn prepend_html(self, _: K, _: SafeHtml) -> Effect {{ Effect }} pub fn replace_html(self, _: K, _: SafeHtml) -> Effect {{ Effect }} }}
|
||||||
#[derive(Clone, Copy)] pub struct Handle<T>(::std::marker::PhantomData<T>);
|
#[derive(Clone, Copy)] pub struct Handle<T>(::std::marker::PhantomData<T>);
|
||||||
impl<T> Handle<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
|
impl<T> Handle<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
|
||||||
#[derive(Clone, Copy)] pub struct Atom<T>(::std::marker::PhantomData<T>);
|
#[derive(Clone, Copy)] pub struct Atom<T>(::std::marker::PhantomData<T>);
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ fn canonical_examples_do_not_author_low_level_resource_plumbing() {
|
|||||||
"InteractionForm",
|
"InteractionForm",
|
||||||
"register_handle(",
|
"register_handle(",
|
||||||
"RenderSlotExt",
|
"RenderSlotExt",
|
||||||
|
"RenderKeyedSlotExt",
|
||||||
".render(&",
|
".render(&",
|
||||||
".html(ui::render",
|
".html(ui::render",
|
||||||
".html(slhx::render",
|
".html(slhx::render",
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ pub mod prelude {
|
|||||||
CssClasses, Effect, EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot,
|
CssClasses, Effect, EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot,
|
||||||
ParamName, SafeHtml, Slot,
|
ParamName, SafeHtml, Slot,
|
||||||
};
|
};
|
||||||
pub use crate::{render, render_html, RenderKeyedSlotExt};
|
pub use crate::{render, render_html};
|
||||||
pub use slhx_derive::{app, component, form, handler, surface};
|
pub use slhx_derive::{app, component, form, handler, surface};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user