Building Pages
One way to build up an application is to think of it in terms of pages. Later we can split the pages into re-usable components but let's start with a simple page.
The Dioxus framework gives us the capability to build user interfaces out of components which can be rendered server side. It's worth looking at their components documentation.
I looked at lots of Rust markup and component libraries. Dioxus stood out as having a lightweight syntax and the ability to create components in a similar way to React.
Creating a web-pages crate
cargo init --lib crates/web-pages
Install Dioxus
cd crates/web-pages cargo add [email protected] --no-default-features -F macro,html,signals cargo add [email protected] --no-default-features cargo add --path ../db
Creating a Layout Component
A layout defines the surroundings of an HTML page. It's the place to define a common look and feel of your final output.
create a file called crates/web-pages/src/layout.rs
.
It's a big one as I've added some styling which we won't use until later.
#![allow(non_snake_case)] use dioxus::prelude::*; #[component] pub fn Layout(title: String, children: Element) -> Element { rsx! { BaseLayout { title, stylesheets: vec![], header: rsx!(), sidebar: rsx!(), sidebar_header: rsx!(), sidebar_footer: rsx!(), children, } } } #[derive(Props, Clone, PartialEq)] pub struct BaseLayoutProps { title: String, fav_icon_src: Option<String>, stylesheets: Vec<String>, js_href: Option<String>, header: Element, children: Element, sidebar: Element, sidebar_footer: Element, sidebar_header: Element, } pub fn BaseLayout(props: BaseLayoutProps) -> Element { rsx!( head { title { "{props.title}" } meta { charset: "utf-8" } meta { "http-equiv": "X-UA-Compatible", content: "IE=edge" } meta { name: "viewport", content: "width=device-width, initial-scale=1" } for href in &props.stylesheets { link { rel: "stylesheet", href: "{href}", "type": "text/css" } } if let Some(js_href) = props.js_href { script { "type": "module", src: "{js_href}" } } if let Some(fav_icon_src) = props.fav_icon_src { link { rel: "icon", "type": "image/svg+xml", href: "{fav_icon_src}" } } } body { div { class: "flex h-screen overflow-hidden", nav { id: "sidebar", class: " border-r border-base-300 fixed bg-base-200 inset-y-0 left-0 w-64 transform -translate-x-full transition-transform duration-200 ease-in-out flex flex-col lg:translate-x-0 lg:static lg:inset-auto lg:transform-none z-20", div { class: "flex items-center p-4", {props.sidebar_header} } div { class: "flex-1 overflow-y-auto", {props.sidebar} } div { class: "p-4", {props.sidebar_footer} } } main { id: "main-content", class: "flex-1 flex flex-col", header { class: "flex items-center p-4 border-b border-base-300", button { id: "toggleButton", svg { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "lucide lucide-panel-left", rect { width: "18", height: "18", x: "3", y: "3", rx: "2", } path { d: "M9 3v18", } } } {props.header} } section { class: "flex-1 overflow-y-auto", {props.children} } } } } ) }
Let's use this layout to create a very simple users screen that will show a table of users.
Create a file crates/web-pages/src/root.rs
. we call it root.rs
because it's the root of our routes. If we had a route such as customers
we would call it customers.rs
.
use crate::{layout::Layout, render}; use db::User; use dioxus::prelude::*; use web_assets::files::favicon_svg; pub fn index(users: Vec<User>) -> String { let page = rsx! { Layout { // <-- Use our layout title: "Users Table", table { thead { tr { th { "ID" } th { "Email" } } } tbody { for user in users { tr { td { strong { "{user.id}" } } td { "{user.email}" } } } } } } }; render(page) }
If we update our crates/web-pages/src/lib.rs
to look like the following...
mod layout; pub mod root; use dioxus::prelude::*; pub fn render_page(page: Element) -> String { let html = dioxus_ssr::render_element(page); format!("<!DOCTYPE html><html lang='en'>{}</html>", html) }
Then finally we can change our web-server
code to generate HTML rather than JSON.
Make sure you're in the crates/web-server
folder and add the web-pages
crate to your Cargo.toml
using the following command:
cargo add --path ../web-pages
Update crates/web-server/src/root.rs
use crate::errors::CustomError; use axum::{response::Html, Extension}; use web_pages::root; pub async fn loader(Extension(pool): Extension<db::Pool>) -> Result<Html<String>, CustomError> { let client = pool.get().await?; let users = db::queries::users::get_users().bind(&client).all().await?; let html = root::index(users); Ok(Html(html)) }
You should get results like the screenshot below.