Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 a Pouch. 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(),
    )
}