✨ (core): Añade TypedOp<C> para restringir Children
Mismo repertorio de operaciones que `ChildOp`, pero cada variante exige un componente de tipo `C`, evitando que un componente ajeno a ese tipo acabe en una lista pensada para un único tipo de elemento.
This commit is contained in:
parent
d8f82ea1d2
commit
c8654a5742
3 changed files with 174 additions and 1 deletions
|
|
@ -14,7 +14,7 @@ pub use definition::{Component, ComponentClone, ComponentRender};
|
|||
|
||||
mod children;
|
||||
pub use children::Children;
|
||||
pub use children::{Child, ChildOp, Embed};
|
||||
pub use children::{Child, ChildOp, Embed, TypedOp};
|
||||
|
||||
mod message;
|
||||
pub use message::{MessageLevel, StatusMessage};
|
||||
|
|
|
|||
|
|
@ -258,6 +258,79 @@ pub enum ChildOp {
|
|||
Reset,
|
||||
}
|
||||
|
||||
/// Mismo repertorio de operaciones de [`ChildOp`] restringido a un tipo de componente.
|
||||
///
|
||||
/// Conserva toda la funcionalidad de [`ChildOp`] (inserción relativa, reemplazo o eliminación por
|
||||
/// `id`, etc.) sin permitir que un componente ajeno al tipo `C` acabe en una lista pensada para un
|
||||
/// único tipo de elemento (p. ej., los elementos de un menú [`Nav`](crate::base::component::Nav)).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let nav = nav::Nav::new()
|
||||
/// // Un componente `nav::Item` se convierte implícitamente en `TypedOp::Add`.
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// // Para el resto de operaciones se construye la variante explícita.
|
||||
/// .with_item(TypedOp::AddMany(vec![
|
||||
/// nav::Item::link(Lc::n("About"), "/about"),
|
||||
/// nav::Item::link(Lc::n("Contact"), "/contact"),
|
||||
/// ]));
|
||||
/// ```
|
||||
pub enum TypedOp<C: Component> {
|
||||
/// Añade un componente al final de la lista.
|
||||
Add(C),
|
||||
/// Añade un componente sólo si la lista está vacía.
|
||||
AddIfEmpty(C),
|
||||
/// Añade varios componentes al final de la lista, en el orden recibido.
|
||||
AddMany(Vec<C>),
|
||||
/// Inserta un componente justo después del que tiene el `id` dado, o al final si no existe.
|
||||
InsertAfterId(&'static str, C),
|
||||
/// Inserta un componente justo antes del que tiene el `id` dado, o al principio si no existe.
|
||||
InsertBeforeId(&'static str, C),
|
||||
/// Inserta un componente al principio de la lista.
|
||||
Prepend(C),
|
||||
/// Inserta varios componentes al principio de la lista, manteniendo el orden recibido.
|
||||
PrependMany(Vec<C>),
|
||||
/// Elimina el primer componente con el `id` dado.
|
||||
RemoveById(&'static str),
|
||||
/// Sustituye el primer componente con el `id` dado por otro.
|
||||
ReplaceById(&'static str, C),
|
||||
/// Vacía la lista eliminando todos los componentes.
|
||||
Reset,
|
||||
}
|
||||
|
||||
impl<C: Component> From<C> for TypedOp<C> {
|
||||
/// Convierte un componente de tipo `C` en [`TypedOp::Add`], permitiendo pasarlo directamente a
|
||||
/// métodos como `with_item()` sin envolverlo explícitamente.
|
||||
#[inline]
|
||||
fn from(component: C) -> Self {
|
||||
TypedOp::Add(component)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> From<TypedOp<C>> for ChildOp {
|
||||
/// Traduce cada variante de [`TypedOp<C>`] a su equivalente en [`ChildOp`], envolviendo cada
|
||||
/// componente `C` en un [`Child`].
|
||||
fn from(op: TypedOp<C>) -> Self {
|
||||
match op {
|
||||
TypedOp::Add(c) => ChildOp::Add(Child::with(c)),
|
||||
TypedOp::AddIfEmpty(c) => ChildOp::AddIfEmpty(Child::with(c)),
|
||||
TypedOp::AddMany(cs) => ChildOp::AddMany(cs.into_iter().map(Child::with).collect()),
|
||||
TypedOp::InsertAfterId(id, c) => ChildOp::InsertAfterId(id, Child::with(c)),
|
||||
TypedOp::InsertBeforeId(id, c) => ChildOp::InsertBeforeId(id, Child::with(c)),
|
||||
TypedOp::Prepend(c) => ChildOp::Prepend(Child::with(c)),
|
||||
TypedOp::PrependMany(cs) => {
|
||||
ChildOp::PrependMany(cs.into_iter().map(Child::with).collect())
|
||||
}
|
||||
TypedOp::RemoveById(id) => ChildOp::RemoveById(id),
|
||||
TypedOp::ReplaceById(id, c) => ChildOp::ReplaceById(id, Child::with(c)),
|
||||
TypedOp::Reset => ChildOp::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lista ordenada de componentes hijo ([`Child`]) mantenida por un componente padre.
|
||||
///
|
||||
/// Permite añadir, modificar, renderizar y consultar componentes hijo en orden de inserción, con
|
||||
|
|
|
|||
|
|
@ -269,6 +269,106 @@ async fn children_render_concatenates_all_outputs_in_order() {
|
|||
);
|
||||
}
|
||||
|
||||
// **< TypedOp >************************************************************************************
|
||||
//
|
||||
// `TypedOp<C>` just translates to the equivalent `ChildOp` variant (see the `From` impl in
|
||||
// `children.rs`), so these tests only check that each variant reaches the right `Children`
|
||||
// operation (the operations themselves are already covered above under `ChildOp`).
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_add_appends_component() {
|
||||
let c = Children::new().with_child(TypedOp::Add(TestComp::text("a")));
|
||||
assert_eq!(c.render(&mut Context::default()).await.into_string(), "a");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_add_if_empty_only_adds_when_list_is_empty() {
|
||||
let c = Children::new()
|
||||
.with_child(TypedOp::AddIfEmpty(TestComp::text("first")))
|
||||
.with_child(TypedOp::AddIfEmpty(TestComp::text("second")));
|
||||
assert_eq!(
|
||||
c.render(&mut Context::default()).await.into_string(),
|
||||
"first"
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_add_many_appends_all_in_order() {
|
||||
let c = Children::new().with_child(TypedOp::AddMany(vec![
|
||||
TestComp::text("x"),
|
||||
TestComp::text("y"),
|
||||
TestComp::text("z"),
|
||||
]));
|
||||
assert_eq!(c.render(&mut Context::default()).await.into_string(), "xyz");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_insert_after_id_inserts_after_matching_element() {
|
||||
let c = Children::new()
|
||||
.with_child(TestComp::tagged("first", "a"))
|
||||
.with_child(TestComp::text("c"))
|
||||
.with_child(TypedOp::InsertAfterId("first", TestComp::text("b")));
|
||||
assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_insert_before_id_inserts_before_matching_element() {
|
||||
let c = Children::new()
|
||||
.with_child(TestComp::text("a"))
|
||||
.with_child(TestComp::tagged("last", "c"))
|
||||
.with_child(TypedOp::InsertBeforeId("last", TestComp::text("b")));
|
||||
assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_prepend_inserts_at_start() {
|
||||
let c = Children::new()
|
||||
.with_child(TestComp::text("b"))
|
||||
.with_child(TypedOp::Prepend(TestComp::text("a")));
|
||||
assert_eq!(c.render(&mut Context::default()).await.into_string(), "ab");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_prepend_many_inserts_all_at_start() {
|
||||
let c = Children::new()
|
||||
.with_child(TestComp::text("c"))
|
||||
.with_child(TypedOp::PrependMany(vec![
|
||||
TestComp::text("a"),
|
||||
TestComp::text("b"),
|
||||
]));
|
||||
assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_remove_by_id_removes_matching_element() {
|
||||
let c = Children::new()
|
||||
.with_child(TestComp::tagged("keep", "a"))
|
||||
.with_child(TestComp::tagged("drop", "b"))
|
||||
.with_child(TypedOp::<TestComp>::RemoveById("drop"));
|
||||
assert_eq!(c.render(&mut Context::default()).await.into_string(), "a");
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_replace_by_id_replaces_matching_element() {
|
||||
let c = Children::new()
|
||||
.with_child(TestComp::tagged("target", "old"))
|
||||
.with_child(TestComp::text("b"))
|
||||
.with_child(TypedOp::ReplaceById("target", TestComp::text("new")));
|
||||
assert_eq!(
|
||||
c.render(&mut Context::default()).await.into_string(),
|
||||
"newb"
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn typed_op_reset_clears_all_elements() {
|
||||
let c = Children::new()
|
||||
.with_child(TestComp::text("a"))
|
||||
.with_child(TestComp::text("b"))
|
||||
.with_child(TypedOp::<TestComp>::Reset);
|
||||
assert!(c.is_empty());
|
||||
}
|
||||
|
||||
// **< Embed >**************************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue