Crafting Reusable Components
Let's turn up the heat in this section and start creating some more complex components for our app. Our assembly line will produce:
- A quick run-through on component props
- A Button that can be used anywhere in our app
- A Film Card to display details about a film
- A Film Modal for creating or updating films
Props
Before we start building, let's break down how we're going to define props in our components. We'll be doing this using two methods: struct
and inline
Props. The main difference between them lies in their location. struct
Props are defined outside in a struct with prop macros and we attach the generic to our Scope
type. On the other hand, inline
Props are tucked right into the component function params. If you're craving more details about this, you can have a peek at the Dioxus Props documentation
Struct Props
These kinds of props are defined separately from the component function, and the generic type needs to be hooked onto the Scope
type. We use the #[derive(Props)]
macro to define the props:
#![allow(unused)] fn main() { #[derive(Props)] pub struct FilmModalProps<'a> { on_create_or_update: EventHandler<'a, Film>, on_cancel: EventHandler<'a, MouseEvent>, #[props(!optional)] film: Option<Film>, } pub fn FilmModal<'a>(cx: Scope<'a, FilmModalProps>) -> Element<'a> { ... } }
Inline Props
Inline props are defined within the component function params. A nice plus is that you can access the prop
variable directly inside the component, while struct props need a bit of navigation like cx.props.my_prop
.
For these props, we tag the component function with the #[inline_props]
macro.
#![allow(unused)] fn main() { #[inline_props] pub fn FilmCard<'a>( cx: Scope<'a>, film: &'a Film, on_edit: EventHandler<'a, MouseEvent>, on_delete: EventHandler<'a, MouseEvent>, ) -> Element { ... } }
Alright, now that we've got props figured out, let's start building some components!
When you want to use props inside your components, here's how to do it: "{cx.props.my_prop}"
, "{my_prop}"
, or "{prop.to_string()}"
. Make sure to keep the curly braces and the prop name as shown.
Button
First up, we're creating a button. Since we'll be using this in various spots, it's a smart move to make it a reusable component.
front/src/components/button.rs
#![allow(unused)] fn main() { use dioxus::prelude::*; use crate::models::ButtonType; #[inline_props] pub fn Button<'a>( cx: Scope<'a>, button_type: ButtonType, onclick: EventHandler<'a, MouseEvent>, children: Element<'a>, ) -> Element { cx.render(rsx!(button { class: "text-slate-200 inline-flex items-center border-0 py-1 px-3 focus:outline-none rounded mt-4 md:mt-0 {button_type.to_string()}", onclick: move |event| onclick.call(event), children })) } }
Notice that we're importing models::ButtonType
here. This is an enum that helps us define the different button types we might use in our app. By using this, we can easily switch up the button styles based on our needs.
Button props are pretty straightforward.
button_type
prop that takes aButtonType
enum and assign the right Tailwind classes to the button.onclick
prop that takes anEventHandler
for the click event, and achildren
prop that takes anElement
for the button text, icon or whateverElement
desired.
Just like we did with the components, we're going to set up a models folder inside our frontend directory. Here, we'll create a button.rs
file to hold our Button models. While we're at it, let's also create a film.rs
file for our Film models. We'll need those soon!
└── src # Source code
├── models # Models folder
│ ├── mod.rs # Models module
│ ├── button.rs # Button models
│ └── film.rs # Film models
Here's what we're working with for these files:
front/src/models/mod.rs
#![allow(unused)] fn main() { mod button; mod film; pub use button::ButtonType; pub use film::FilmModalVisibility; }
front/src/models/button.rs
#![allow(unused)] fn main() { use std::fmt; pub enum ButtonType { Primary, Secondary, } impl fmt::Display for ButtonType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ButtonType::Primary => write!(f, "bg-blue-700 hover:bg-blue-800 active:bg-blue-900"), ButtonType::Secondary => write!(f, "bg-rose-700 hover:bg-rose-800 active:bg-rose-900"), } } } }
front/src/models/film.rs
#![allow(unused)] fn main() { pub struct FilmModalVisibility(pub bool); }
But wait, what's that impl
thing in button.rs
? This is where Rust's implementation blocks come in. We're using impl
to add methods to our ButtonType
enum. Specifically, we're implementing the Display
trait, which gives us a standard way to display our enum as a string. The fmt
method determines how each variant of the enum should be formatted as a string. So, when we use button_type.to_string()
in our Button component, it will return the right Tailwind classes based on the button type. Handy, right?
Update components module
Add the button
module to the components
module.
front/src/components/mod.rs
+mod button;
mod footer;
mod header;
+pub use button::Button;
pub use footer::Footer;
pub use header::Header;
Film Card
Moving along, our next creation is the Film Card component. Its role is to present the specifics of a film in our list. Moreover, it will integrate a pair of Button components allowing us to edit and delete the film.
front/src/components/film_card.rs
#![allow(unused)] fn main() { use crate::{components::Button, models::ButtonType}; use dioxus::prelude::*; use shared::models::Film; #[inline_props] pub fn FilmCard<'a>( cx: Scope<'a>, film: &'a Film, on_edit: EventHandler<'a, MouseEvent>, on_delete: EventHandler<'a, MouseEvent>, ) -> Element { cx.render(rsx!( li { class: "film-card md:basis-1/4 p-4 rounded box-border bg-neutral-100 drop-shadow-md transition-all ease-in-out hover:drop-shadow-xl flex-col flex justify-start items-stretch animate-fade animate-duration-500 animate-ease-in-out animate-normal animate-fill-both", header { img { class: "max-h-80 w-auto mx-auto rounded", src: "{film.poster}" }, } section { class: "flex-1", h3 { class: "text-lg font-bold my-3", "{film.title}" } p { "{film.director}" } p { class: "text-sm text-gray-500", "{film.year.to_string()}" } } footer { class: "flex justify-end space-x-2 mt-auto", Button { button_type: ButtonType::Secondary, onclick: move |event| on_delete.call(event), svg { fill: "none", stroke: "currentColor", stroke_width: "1.5", view_box: "0 0 24 24", class: "w-5 h-5", path { stroke_linecap: "round", stroke_linejoin: "round", d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" } } } Button { button_type: ButtonType::Primary, onclick: move |event| on_edit.call(event), svg { fill: "none", stroke: "currentColor", stroke_width: "1.5", view_box: "0 0 24 24", class: "w-5 h-5", path { stroke_linecap: "round", stroke_linejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" } } } } } )) } }
This Film Card component is indeed more intricate than the Button component, due to its wider use of Tailwind classes and the incorporation of event handlers. Let's dissect this a bit:
on_edit
andon_delete
are event handlers that we introduce into the component. They are responsible for managing the click events on the edit and delete buttons respectively.film
is a reference to the film whose details we are exhibiting in the card.
Film Modal
As the grand finale of our components building phase, we're constructing the Film Modal component. This vital piece will facilitate the creation or update of a film. Its appearance will be commanded by a button located in the app's header or the edit
button inside the Film Card.
front/src/components/film_modal.rs
#![allow(unused)] fn main() { use dioxus::prelude::*; use crate::components::Button; use crate::models::{ButtonType}; #[derive(Props)] pub struct FilmModalProps<'a> { on_create_or_update: EventHandler<'a, MouseEvent>, on_cancel: EventHandler<'a, MouseEvent>, } pub fn FilmModal<'a>(cx: Scope<'a, FilmModalProps>) -> Element<'a> { cx.render(rsx!( article { class: "z-50 w-full h-full fixed top-0 right-0 bg-gray-800 bg-opacity-50 flex flex-col justify-center items-center", section { class: "w-1/3 h-auto bg-white rounded-lg flex flex-col justify-center items-center box-border p-6", header { class: "mb-4", h2 { class: "text-xl text-teal-950 font-semibold", "🎬 Film" } } form { class: "w-full flex-1 flex flex-col justify-stretch items-start gap-y-2", div { class: "w-full", label { class: "text-sm font-semibold", "Title" } input { class: "w-full border border-gray-300 rounded-lg p-2", "type": "text", placeholder: "Enter film title", } } div { class: "w-full", label { class: "text-sm font-semibold", "Director" } input { class: "w-full border border-gray-300 rounded-lg p-2", "type": "text", placeholder: "Enter film director", } } div { class: "w-full", label { class: "text-sm font-semibold", "Year" } input { class: "w-full border border-gray-300 rounded-lg p-2", "type": "number", placeholder: "Enter film year", } } div { class: "w-full", label { class: "text-sm font-semibold", "Poster" } input { class: "w-full border border-gray-300 rounded-lg p-2", "type": "text", placeholder: "Enter film poster URL", } } } footer { class: "flex flex-row justify-center items-center mt-4 gap-x-2", Button { button_type: ButtonType::Secondary, onclick: move |evt| { cx.props.on_cancel.call(evt) }, "Cancel" } Button { button_type: ButtonType::Primary, onclick: move |evt| { cx.props.on_create_or_update.call(evt); }, "Save film" } } } } )) } }
At the moment, we're primarily focusing on establishing the basic structural framework of the modal. We'll instill the logic in the upcoming section. The current modal props comprise on_create_or_update and on_cancel. These event handlers are key to managing the click events associated with modal actions.
on_create_or_update
: This handler is in charge of creating or updating a film.on_cancel
: This one takes responsibility for shutting down the modal and aborting any ongoing film modification or creation.
Let's update our main.rs
file to include the Film Modal component. Film Card component will be added later.
front/src/main.rs
#![allow(non_snake_case)]
// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types
+mod components;
+mod models;
...
-use components::{Footer, Header};
+use components::{FilmModal, Footer, Header};
...
fn App(cx: Scope) -> Element {
...
cx.render(rsx! {
main {
...
+ FilmModal {
+ on_create_or_update: move |_| {},
+ on_cancel: move |_| {}
+ }
}
})
}