docs-site/content/docs/the-app/views.md
+++ title = "Views" description = "" date = 2021-05-01T18:10:00+00:00 updated = 2021-05-01T18:10:00+00:00 draft = false weight = 4 sort_by = "weight" template = "docs/page.html"
[extra] lead = "" toc = true top = false flair =[] +++
In Loco, the processing of web requests is divided between a controller, model and view.
You can choose to have JSON views, which are JSON responses, or Template views which are powered by a template view engine and eventually are HTML responses. You can also combine both.
<div class="infobox"> This is similar in spirit to Rails' `jbuilder` views which are JSON, and regular views, which are HTML, only that in LOCO we focus on being JSON-first. </div>As an example we have an endpoint that handles user login. When the user is valid we can pass the user model into the LoginResponse view (which is a JSON view) to return the response.
There are 3 steps:
The following Rust code represents a controller responsible for handling user login requests, which handes off shaping of the response to LoginResponse.
use crate::{views::auth::LoginResponse};
async fn login(
State(ctx): State<AppContext>,
Json(params): Json<LoginParams>,
) -> Result<Response> {
// Fetching the user model with the requested parameters
// let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?;
// Formatting the JSON response using LoginResponse view
format::json(LoginResponse::new(&user, &token))
}
On the other hand, LoginResponse is a response shaping view, which is powered by serde:
use serde::{Deserialize, Serialize};
use crate::models::_entities::users;
#[derive(Debug, Deserialize, Serialize)]
pub struct LoginResponse {
pub token: String,
pub pid: String,
pub name: String,
}
impl LoginResponse {
#[must_use]
pub fn new(user: &users::Model, token: &String) -> Self {
Self {
token: token.to_string(),
pid: user.pid.to_string(),
name: user.name.clone(),
}
}
}
When you want to return HTML to the user, you use server-side templates. This is similar to how Ruby's erb works, or Node's ejs, or PHP for that matter.
For server-side templates rendering we provide the built in TeraView engine which is based on the popular Tera template engine.
The Tera view engine takes resources from the new assets/ folder. Here is an example structure:
assets/
├── i18n
│ ├── de-DE
│ │ └── main.ftl
│ ├── en-US
│ │ └── main.ftl
│ └── shared.ftl
├── static
│ ├── 404.html
│ └── image.png
└── views
└── home
└── hello.html
config/
:
src/
├── controllers/
├── models/
:
└── views/
First, create a template. In this case we add a Tera template, in assets/views/home/hello.html. Note that assets/ sits in the root of your project (next to src/ and config/).
<html>
<body>
find this tera template at <code>assets/views/home/hello.html</code>:
{{ /* t(key="hello-world", lang="en-US") */ }},
{{ /* t(key="hello-world", lang="de-DE") */ }}
</body>
</html>
Now create a strongly typed view to encapsulate this template in src/views/dashboard.rs:
// src/views/dashboard.rs
use loco_rs::prelude::*;
pub fn home(v: impl ViewRenderer) -> Result<impl IntoResponse> {
format::render().view(&v, "home/hello.html", data!({}))
}
And add it to src/views/mod.rs:
pub mod dashboard;
Next, go to your controller and use the view:
// src/controllers/dashboard.rs
use loco_rs::prelude::*;
use crate::views;
pub async fn render_home(ViewEngine(v): ViewEngine<TeraView>) -> Result<impl IntoResponse> {
views::dashboard::home(v)
}
pub fn routes() -> Routes {
Routes::new().prefix("home").add("/", get(render_home))
}
Finally, register your new controller's routes in src/app.rs
pub struct App;
#[async_trait]
impl Hooks for App {
// omitted for brevity
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes()
.add_route(controllers::auth::routes())
// include your controller's routes here
.add_route(controllers::dashboard::routes())
}
Once you've done all the above, you should be able to see your new routes when running cargo loco routes
$ cargo loco routes
[GET] /_health
[GET] /_ping
[POST] /api/auth/forgot
[POST] /api/auth/login
[POST] /api/auth/register
[POST] /api/auth/reset
[POST] /api/auth/verify
[GET] /api/auth/current
[GET] /home <-- the corresponding URL for our new view
ViewEngine is an extractor that's available to you via loco_rs::prelude::*TeraView is the Tera view engine that we supply with Loco also available via loco_rs::prelude::*views::dashboard::home is an opaque call, it hides the details of how a view works, or how the bytes find their way into a browser, which is a Good ThingViewEngine<Foobar> and everything works, because v is eventually just a ViewRenderer traitIf you want to serve static assets and reference those in your view templates, you can use the Static Middleware, configure it this way:
static:
enable: true
must_exist: true
precompressed: false
folder:
uri: "/static"
path: "assets/static"
fallback: "assets/static/404.html"
In your templates you can refer to static resources in this way:
However, for the static middleware to work, ensure that the default fallback is disabled:
fallback:
enable: false
The Tera view engine comes with the following configuration:
assets/**/*.htmlt(..) to use in your templatesIf you want to change any configuration detail for the i18n library, you can go and edit src/initializers/view_engine.rs.
By editing the initializer you can:
i18n libraryi18n libraryIf you do not like Tera as a view engine, or want to use Handlebars, or others you can create your own custom view engine very easily.
Here's an example for a dummy "Hello" view engine. It's a view engine that always returns the word hello.
// src/initializers/hello_view_engine.rs
use axum::{Extension, Router as AxumRouter};
use async_trait::async_trait;
use loco_rs::{
app::{AppContext, Initializer},
controller::views::{ViewEngine, ViewRenderer},
Result,
};
use serde::Serialize;
#[derive(Clone)]
pub struct HelloView;
impl ViewRenderer for HelloView {
fn render<S: Serialize>(&self, _key: &str, _data: S) -> Result<String> {
Ok("hello".to_string())
}
}
pub struct HelloViewEngineInitializer;
#[async_trait]
impl Initializer for HelloViewEngineInitializer {
fn name(&self) -> String {
"custom-view-engine".to_string()
}
async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
Ok(router.layer(Extension(ViewEngine::from(HelloView))))
}
}
To use it, you need to add it to your src/app.rs hooks:
// src/app.rs
// add your custom "hello" view engine in the `initializers(..)` hook
impl Hooks for App {
// ...
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![
// ,.----- add it here
Box::new(initializers::hello_view_engine::HelloViewEngineInitializer),
])
}
// ...
Loco includes Tera with its built-ins functions. In addition, Loco introduces the following custom built-in functions:
To see Loco built-in function:
The Embedded Assets feature in Loco allows you to bundle all your static assets directly into your application binary. This means that everything under the assets folder, including CSS, images, PDFs, and more, becomes part of a single executable file.
To use this feature, you need to enable the embedded_assets feature when importing loco-rs in your Cargo.toml:
[dependencies]
loco-rs = { version = "...", features = ["embedded_assets"] }
You can easily switch between using embedded assets and serving assets from the filesystem without any code changes in your controllers or views. The switch is handled by the presence or absence of the embedded_assets feature flag.
However, to ensure Tera functions correctly when not using embedded assets (i.e., serving from the filesystem), you need to ensure that your src/initializers/view_engine.rs file only contains the necessary Tera function registration if you had customized it previously. Specifically, for the translation function t, ensure your initializer looks like this if you are not using loco_rs::tera_helpers::FluentLoader:
tera_engine
.tera
.register_function("t", FluentLoader::new(arc));
Alternatively, you can introduce an internal feature flag within your application to toggle how assets are loaded or how Tera is configured, providing more granular control.
When you build your application with the embedded_assets feature enabled, Loco will scan your assets directory and embed the discovered files. You will see logs similar to the following during the build process, indicating which assets are being included:
warning: [email protected]: Assets will only be loaded from the application directory
warning: [email protected]: Discovered directories for assets:
warning: [email protected]: - /path/to/your/myapp/assets
warning: [email protected]: - /path/to/your/myapp/assets/static
warning: [email protected]: - /path/to/your/myapp/assets/i18n
warning: [email protected]: - /path/to/your/myapp/assets/i18n/de-DE
warning: [email protected]: - /path/to/your/myapp/assets/i18n/en-US
warning: [email protected]: - /path/to/your/myapp/assets/views
warning: [email protected]: - /path/to/your/myapp/assets/views/home
warning: [email protected]: Found asset: /path/to/your/myapp/assets/static/styles.css -> /static/styles.css
warning: [email protected]: Found asset: /path/to/your/myapp/assets/static/dummy.pdf -> /static/dummy.pdf
warning: [email protected]: Found asset: /path/to/your/myapp/assets/static/404.html -> /static/404.html
warning: [email protected]: Found asset: /path/to/your/myapp/assets/views/base.html -> base.html
warning: [email protected]: Found 13 asset files
warning: [email protected]: Generated code for 6 static assets and 7 templates
This output confirms that Loco has found your asset files (like CSS, PDFs, HTML templates) and has generated the necessary code to embed them into the binary. The paths will reflect your project's structure.