Template Helpers
Quokka comes with a variety of helpers for templating making it easy to quickly build and extend a web application keeping functionality separated from each other.
NOTE
Some examples come up with a random a
fn get_router(&self) -> axum::Router<S>function as an entrypoint. The function has to be seen as the snippet from aPouch. The examples also assume that templates and other resources are already registered if not explicitely shown. I also assume, that by now, you got to know how the application is started, otherwise check out the Quickstart chapter.Most code here is similar to the one represented in the Handler chapter and much better documented there.
For the full, tested example about the template helpers check the examples/data_handlers.rs
Static templates
You can define a simple, static template with the StaticTemplate helper. While it renders a static template (which sounds useless so far), it also
allows you to provide some static data to work with using the StaticTemplateContext.
use quokka::handler::templating::StaticTemplate;
fn get_router(&self) -> axum::Router<S> {
axum::Router::new()
.route("/static", StaticTemplate("test-template").into())
.route(
"/static/greeting",
StaticTemplate("greet-name").into_with_context(serde_json::json! {{
"name": "Static"
}}),
)
}
TemplateRenderer
The TemplateRenderer is a bit more invovled. While being registered in the same way to the route, they require you to provide a struct
which implements the DataLoader trait.
A DataLoader can range from very simple up to a complex handler combining any axum extractor with some internal services provided
through a state (see the states#FromState section for a glimpse of how a service can be implemented using
the global state).
#[derive(Clone, FromState)]
struct NameParamDataLoader {}
impl<S: Send + Sync + Clone + 'static> DataLoader<S> for NameParamDataLoader {
type Args = Path<String>;
type Data = TemplateData;
type Error = Infallible;
async fn load_data(&self, name: Self::Args) -> Result<Self::Data, Self::Error> {
Ok(TemplateData {
name: name.to_string(),
})
}
}
// [...]
fn get_router(&self) -> axum::Router<S> {
axum::Router::new()
.route(
"/greet/{name}",
TemplateRenderer::<NameParamDataLoader>::new("greet-name").into(),
)
}
This can feel a bit restricting and does not allow to handle methods other than GET yet, but this general way of constructing the data
allows you not only to render templates but in combination with the JsonRenderer you can even present your data as JSON (other Renderer
structs can be implemented of cause too).
FormHandler
Now we want to handle other requests too (Like POST, PUT, DELETE or other HTML verbs), for this we have the FormHandler or to be
more acurate, for this we implement the DataHandler trait. It allows us to accept some Body and forward data (through axum extensions)
to a renderer.
The first step is to get some struct and implement the FormHandler trait to it:
#[derive(Clone, FromState)]
struct PostDataHandler;
#[derive(serde::Deserialize)]
struct InputData {
name: String,
}
#[derive(Clone)]
struct TemplateData {
name: String,
}
impl<S: Send + Sync + Clone + 'static> DataHandler<S> for PostDataHandler {
type Args = ();
type Body = InputData;
type Error = Infallible;
type Extension = TemplateData;
async fn process_data(
&self,
_: Self::Args,
body: Self::Body,
) -> Result<Self::Extension, Self::Error> {
Ok(TemplateData { name: body.name })
}
}
This very basic handler just takes a parameter from the body and passes it on as an extension. But in the same place it could also grab the
Session throug the Args type, authorize a user and if applicable store the body data into a database.
So far we can receive data, but we also need to display something to the user. For this our PostDataHandler has to implement the
DataLoader trait too (actually displaying the data can also be done on a different struct as long as the response extension is compatible)
Lets implement some displaying, including error handling:
impl<S: Clone + Send + Sync + 'static> DataLoader<S> for PostDataHandler {
type Args = (
Option<Extension<TemplateData>>,
// The error (`Infallible` in this case) has to match the error of the one emitted in the `DataHandler` implementation
// Multiple extensions can of cause be expected for different handlers
Option<Extension<quokka::handler::HandlerError<Infallible>>>,
);
type Data = TemplateData;
// This error can be different though.
type Error = Infallible;
async fn load_data(&self, (data, error): Self::Args) -> Result<Self::Data, Self::Error> {
if let Some(error) = error {
match error.0 {
// This error is being emitted when the body could not be parsed in the Handler.
// This method of handling it should not be presented, but shows the possibilities
quokka_handler::HandlerError::DataExtractorRejection(_) => {
return Ok(TemplateData {
name: String::from("Unable to extract Body from request"),
})
}
// This catches the `Infallible` (error variant of the `DataHandler`) or an error with extracting the request parameters
// which cannot happen in this case though as we don't extract any
error => panic!("We got some unexpected error: {error:?}"),
}
}
if let Some(data) = data {
return Ok(data.0);
}
Ok(TemplateData {
name: String::from("empty"),
})
}
}
Now this handler can output some error information in case some error ocurred. To finally register the route we can simply combine the
FormHandler with a TemplateRenderer:
fn get_router(&self) -> axum::Router<S> {
axum::Router::new().route(
"/greet",
FormHandler::<PostDataHandler, _>::new(TemplateRenderer::<PostDataHandler>::new(
"greet-name",
))
.into(),
)
}