+++
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.
* **The controller** is handling requests parsing payload, and then control flows to models
* **The model** primarily deals with communicating with the database and executing CRUD operations when required. As well as modeling all business and domain logic and operations.
* **The view** takes on the responsibility of assembling and rendering the final response to be sent back to the client.
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.
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.
## JSON views
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:
1. Parse, accept the request
2. Create domain objects: models
3. Hand off the domain model to a view object which **shapes** the final response
The following Rust code represents a controller responsible for handling user login requests, which handes off _shaping_ of the response to `LoginResponse`.
```rust
use crate::{views::auth::LoginResponse};
async fn login(
State(ctx): State,
Json(params): Json,
) -> Result {
// 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`:
```rust
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(),
}
}
}
```
## Template views
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](http://keats.github.io/tera/) template engine.
To use this engine you need to verify that you have a ViewEngineInitializer
in initializers/view_engine.rs
which is also specified in your app.rs
. If you used the SaaS Starter, this should already be configured for you.
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/
```
### Creating a new view
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
find this tera template at assets/views/home/hello.html
:
{{ /* t(key="hello-world", lang="en-US") */ }},
{{ /* t(key="hello-world", lang="de-DE") */ }}
```
Now create a strongly typed `view` to encapsulate this template in `src/views/dashboard.rs`:
```rust
// src/views/dashboard.rs
use loco_rs::prelude::*;
pub fn home(v: impl ViewRenderer) -> Result {
format::render().view(&v, "home/hello.html", data!({}))
}
```
And add it to `src/views/mod.rs`:
```rust
pub mod dashboard;
```
Next, go to your controller and use the view:
```rust
// src/controllers/dashboard.rs
use loco_rs::prelude::*;
use crate::views;
pub async fn render_home(ViewEngine(v): ViewEngine) -> Result {
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`
```rust
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
```
### How does it work?
* `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::*`
* Controllers need to deal with getting a request, calling some model logic, and then supplying a view with **models and other data**, not caring about how the view does its thing
* `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 Thing_
* Should you ever want to swap a view engine, the encapsulation here works like magic. You can change the extractor type: `ViewEngine` and everything works, because `v` is eventually just a `ViewRenderer` trait
### Static assets
If you want to serve static assets and reference those in your view templates, you can use the _Static Middleware_, configure it this way:
```yaml
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:
```html
```
### Customizing the Tera view engine
The Tera view engine comes with the following configuration:
* Template loading and location: `assets/**/*.html`
* Internationalization (i18n) configured into the Tera view engine, you get the translation function: `t(..)` to use in your templates
If 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:
* Add custom Tera functions
* Remove the `i18n` library
* Change configuration for Tera or the `i18n` library
* Provide a new or custom, Tera (maybe a different version) instance
### Using your own view engine
If 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_.
```rust
// src/initializers/hello_view_engine.rs
use axum::{async_trait, Extension, Router as AxumRouter};
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(&self, _key: &str, _data: S) -> Result {
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 {
Ok(router.layer(Extension(ViewEngine::from(HelloView))))
}
}
```
To use it, you need to add it to your `src/app.rs` hooks:
```rust
// src/app.rs
// add your custom "hello" view engine in the `initializers(..)` hook
impl Hooks for App {
// ...
async fn initializers(_ctx: &AppContext) -> Result>> {
Ok(vec![
// ,.----- add it here
Box::new(initializers::hello_view_engine::HelloViewEngineInitializer),
])
}
// ...
```
### Tera Built-ins
Loco includes Tera with its [built-ins](https://keats.github.io/tera/docs/#built-ins) functions. In addition, Loco introduces the following custom built-in functions:
To see Loco built-in function:
* [numbers](https://docs.rs/loco-rs/latest/loco_rs/controller/views/tera_builtins/filters/number/index.html)