diff --git a/README.md b/README.md index 1ed7d31..02ce3ab 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ For beginner and production-shaped app code, stay on this path. req: public_api/ also carries the stable row key. A list target inside `h-for` must have a stable `h-key`, so row updates are addressable without CSS selectors. req: list/001 - **Effect:** handlers return typed commands that become a checked effect - response. Tuple composition is the normal batch syntax. + response. Tuple composition is the normal fixed batch syntax; arrays and + `Vec` cover fixed or dynamic repeated partial updates. - **Runtime:** the browser checks the build fingerprint, resolves targets within the current `data-hemx-root`, and applies compatible batches. Mismatched server/runtime builds fail closed instead of silently mutating the wrong DOM. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 29362ee..357105f 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -53,7 +53,7 @@ client app state framework. 002 Typed partial swaps are the primary UX, not an advanced feature: handlers change domain state in Rust, convert domain values into view values, render hemplate partials through generated helpers, and place them into generated targets. The real primitive is generated target + rendered partial + swap kind. [north_star] ### req: canonical/003 -003 Canonical keyed-row CRUD reads like ordinary Rust intent: create appends a rendered row partial, update/toggle replaces a keyed row partial, delete removes a keyed row, summary/text/form effects compose in tuples or arrays implementing `IntoEffect`, and no handler chooses a target with a CSS selector. [north_star] +003 Canonical keyed-row CRUD reads like ordinary Rust intent: create appends a rendered row partial, update/toggle replaces a keyed row partial, delete removes a keyed row, summary/text/form effects compose in tuples, arrays, or `Vec` for dynamic batches, and no handler chooses a target with a CSS selector. [north_star] ### req: canonical/004 004 Generated helpers may compose only facts uniquely known from templates and checked Rust types: template, slot, optional key, form/control, class token, explicit island/event marker, and effect kind. If a handler parameter, key, form, target, raw route, or legacy target would require guessing, the user must say it explicitly and diagnostics must point to the Rust and hemplate spans. [north_star] diff --git a/examples/v0/README.md b/examples/v0/README.md index f3a23ce..fe9ac5e 100644 --- a/examples/v0/README.md +++ b/examples/v0/README.md @@ -11,7 +11,7 @@ Open . The page includes working examples for generated target objects and server-first UI commands: - counter updates -- todo form submission with one `TodoRow` hemplate partial reused by the initial page render and generated row append/replace/remove commands +- todo form submission with one `TodoRow` hemplate partial reused by the initial page render, generated row append/replace/remove commands, and dynamic `Vec` row batches - wizard step updates - login form feedback - page swap/navigation diff --git a/hemx-core/src/lib.rs b/hemx-core/src/lib.rs index cb4f5e2..7b8437c 100644 --- a/hemx-core/src/lib.rs +++ b/hemx-core/src/lib.rs @@ -850,6 +850,22 @@ impl IntoEffect for Option { } } +impl IntoEffect for Vec { + fn append_to(self, ops: &mut Vec) { + for effect in self { + effect.append_to(ops); + } + } +} + +impl IntoEffect for [T; N] { + fn append_to(self, ops: &mut Vec) { + for effect in self { + effect.append_to(ops); + } + } +} + macro_rules! impl_tuple_into_effect { ($($name:ident $idx:tt),+) => { impl<$($name),+> IntoEffect for ($($name,)+) diff --git a/hemx-core/tests/effect_batch.rs b/hemx-core/tests/effect_batch.rs index de84b1a..0a7da9a 100644 --- a/hemx-core/tests/effect_batch.rs +++ b/hemx-core/tests/effect_batch.rs @@ -38,6 +38,30 @@ fn optional_effects_compose_into_batches() { assert_eq!(batch.ops[0], count.text(2)); } +#[test] +fn effect_collections_compose_into_batches() { + // req: canonical_authoring/003 + let rows = KeyedSlot::::new(2); + let summary = Slot::::new(3); + let notice = Slot::::new(4); + + let dynamic_rows = [7, 8] + .into_iter() + .map(|id| rows.replace_text(id, format!("todo {id}"))) + .collect::>(); + let fixed_notices = [notice.text("Saved"), notice.text("Synced")]; + + let batch = + (dynamic_rows, Some(summary.text(2)), fixed_notices).into_batch(BuildFingerprint(42)); + + assert_eq!(batch.ops.len(), 5); + assert_eq!(batch.ops[0], rows.replace_text(7, String::from("todo 7"))); + assert_eq!(batch.ops[1], rows.replace_text(8, String::from("todo 8"))); + assert_eq!(batch.ops[2], summary.text(2)); + assert_eq!(batch.ops[3], notice.text("Saved")); + assert_eq!(batch.ops[4], notice.text("Synced")); +} + #[test] fn keyed_slot_replace_uses_scoped_resource_ref() { let todos = KeyedSlot::::new(9);