♻️ (pagetop): Refactorización de Contextual

Los métodos `required_id()` y `push_message()` son operaciones de tiempo
de renderizado, no de construcción. Se trasladan como métodos inherentes
de `Context`. También se ajustan los métodos asociados a parámetros.
This commit is contained in:
Manuel Cillero 2026-04-12 13:43:32 +02:00
parent 318d7de2b6
commit 0419658192
7 changed files with 121 additions and 189 deletions

View file

@ -79,10 +79,10 @@ impl Component for Navbar {
} }
// Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas. // Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
let id = cx.required_id::<Self>(self.id()); let id = cx.required_id::<Self>(self.id(), 1);
Ok(html! { Ok(html! {
nav id=(id) class=[self.classes().get()] { nav id=(&id) class=[self.classes().get()] {
div class="container-fluid" { div class="container-fluid" {
@match self.layout() { @match self.layout() {
// Barra más sencilla: sólo contenido. // Barra más sencilla: sólo contenido.
@ -95,7 +95,7 @@ impl Component for Navbar {
@let id_content = util::join!(id, "-content"); @let id_content = util::join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content)) (button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(id_content) class="collapse navbar-collapse" { div id=(&id_content) class="collapse navbar-collapse" {
(items) (items)
} }
}, },
@ -112,7 +112,7 @@ impl Component for Navbar {
(brand.render(cx)) (brand.render(cx))
(button(cx, TOGGLE_COLLAPSE, &id_content)) (button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(id_content) class="collapse navbar-collapse" { div id=(&id_content) class="collapse navbar-collapse" {
(items) (items)
} }
}, },
@ -123,7 +123,7 @@ impl Component for Navbar {
(button(cx, TOGGLE_COLLAPSE, &id_content)) (button(cx, TOGGLE_COLLAPSE, &id_content))
(brand.render(cx)) (brand.render(cx))
div id=(id_content) class="collapse navbar-collapse" { div id=(&id_content) class="collapse navbar-collapse" {
(items) (items)
} }
}, },

View file

@ -152,7 +152,7 @@ impl Offcanvas {
return html! {}; return html! {};
} }
let id = cx.required_id::<Self>(self.id()); let id = cx.required_id::<Self>(self.id(), 1);
let id_label = util::join!(id, "-label"); let id_label = util::join!(id, "-label");
let id_target = util::join!("#", id); let id_target = util::join!("#", id);
@ -171,7 +171,7 @@ impl Offcanvas {
html! { html! {
div div
id=(id) id=(&id)
class=[self.classes().get()] class=[self.classes().get()]
tabindex="-1" tabindex="-1"
data-bs-scroll=[body_scroll] data-bs-scroll=[body_scroll]
@ -180,7 +180,7 @@ impl Offcanvas {
{ {
div class="offcanvas-header" { div class="offcanvas-header" {
@if !title.is_empty() { @if !title.is_empty() {
h5 class="offcanvas-title" id=(id_label) { (title) } h5 id=(&id_label) class="offcanvas-title" { (title) }
} }
button button
type="button" type="button"

View file

@ -36,10 +36,10 @@ impl Component for Block {
return Ok(html! {}); return Ok(html! {});
} }
let id = cx.required_id::<Block>(self.id()); let id = cx.required_id::<Self>(self.id(), 1);
Ok(html! { Ok(html! {
div id=(id) class=[self.classes().get()] { div id=(&id) class=[self.classes().get()] {
@if let Some(title) = self.title().lookup(cx) { @if let Some(title) = self.title().lookup(cx) {
h2 class="block__title" { span { (title) } } h2 class="block__title" { span { (title) } }
} }

View file

@ -26,7 +26,7 @@ use std::sync::Arc;
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let component = Html::with(|cx| { /// let component = Html::with(|cx| {
/// let user = cx.param::<String>("username").cloned().unwrap_or("visitor".to_string()); /// let user = cx.param_or("username", "visitor".to_string());
/// html! { /// html! {
/// h1 { "Hello, " (user) } /// h1 { "Hello, " (user) }
/// } /// }

View file

@ -63,7 +63,7 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
/// ///
/// // Se instancia un componente que sólo se renderiza si `user_logged_in` es `true`. /// // Se instancia un componente que sólo se renderiza si `user_logged_in` es `true`.
/// let mut component = SampleComponent::new().with_renderable(Some(|cx: &Context| { /// let mut component = SampleComponent::new().with_renderable(Some(|cx: &Context| {
/// cx.param::<bool>("user_logged_in").copied().unwrap_or(false) /// cx.param_or_default::<bool>("user_logged_in")
/// })); /// }));
/// ///
/// // Aquí simplemente se comprueba que compila y se puede invocar. /// // Aquí simplemente se comprueba que compila y se puede invocar.

View file

@ -1,4 +1,4 @@
use crate::core::component::{ChildOp, MessageLevel, StatusMessage}; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::all::DEFAULT_THEME;
use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef}; use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef};
use crate::core::TypeInfo; use crate::core::TypeInfo;
@ -77,7 +77,6 @@ impl std::error::Error for ContextError {}
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo /// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`]. /// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`].
/// - Leer y mantener **parámetros dinámicos tipados** de contexto. /// - Leer y mantener **parámetros dinámicos tipados** de contexto.
/// - Generar **identificadores únicos** por tipo de componente.
/// ///
/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como /// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::page::Page). /// [`Context`](crate::core::component::Context) o [`Page`](crate::response::page::Page).
@ -94,7 +93,7 @@ impl std::error::Error for ContextError {}
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) /// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js"))) /// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
/// .with_param("usuario_id", 42_i32) /// .with_param("user_id", 42_i32)
/// } /// }
/// ``` /// ```
pub trait Contextual: LangId { pub trait Contextual: LangId {
@ -118,16 +117,17 @@ pub trait Contextual: LangId {
/// Añade o modifica un parámetro dinámico del contexto. /// Añade o modifica un parámetro dinámico del contexto.
/// ///
/// El valor se guardará conservando el *nombre del tipo* real para mejorar los mensajes de /// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de
/// error posteriores. /// error precisos al recuperarlo con [`param`](Contextual::param) si el tipo solicitado no
/// coincide.
/// ///
/// # Ejemplos /// # Ejemplo
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let cx = Context::new(None) /// let cx = Context::new(None)
/// .with_param("usuario_id", 42_i32) /// .with_param("user_id", 42_i32)
/// .with_param("titulo", "Hola".to_string()) /// .with_param("title", "Hello".to_string())
/// .with_param("flags", vec!["a", "b"]); /// .with_param("flags", vec!["a", "b"]);
/// ``` /// ```
#[builder_fn] #[builder_fn]
@ -158,49 +158,43 @@ pub trait Contextual: LangId {
/// Devuelve la plantilla configurada para renderizar el documento. /// Devuelve la plantilla configurada para renderizar el documento.
fn template(&self) -> TemplateRef; fn template(&self) -> TemplateRef;
/// Recupera un parámetro como [`Option`], simplificando el acceso. /// Recupera una *referencia tipada* al parámetro solicitado.
/// ///
/// A diferencia de [`get_param`](Context::get_param), que devuelve un [`Result`] con /// Devuelve:
/// información detallada de error, este método devuelve `None` tanto si la clave no existe como
/// si el valor guardado no coincide con el tipo solicitado.
/// ///
/// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo /// - `Ok(&T)` si la clave existe y el tipo coincide.
/// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo. /// - `Err(ContextError::ParamNotFound)` si la clave no existe.
/// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide.
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let cx = Context::new(None).with_param("username", "Alice".to_string()); /// let cx = Context::new(None)
/// .with_param("user_id", 42_i32)
/// .with_param("title", "Hello".to_string());
/// ///
/// // Devuelve Some(&String) si existe y coincide el tipo. /// let id: i32 = *cx.param("user_id").unwrap();
/// assert_eq!(cx.param::<String>("username").map(|s| s.as_str()), Some("Alice")); /// let title: &String = cx.param("title").unwrap();
/// ///
/// // Devuelve None si no existe o si el tipo no coincide. /// // Error de tipo:
/// assert!(cx.param::<i32>("username").is_none()); /// assert!(cx.param::<String>("user_id").is_err());
/// assert!(cx.param::<String>("missing").is_none());
///
/// // Acceso con valor por defecto.
/// let user = cx.param::<String>("missing")
/// .cloned()
/// .unwrap_or_else(|| "visitor".to_string());
/// assert_eq!(user, "visitor");
/// ``` /// ```
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>; fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError>;
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`). /// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
fn param_or_default<T: Default + Clone + 'static>(&self, key: &'static str) -> T { fn param_or_default<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
self.param::<T>(key).cloned().unwrap_or_default() self.param::<T>(key).ok().cloned().unwrap_or_default()
} }
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe. /// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T { fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
self.param::<T>(key).cloned().unwrap_or(default) self.param::<T>(key).ok().cloned().unwrap_or(default)
} }
/// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe. /// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe.
fn param_or_else<T: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T { fn param_or_else<T: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
self.param::<T>(key).cloned().unwrap_or_else(f) self.param::<T>(key).ok().cloned().unwrap_or_else(f)
} }
/// Devuelve el Favicon de los recursos del contexto. /// Devuelve el Favicon de los recursos del contexto.
@ -214,27 +208,17 @@ pub trait Contextual: LangId {
// **< Contextual HELPERS >********************************************************************* // **< Contextual HELPERS >*********************************************************************
/// Devuelve el `id` proporcionado tal cual, o genera uno único para el tipo `T` si no se /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
/// proporciona ninguno.
///
/// Si `id` es `None`, construye un identificador en la forma `<tipo>-<n>`, donde `<tipo>` es el
/// nombre corto del tipo en minúsculas y `<n>` un contador incremental interno del contexto. Es
/// útil para asignar identificadores HTML predecibles cuando el componente no recibe uno
/// explícito.
fn required_id<T>(&self, id: Option<String>) -> String;
/// Acumula un [`StatusMessage`] en el contexto para notificar al visitante.
///
/// Pueden generarse en cualquier punto del ciclo de una petición web (manejadores, renderizado,
/// lógica de negocio, etc.) que tengan acceso al contexto, y mostrarlos luego, por ejemplo, en
/// la página final devuelta al usuario.
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust
/// cx.push_message(MessageLevel::Warning, L10n::l("session-not-valid")); /// # use pagetop::prelude::*;
/// let mut cx = Context::new(None).with_param("temp", 1u8);
/// assert!(cx.remove_param("temp"));
/// assert!(!cx.remove_param("temp")); // ya no existe
/// ``` /// ```
fn push_message(&mut self, level: MessageLevel, text: L10n); fn remove_param(&mut self, key: &'static str) -> bool;
} }
/// Implementa un **contexto de renderizado** para un documento HTML. /// Implementa un **contexto de renderizado** para un documento HTML.
@ -264,7 +248,7 @@ pub trait Contextual: LangId {
/// // Añade un script JavaScript. /// // Añade un script JavaScript.
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js"))) /// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
/// // Añade un parámetro dinámico al contexto. /// // Añade un parámetro dinámico al contexto.
/// .with_param("usuario_id", 42) /// .with_param("user_id", 42)
/// } /// }
/// ``` /// ```
/// ///
@ -272,18 +256,22 @@ pub trait Contextual: LangId {
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # #[derive(AutoDefault, Clone, Debug)]
/// # struct Menu;
/// # impl Component for Menu {
/// # fn new() -> Self { Self::default() }
/// # }
/// fn use_context(cx: &mut Context) { /// fn use_context(cx: &mut Context) {
/// // Recupera el tema seleccionado. /// // Recupera el tema seleccionado.
/// let active_theme = cx.theme(); /// let active_theme = cx.theme();
/// assert_eq!(active_theme.short_name(), "aliner"); /// assert_eq!(active_theme.short_name(), "aliner");
/// ///
/// // Recupera el parámetro a su tipo original. /// // Recupera el parámetro a su tipo original.
/// let id: i32 = *cx.get_param::<i32>("usuario_id").unwrap(); /// let id: i32 = *cx.param::<i32>("user_id").unwrap();
/// assert_eq!(id, 42); /// assert_eq!(id, 42);
/// ///
/// // Genera un identificador para un componente de tipo `Menu`. /// // Genera un identificador para un componente de tipo `Menu`.
/// struct Menu; /// let unique_id = cx.required_id::<Menu>(None, 1);
/// let unique_id = cx.required_id::<Menu>(None);
/// assert_eq!(unique_id, "menu-1"); // Si es el primero generado. /// assert_eq!(unique_id, "menu-1"); // Si es el primero generado.
/// } /// }
/// ``` /// ```
@ -298,7 +286,7 @@ pub struct Context {
javascripts: Assets<JavaScript>, // Scripts JavaScript. javascripts: Assets<JavaScript>, // Scripts JavaScript.
regions : ChildrenInRegions, // Regiones de componentes para renderizar. regions : ChildrenInRegions, // Regiones de componentes para renderizar.
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución. params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
id_counter : Cell<usize>, // Cell permite incrementarlo desde &self en required_id(). id_counter : Cell<usize>, // Cell permite incrementar desde &self en required_id().
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados. messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
} }
@ -366,90 +354,6 @@ impl Context {
.render(self) .render(self)
} }
// **< Context PARAMS >*************************************************************************
/// Recupera una *referencia tipada* al parámetro solicitado.
///
/// Devuelve:
///
/// - `Ok(&T)` si la clave existe y el tipo coincide.
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
/// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop::prelude::*;
/// let cx = Context::new(None)
/// .with_param("usuario_id", 42_i32)
/// .with_param("titulo", "Hola".to_string());
///
/// let id: &i32 = cx.get_param("usuario_id").unwrap();
/// let titulo: &String = cx.get_param("titulo").unwrap();
///
/// // Error de tipo:
/// assert!(cx.get_param::<String>("usuario_id").is_err());
/// ```
pub fn get_param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
let (any, type_name) = self.params.get(key).ok_or(ContextError::ParamNotFound)?;
any.downcast_ref::<T>()
.ok_or_else(|| ContextError::ParamTypeMismatch {
key,
expected: TypeInfo::FullName.of::<T>(),
saved: type_name,
})
}
/// Recupera el parámetro solicitado y lo elimina del contexto.
///
/// Devuelve:
///
/// - `Ok(T)` si la clave existía y el tipo coincide.
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
/// - `Err(ContextError::ParamTypeMismatch)` si el tipo no coincide.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop::prelude::*;
/// let mut cx = Context::new(None)
/// .with_param("contador", 7_i32)
/// .with_param("titulo", "Hola".to_string());
///
/// let n: i32 = cx.take_param("contador").unwrap();
/// assert!(cx.get_param::<i32>("contador").is_err()); // ya no está
///
/// // Error de tipo:
/// assert!(cx.take_param::<i32>("titulo").is_err());
/// ```
pub fn take_param<T: 'static>(&mut self, key: &'static str) -> Result<T, ContextError> {
let (boxed, saved) = self.params.remove(key).ok_or(ContextError::ParamNotFound)?;
boxed
.downcast::<T>()
.map(|b| *b)
.map_err(|_| ContextError::ParamTypeMismatch {
key,
expected: TypeInfo::FullName.of::<T>(),
saved,
})
}
/// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
///
/// Devuelve `false` en caso contrario. Usar cuando sólo interesa borrar la entrada.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop::prelude::*;
/// let mut cx = Context::new(None).with_param("temp", 1u8);
/// assert!(cx.remove_param("temp"));
/// assert!(!cx.remove_param("temp")); // ya no existe
/// ```
pub fn remove_param(&mut self, key: &'static str) -> bool {
self.params.remove(key).is_some()
}
// **< Context HELPERS >************************************************************************ // **< Context HELPERS >************************************************************************
/// Construye una ruta aplicada al contexto actual. /// Construye una ruta aplicada al contexto actual.
@ -470,6 +374,51 @@ impl Context {
route route
} }
/// Garantiza un identificador único para un componente `C`, generándolo si no se proporciona
/// ninguno.
///
/// Si `id` es `None`, crea un identificador usando los últimos segmentos del *path* completo
/// del tipo `C`, separados por `-` y en minúsculas, seguidos de un contador incremental interno
/// del contexto. Por ejemplo, para un componente `MyApp::ui::Menu` con `parts = 2` podría
/// devolver un identificador como `ui-menu-1` si ha sido el primero en generarse.
///
/// Con `parts = 1` se usa el nombre corto del tipo. Si `parts` es `0` o supera el número de
/// segmentos del *path*, entonces se usará el *path* completo.
///
/// Es útil para asignar identificadores HTML predecibles cuando el componente no recibe uno
/// explícito.
pub fn required_id<C: Component>(&self, id: Option<String>, parts: usize) -> String {
if let Some(id) = id {
return id;
}
let segments: Vec<&str> = TypeInfo::FullName.of::<C>().split("::").collect();
let parts = if parts == 0 || parts >= segments.len() {
segments.len()
} else {
parts
};
self.id_counter.set(self.id_counter.get() + 1);
let prefix = segments[segments.len() - parts..].join("-").to_lowercase();
util::join!(prefix, "-", self.id_counter.get().to_string())
}
/// Acumula un [`StatusMessage`] en el contexto para notificar al visitante.
///
/// Pueden generarse en cualquier punto del ciclo de una petición web (manejadores, renderizado,
/// lógica de negocio, etc.) que tengan acceso al contexto, y mostrarlos luego, por ejemplo, en
/// la página final devuelta al usuario.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # let mut cx = Context::new(None);
/// cx.push_message(MessageLevel::Warning, L10n::n("Session is not valid"));
/// ```
pub fn push_message(&mut self, level: MessageLevel, text: L10n) {
self.messages.push(StatusMessage::new(level, text));
}
/// Devuelve todos los mensajes de usuario acumulados. /// Devuelve todos los mensajes de usuario acumulados.
pub fn messages(&self) -> &[StatusMessage] { pub fn messages(&self) -> &[StatusMessage] {
&self.messages &self.messages
@ -589,8 +538,14 @@ impl Contextual for Context {
self.template self.template
} }
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> { fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
self.get_param::<T>(key).ok() let (any, type_name) = self.params.get(key).ok_or(ContextError::ParamNotFound)?;
any.downcast_ref::<T>()
.ok_or_else(|| ContextError::ParamTypeMismatch {
key,
expected: TypeInfo::FullName.of::<T>(),
saved: type_name,
})
} }
fn favicon(&self) -> Option<&Favicon> { fn favicon(&self) -> Option<&Favicon> {
@ -607,26 +562,7 @@ impl Contextual for Context {
// **< Contextual HELPERS >********************************************************************* // **< Contextual HELPERS >*********************************************************************
fn required_id<T>(&self, id: Option<String>) -> String { fn remove_param(&mut self, key: &'static str) -> bool {
if let Some(id) = id { self.params.remove(key).is_some()
id
} else {
let prefix = TypeInfo::ShortName
.of::<T>()
.trim()
.replace(' ', "_")
.to_lowercase();
let prefix = if prefix.is_empty() {
"prefix".to_string()
} else {
prefix
};
self.id_counter.set(self.id_counter.get() + 1);
util::join!(prefix, "-", self.id_counter.get().to_string())
}
}
fn push_message(&mut self, level: MessageLevel, text: L10n) {
self.messages.push(StatusMessage::new(level, text));
} }
} }

View file

@ -19,7 +19,7 @@ pub use error::ErrorPage;
pub use actix_web::Result as ResultPage; pub use actix_web::Result as ResultPage;
use crate::base::action; use crate::base::action;
use crate::core::component::{AssetsOp, ChildOp, Context, Contextual}; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual};
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
use crate::html::{html, Markup, DOCTYPE}; use crate::html::{html, Markup, DOCTYPE};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
@ -349,7 +349,7 @@ impl Contextual for Page {
self.context.template() self.context.template()
} }
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> { fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
self.context.param(key) self.context.param(key)
} }
@ -367,11 +367,7 @@ impl Contextual for Page {
// **< Contextual HELPERS >********************************************************************* // **< Contextual HELPERS >*********************************************************************
fn required_id<T>(&self, id: Option<String>) -> String { fn remove_param(&mut self, key: &'static str) -> bool {
self.context.required_id::<T>(id) self.context.remove_param(key)
}
fn push_message(&mut self, level: crate::prelude::MessageLevel, text: L10n) {
self.context.push_message(level, text);
} }
} }