diff --git a/docs/src/04_03_components.md b/docs/src/04_03_components.md deleted file mode 100644 index f6ce326..0000000 --- a/docs/src/04_03_components.md +++ /dev/null @@ -1 +0,0 @@ -# Components \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 10dc9c1..61dd723 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -21,7 +21,12 @@ - [Watch Mode](./02_11_watch_mode.md) # Frontend -- [Frontend](./04_frontend.md) - - [Setup](./04_01_setup.md) - - [Starting the Application](./04_02_app_startup.md) - - [Components](./04_03_components.md) +- [Frontend](./frontend/04_frontend.md) + - [Setup](./frontend/04_01_setup.md) + - [Starting the Application](./frontend/04_02_app_startup.md) + - [Components](./frontend/04_03_components.md) + - [Layout Components](./frontend/04_03_01_layout.md) + - [Reusable Components](./frontend/04_03_02_reusable_components.md) + - [State management](./frontend/04_04_state_management.md) + - [Global state](./frontend/04_04_01_global_state.md) + - [Local state](./frontend/04_04_02_local_state.md) diff --git a/docs/src/04_01_setup.md b/docs/src/frontend/04_01_setup.md similarity index 100% rename from docs/src/04_01_setup.md rename to docs/src/frontend/04_01_setup.md diff --git a/docs/src/04_02_app_startup.md b/docs/src/frontend/04_02_app_startup.md similarity index 95% rename from docs/src/04_02_app_startup.md rename to docs/src/frontend/04_02_app_startup.md index 9785603..764f8c6 100644 --- a/docs/src/04_02_app_startup.md +++ b/docs/src/frontend/04_02_app_startup.md @@ -41,9 +41,9 @@ public ├── image3.png └── ... (rest of your images) ``` - - - + + + Now that we've confirmed the directory structure, let's proceed to initialize your application... diff --git a/docs/src/frontend/04_03_01_layout.md b/docs/src/frontend/04_03_01_layout.md new file mode 100644 index 0000000..83ce3ac --- /dev/null +++ b/docs/src/frontend/04_03_01_layout.md @@ -0,0 +1,154 @@ +# Layout Components + +First up, we're going to craft some general layout components for our app. This is a nice, gentle introduction to creating components, and we'll also get some reusable pieces out of it. We're going to create: +- `Header` component +- `Footer` component +- We'll also tweak the `App` component to incorporate these new components + +## Components Folder + +Time to get our code all nice and organized! We're going to make a `components` folder in our `src` directory. This is where we'll store all of our components. This way, we can easily import them into our `main.rs` file. Neat, right? + +If you want to get a deeper understanding of how to structure your code within a Rust project, the Rust Lang book has a fantastic section on it called [Managing Growing Projects with Packages, Crates, and Modules](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html). Definitely worth checking out! + +Here's what our new structure will look like: + +```bash +└── src # Source code + ├── components # Components folder + │ ├── mod.rs # Components module + │ ├── footer.rs # Footer component + │ └── header.rs # Header component +``` + +And let's take a peek at what our `mod.rs` file should look like: + +```rust +mod footer; +mod header; + +pub use footer::Footer; +pub use header::Header; +``` + +We've got our `mod.rs` pulling double duty here. First, it's declaring our `footer` and `header` modules. Then, it's making `Footer` and `Header` available for other modules to use. This sets us up nicely for using these components in our `main.rs` file. + +## Header Component + +Alright, let's start with the `Header` component. For now, we're keeping it simple, just displaying our app's title and a logo. + +Whenever you're building a new component or working in our `main.rs` file, remember to import `dioxus::prelude::*`. It gives you access to all the macros and functions you need. + +> **Note:** You can adjust the Tailwind classes to suit your style. + + +`components/header.rs` +```rust +use dioxus::prelude::*; + +pub fn Header(cx: Scope) -> Element { + cx.render(rsx!( + header { + class: "sticky top-0 z-10 text-gray-400 bg-blue-300 body-font shadow-md", + div { class: "container mx-auto flex flex-wrap p-0 flex-col md:flex-row justify-between items-center", + a { + class: "flex title-font font-medium items-center text-teal-950 mb-4 md:mb-0", + img { + class: "bg-transparent p-2 animate-jump", + alt: "ferris", + src: "ferris.png", + "loading": "lazy" + } + span { class: "ml-3 text-2xl", "Rusty films"} + } + } + } + )) +} +``` + +## Footer Component + +Next up, we're going to build the `Footer` component. This one's pretty straightforward – we're just going to stick a couple of images at the bottom of our app. + +`components/footer.rs` +```rust +use dioxus::prelude::*; + +pub fn Footer(cx: Scope) -> Element { + cx.render(rsx!( + footer { + class: "bg-blue-200 w-full h-16 p-2 box-border gap-6 flex flex-row justify-center items-center text-teal-950", + a { + class: "w-auto h-full", + href: "https://www.devbcn.com/", + target: "_blank", + img { + class: "h-full w-auto", + alt: "DevBcn", + src: "devbcn.png", + "loading": "lazy" + } + } + svg { + fill: "none", + view_box: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-6 h-6", + path { + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M6 18L18 6M6 6l12 12" + } + } + a { + class: "w-auto h-full", + href: "https://www.meetup.com/es-ES/bcnrust/", + target: "_blank", + img { + class: "h-full w-auto", + alt: "BcnRust", + src: "bcnrust.png", + "loading": "lazy" + } + } + } + )) +} +``` + +Just like we did with the `Header` component, remember to import `dioxus::prelude::*` to have access to all the macros and functions we need. And feel free to change up the Tailwind classes to fit your design. + +Now, we've got a `Header` and `Footer` ready to roll. Next, let's update our `App` component to use these new elements. + +```diff +#![allow(non_snake_case)] +// Import the Dioxus prelude to gain access to the `rsx!` macro and the `Scope` and `Element` types. ++mod components; + ++use components::{Footer, Header}; +use dioxus::prelude::*; + + +fn main() { + // Launch the web application using the App component as the root. + dioxus_web::launch(App); +} + +// Define a component that renders a div with the text "Hello, world!" +fn App(cx: Scope) -> Element { + cx.render(rsx! { +- div { +- "Hello, world!" +- } ++ main { ++ Header {} ++ section { ++ class: "md:container md:mx-auto md:py-8 flex-1", ++ } ++ Footer {} ++ } + }) +} +``` diff --git a/docs/src/frontend/04_03_02_reusable_components.md b/docs/src/frontend/04_03_02_reusable_components.md new file mode 100644 index 0000000..62e4915 --- /dev/null +++ b/docs/src/frontend/04_03_02_reusable_components.md @@ -0,0 +1,346 @@ +# 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](https://dioxuslabs.com/docs/0.3/guide/en/describing_ui/component_props.html) + +### 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: + +> **Note:** You can mark a prop as optional using `#[props(!optional)]` +```rust +#[derive(Props)] +pub struct FilmModalProps<'a> { + on_create_or_update: EventHandler<'a, Film>, + on_cancel: EventHandler<'a, MouseEvent>, + #[props(!optional)] + film: Option, +} + +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. + +```rust +#[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! + +> **Note:** 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. + +```rust +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. + +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! + +```bash +└── 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: + +`mod.rs` +```rust +mod button; +mod film; + +pub use button::ButtonType; +pub use film::FilmModalVisibility; +``` + +`button.rs` +```rust +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"), + } + } +} +``` + +`film.rs` +```rust +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? + +## 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. + +`film_card.rs` +```rust +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` and `on_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. + +`film_modal.rs` +```rust +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. + +`main.rs` +```diff +... + +-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 |_| {} ++ } + } + }) +} +``` \ No newline at end of file diff --git a/docs/src/frontend/04_03_components.md b/docs/src/frontend/04_03_components.md new file mode 100644 index 0000000..9ef4508 --- /dev/null +++ b/docs/src/frontend/04_03_components.md @@ -0,0 +1,27 @@ +# Components + +Alright, let's roll up our sleeves and dive into building some reusable components for our app. We'll start with layout components and then craft some handy components that we can use all over our app. + +When you're putting together a component, keep these points in mind: +- Always remember to import `dioxus::prelude::*`. This gives you all the macros and functions you need, right at your fingertips. +- Create a `pub fn` with your chosen component name. +- Your function should include a `cx: Scope` parameter. +- It should return an `Element` type. + +The real meat of our component is in the `cx.render` function. This is where the `rsx!` macro comes into play to create the markup of the component. You can put together your markup using html tags, attributes, and text. + +Inside html tags, you can go wild with any attributes you want. Dioxus has a ton of them ready for you to use. But if you can't find what you're looking for, no problem! You can add it yourself using "double quotes". + +```rust +use dioxus::prelude::*; + +pub fn MyComponent(cx: Scope) -> Element { + cx.render(rsx!( + div { + class: "my-component", + "data-my-attribute": "my value", + "My component" + } + )) +} +``` diff --git a/docs/src/frontend/04_04_01_global_state.md b/docs/src/frontend/04_04_01_global_state.md new file mode 100644 index 0000000..3cd6803 --- /dev/null +++ b/docs/src/frontend/04_04_01_global_state.md @@ -0,0 +1,95 @@ +# Implementing Global State + +To begin, let's create a global state responsible for managing the visibility of our Film Modal. + +We will utilize a functionality similar to React's Context. This approach allows us to establish a context that will be accessible to all components contained within the context provider. To this end, we will construct a `use_shared_state_provider` that will be located within our `App` component. + +The value should be initialized using a closure. + +`main.rs` +```diff ++mod models; + +use components::{FilmModal, Footer, Header}; +use dioxus::prelude::*; ++use models::FilmModalVisibility; +... + +fn App(cx: Scope) -> Element { ++ use_shared_state_provider(cx, || FilmModalVisibility(false)); + +... + +} +``` + +Now, by leveraging the `use_shared_state` hook, we can both retrieve the state and modify it. Therefore, it is necessary to incorporate this hook in locations where we need to read or alter the Film Modal visibility. + +`header.rs` +```diff +... + +pub fn Header(cx: Scope) -> Element { ++ let is_modal_visible = use_shared_state::(cx).unwrap(); + +... + + Button { + button_type: ButtonType::Primary, + onclick: move |_| { + is_modal_visible.write().0 = true; + }, + "Add new film" + } + } + } + )) +} +``` + +The value can be updated using the `write` method, which returns a mutable reference to the value. Consequently, we can use the `=` operator to update the visibility of the Film Modal when the button is clicked. + +`film_modal.rs` +```diff +... +-use crate::models::{ButtonType}; ++use crate::models::{ButtonType, FilmModalVisibility}; +... +pub fn FilmModal<'a>(cx: Scope<'a, FilmModalProps>) -> Element<'a> { ++ let is_modal_visible = use_shared_state::(cx).unwrap(); + +... ++ if !is_modal_visible.read().0 { ++ return None; ++ } +... +} +``` + +This demonstrates an additional concept of Dioxus: dynamic rendering. Essentially, the component is only rendered if the condition is met. +> **Note:** Dynamic rendering is a technique that enables rendering different content based on a condition. Further information can be found in the [Dioxus Dynamic Rendering documentation](https://dioxuslabs.com/docs/0.3/guide/en/interactivity/dynamic_rendering.html) + +`main.rs` +```diff +... + +fn App(cx: Scope) -> Element { + use_shared_state_provider(cx, || FilmModalVisibility(false)); ++ let is_modal_visible = use_shared_state::(cx).unwrap(); + + + ... + cx.render(rsx! { + main { + ... + FilmModal { + on_create_or_update: move |_| {}, + on_cancel: move |_| { ++ is_modal_visible.write().0 = false; + } + } + } + }) +} +``` +In the same manner we open the modal by altering the value, we can also close it. Here, we close the modal when the cancel button is clicked, invoking the `write` method to update the value. \ No newline at end of file diff --git a/docs/src/frontend/04_04_02_local_state.md b/docs/src/frontend/04_04_02_local_state.md new file mode 100644 index 0000000..8f6c9ac --- /dev/null +++ b/docs/src/frontend/04_04_02_local_state.md @@ -0,0 +1,255 @@ +# Local state + +In the context of component state, we typically refer to the local state. Dioxus simplifies the management of a component's state with the `use_state` hook. Noteworthy characteristics of this hook include: + +- State initialization is achieved by passing a closure that returns the initial state. +```rust +let mut count = use_state(cx, || 0); +``` +- The `use_state` hook provides the current value via `get()` and enables its modification using `set()`. +- Each value update triggers a component re-render. + +In the `main.rs` file, the `App` component needs to be updated to introduce some local state. This state will be situated at the top of our app and can be passed to components as props. Our app's local states will consist of: + +- `films`: A list of films. +- `selected_film`: The film to be updated. +- `force_get_films`: A flag that will be employed to force a refetch of the films list from the API. + +> **Note:** We are going to apply dynamic rendering again, this time to render a list of Film Cards only if the films list is not empty. + +`main.rs` +```diff +... + +fn App(cx: Scope) -> Element { + use_shared_state_provider(cx, || FilmModalVisibility(false)); ++ let films = use_state::>>(cx, || None); ++ let selected_film = use_state::>(cx, || None); ++ let force_get_films = use_state(cx, || ()); + +... + + cx.render(rsx! { + main { + ... + section { + class: "md:container md:mx-auto md:py-8 flex-1", ++ if let Some(films) = films.get() { ++ rsx!( ++ ul { ++ class: "flex flex-row justify-center items-stretch gap-4 flex-wrap", ++ {films.iter().map(|film| { ++ rsx!( ++ FilmCard { ++ key: "{film.id}", ++ film: film, ++ on_edit: move |_| { ++ selected_film.set(Some(film.clone())); ++ is_modal_visible.write().0 = true ++ }, ++ on_delete: move |_| {} ++ } ++ ) ++ })} ++ } ++ ) ++ } + } + ... + } + FilmModal { ++ film: selected_film.get().clone(), + on_create_or_update: move |new_film| {}, + on_cancel: move |_| { ++ selected_film.set(None); ++ is_modal_visible.write().0 = false; + } + } + }) +} +``` + +As you can observe, the Film Modal is opened when the `FilmCard` edit button is clicked. Additionally, the selected **film** is passed as a prop to the `FilmModal` component. + +We will implement the delete film feature later. + +The `FilmModal` component also undergoes an update in the `on_cancel` callback to clear the selected film and close the modal, in case we decide not to create or update a film. + +> **Note:** We utilize the `clone` method to generate a copy of the selected film. This is because we're employing the same film object in the `FilmCard`. Remember, ownership rules apply![MEH a revisar] + +Finally, it's essential to modify the `FilmModal` component to: + +- Accept the selected film as a prop. +- Add a `draft_film` local state to contain the film that will be created or updated. +- Refresh the `on_cancel` callback to clear the `draft_film` and close the modal. +- Update the + + `on_create_or_update` callback to create or update the `draft_film` and close the modal. +- Assign values and change handlers to the input fields. + +```diff +use dioxus::prelude::*; ++use shared::models::Film; ++use uuid::Uuid; + +use crate::components::Button; +use crate::models::{ButtonType, FilmModalVisibility}; + +#[derive(Props)] +pub struct FilmModalProps<'a> { + on_create_or_update: EventHandler<'a, MouseEvent>, + on_cancel: EventHandler<'a, MouseEvent>, ++ #[props(!optional)] ++ film: Option, +} + +pub fn FilmModal<'a>(cx: Scope<'a, FilmModalProps>) -> Element<'a> { + let is_modal_visible = use_shared_state::(cx).unwrap(); ++ let draft_film = use_state::(cx, || Film { ++ title: "".to_string(), ++ poster: "".to_string(), ++ director: "".to_string(), ++ year: 1900, ++ id: Uuid::new_v4(), ++ created_at: None, ++ updated_at: None, ++ }); + + if !is_modal_visible.read().0 { + return None; + } + 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", ++ value: "{draft_film.get().title}", ++ oninput: move |evt| { ++ draft_film.set(Film { ++ title: evt.value.clone(), ++ ..draft_film.get().clone() ++ }) ++ } + } + } + 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", ++ value: "{draft_film.get().director}", ++ oninput: move |evt| { ++ draft_film.set(Film { ++ director: evt.value.clone(), ++ ..draft_film.get().clone() ++ }) ++ } + } + } + 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", ++ value: "{draft_film.get().year.to_string()}", ++ oninput: move |evt| { ++ draft_film.set(Film { ++ year: evt.value.clone().parse::().unwrap_or(1900), ++ ..draft_film.get + +().clone() ++ }) ++ } + } + } + 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", ++ value: "{draft_film.get().poster}", ++ oninput: move |evt| { ++ draft_film.set(Film { ++ poster: evt.value.clone(), ++ ..draft_film.get().clone() ++ }) ++ } + } + } + } + footer { + class: "flex flex-row justify-center items-center mt-4 gap-x-2", + Button { + button_type: ButtonType::Secondary, + onclick: move |evt| { ++ draft_film.set(Film { ++ title: "".to_string(), ++ poster: "".to_string(), ++ director: "".to_string(), ++ year: 1900, ++ id: Uuid::new_v4(), ++ created_at: None, ++ updated_at: None, ++ }); + cx.props.on_cancel.call(evt) + }, + "Cancel" + } + Button { + button_type: ButtonType::Primary, + onclick: move |evt| { +- cx.props.on_create_or_update.call(evt); ++ cx.props.on_create_or_update.call(draft_film.get().clone()); ++ draft_film.set(Film { ++ title: "".to_string(), ++ poster: "".to_string(), ++ director: "".to_string(), ++ year: 1900, ++ id: Uuid::new_v4(), ++ created_at: None, ++ updated_at: None, ++ }); + }, + "Save film" + } + } + } + + } + )) +} +``` diff --git a/docs/src/frontend/04_04_state_management.md b/docs/src/frontend/04_04_state_management.md new file mode 100644 index 0000000..f9a6f05 --- /dev/null +++ b/docs/src/frontend/04_04_state_management.md @@ -0,0 +1,20 @@ +# State Management + +In this part of our journey, we're going to dive into the lifeblood of the application — state management. We'll tackle this crucial aspect in two stages: local state management and global state management. + +While we're only scratching the surface to get the application up and running, it's highly recommended that you refer to the [Dioxus Interactivity](https://dioxuslabs.com/docs/0.3/guide/en/interactivity/index.html) documentation. This way, you'll not only comprehend how it operates more fully, but also grasp the extensive capabilities the framework possesses. + +For now, let's start with the basics. Dioxus as is very influenced by React and its ecosystem, so it's no surprise that it uses the same approach to state management, Hooks. +Hooks are Rust functions that take a reference to ScopeState (in a component, you can pass cx), and provide you with functionality and state. Dioxus allows hooks to maintain state across renders through a reference to ScopeState, which is why you must pass &cx to them. + +But wait! We can't use Hooks everywhere, there are some rules we must follow: + +## Rules of Hooks + +1. Hooks may be only used in components or other hooks +2. On every call to the component function + 1. The same hooks must be called + 2. In the same order +3. Hooks name's should start with `use_` so you don't accidentally confuse them with regular functions + + diff --git a/docs/src/04_frontend.md b/docs/src/frontend/04_frontend.md similarity index 93% rename from docs/src/04_frontend.md rename to docs/src/frontend/04_frontend.md index 5c6dcfb..abd7fcf 100644 --- a/docs/src/04_frontend.md +++ b/docs/src/frontend/04_frontend.md @@ -1,8 +1,8 @@ # Frontend -In this guide, we'll be using [Dioxus](https://dioxuslabs.com/) as the frontend for our project. Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust. Heavily inspired by React, Dioxus allows you to build apps for the Web, Desktop, Mobile, and more. Its core implementation can run anywhere with no platform-dependent linking, which means it's not intrinsically linked to WebSys like many other Rust frontend toolkits. However, it's important to note that Dioxus hasn't reached a stable release yet, so some APIs, particularly for Desktop, may still be in flux​1​. +In this guide, we'll be using [Dioxus](https://dioxuslabs.com/) as the frontend for our project. Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust. Heavily inspired by React, Dioxus allows you to build apps for the Web, Desktop, Mobile, and more. Its core implementation can run anywhere with no platform-dependent linking, which means it's not intrinsically linked to WebSys like many other Rust frontend toolkits. However, it's important to note that Dioxus hasn't reached a stable release yet, so some APIs, particularly for Desktop, may still be inestable. -As for styling our app, we'll be using [Tailwind CSS](https://tailwindcss.com/). Tailwind is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override. You can set it up in your project, build something with it in an online playground, and even learn more about it directly from the team on their channel. Tailwind also offers a set of beautiful UI components crafted by its creators to help you speed up your development process​2​. +As for styling our app, we'll be using [Tailwind CSS](https://tailwindcss.com/). Tailwind is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override. You can set it up in your project, build something with it in an online playground, and even learn more about it directly from the team on their channel. Tailwind also offers a set of beautiful UI components crafted by its creators to help you speed up your development process​. This combination of tools will allow us to concentrate our energy on frontend development in Rust, rather than spending excessive time on styling our app.