From 299c5da2b8a32227515119b45396033fff4fcd9d Mon Sep 17 00:00:00 2001 From: piny4man Date: Thu, 29 Jun 2023 23:43:59 +0200 Subject: [PATCH 1/9] docs: frontend layout components section --- docs/src/04_03_components.md | 1 - docs/src/SUMMARY.md | 9 +- docs/src/{ => frontend}/04_01_setup.md | 0 docs/src/{ => frontend}/04_02_app_startup.md | 0 docs/src/frontend/04_03_01_layout.md | 154 +++++++++++++++++++ docs/src/frontend/04_03_components.md | 27 ++++ docs/src/{ => frontend}/04_frontend.md | 4 +- 7 files changed, 188 insertions(+), 7 deletions(-) delete mode 100644 docs/src/04_03_components.md rename docs/src/{ => frontend}/04_01_setup.md (100%) rename docs/src/{ => frontend}/04_02_app_startup.md (100%) create mode 100644 docs/src/frontend/04_03_01_layout.md create mode 100644 docs/src/frontend/04_03_components.md rename docs/src/{ => frontend}/04_frontend.md (93%) 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 1b77ab4..72a17e3 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -6,7 +6,8 @@ - [Prerequisites](./01_prerequisites.md) - [Project structure](./02_project_structure.md) -- [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) 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 100% rename from docs/src/04_02_app_startup.md rename to docs/src/frontend/04_02_app_startup.md 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..9d5f9e8 --- /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. + +> Heads up: 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_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/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. From 4adac4e5ebb8623a06dfbdd497005d9a7d899dc3 Mon Sep 17 00:00:00 2001 From: piny4man Date: Fri, 30 Jun 2023 00:09:31 +0200 Subject: [PATCH 2/9] wip: reusable components --- docs/src/SUMMARY.md | 1 + docs/src/frontend/04_03_01_layout.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 72a17e3..7924946 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -11,3 +11,4 @@ - [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) diff --git a/docs/src/frontend/04_03_01_layout.md b/docs/src/frontend/04_03_01_layout.md index 9d5f9e8..786dfba 100644 --- a/docs/src/frontend/04_03_01_layout.md +++ b/docs/src/frontend/04_03_01_layout.md @@ -7,7 +7,7 @@ First up, we're going to craft some general layout components for our app. This ## 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? +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! From 3394fce184f4d24e966e23d75df5570679a9ed8e Mon Sep 17 00:00:00 2001 From: piny4man Date: Fri, 30 Jun 2023 00:10:03 +0200 Subject: [PATCH 3/9] add missing files --- .../frontend/04_03_02_reusable_components.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/src/frontend/04_03_02_reusable_components.md 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..011814f --- /dev/null +++ b/docs/src/frontend/04_03_02_reusable_components.md @@ -0,0 +1,80 @@ +# Creating Reusable Components + +In this section, we'll be stepping up our game a bit and creating the rest of the components for our app. The lineup includes: + +- A Button that we can use all over the place +- A Film Card for showcasing films +- A Film Modal for creating or updating films + +## Button + +First up, we're tackling the 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? \ No newline at end of file From f458c0c27570af6e29f27780dcbad97679d41fc9 Mon Sep 17 00:00:00 2001 From: piny4man Date: Sat, 1 Jul 2023 17:32:05 +0200 Subject: [PATCH 4/9] docs: film_card component --- docs/src/frontend/04_03_01_layout.md | 2 +- .../frontend/04_03_02_reusable_components.md | 143 +++++++++++++++++- 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/docs/src/frontend/04_03_01_layout.md b/docs/src/frontend/04_03_01_layout.md index 786dfba..83ce3ac 100644 --- a/docs/src/frontend/04_03_01_layout.md +++ b/docs/src/frontend/04_03_01_layout.md @@ -39,7 +39,7 @@ Alright, let's start with the `Header` component. For now, we're keeping it simp 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. -> Heads up: You can adjust the Tailwind classes to suit your style. +> **Note:** You can adjust the Tailwind classes to suit your style. `components/header.rs` diff --git a/docs/src/frontend/04_03_02_reusable_components.md b/docs/src/frontend/04_03_02_reusable_components.md index 011814f..45be00b 100644 --- a/docs/src/frontend/04_03_02_reusable_components.md +++ b/docs/src/frontend/04_03_02_reusable_components.md @@ -1,14 +1,60 @@ -# Creating Reusable Components +# Crafting Reusable Components -In this section, we'll be stepping up our game a bit and creating the rest of the components for our app. The lineup includes: +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 Button that we can use all over the place -- A Film Card for showcasing films +- 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 tackling the button. Since we'll be using this in various spots, it's a smart move to make it a reusable component. +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::*; @@ -77,4 +123,89 @@ impl fmt::Display for ButtonType { 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? \ No newline at end of file +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. \ No newline at end of file From 7e519171040dff527842c23b79a900915c25a231 Mon Sep 17 00:00:00 2001 From: piny4man Date: Sat, 1 Jul 2023 18:15:13 +0200 Subject: [PATCH 5/9] docs: film_modal component --- .../frontend/04_03_02_reusable_components.md | 114 +++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/src/frontend/04_03_02_reusable_components.md b/docs/src/frontend/04_03_02_reusable_components.md index 45be00b..9fe73d3 100644 --- a/docs/src/frontend/04_03_02_reusable_components.md +++ b/docs/src/frontend/04_03_02_reusable_components.md @@ -208,4 +208,116 @@ pub fn FilmCard<'a>( 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. \ No newline at end of file +- `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 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>, +} + +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. \ No newline at end of file From c730fa2f6eec14be50e0b1d090ae5db543f20734 Mon Sep 17 00:00:00 2001 From: piny4man Date: Sat, 1 Jul 2023 18:20:56 +0200 Subject: [PATCH 6/9] docs: film_modal component --- docs/src/SUMMARY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 7924946..1500251 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -12,3 +12,4 @@ - [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) From 26fb89ddc2288862461b0d79adee2c7d173e91ec Mon Sep 17 00:00:00 2001 From: piny4man Date: Sat, 1 Jul 2023 18:21:50 +0200 Subject: [PATCH 7/9] docs: add state_management root --- docs/src/frontend/04_04_state_management.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/src/frontend/04_04_state_management.md 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..1fbbdc6 --- /dev/null +++ b/docs/src/frontend/04_04_state_management.md @@ -0,0 +1,2 @@ +# State management + From 428afd46537e867cfc98f35497e59dc87c8c77ae Mon Sep 17 00:00:00 2001 From: piny4man Date: Sat, 1 Jul 2023 21:27:58 +0200 Subject: [PATCH 8/9] wip: sstate management docs --- docs/src/SUMMARY.md | 2 + docs/src/frontend/04_02_app_startup.md | 6 +- .../frontend/04_03_02_reusable_components.md | 27 +++++- docs/src/frontend/04_04_01_global_state.md | 92 +++++++++++++++++++ docs/src/frontend/04_04_02_local_state.md | 26 ++++++ docs/src/frontend/04_04_state_management.md | 20 +++- 6 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 docs/src/frontend/04_04_01_global_state.md create mode 100644 docs/src/frontend/04_04_02_local_state.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 1500251..063cd34 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -13,3 +13,5 @@ - [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/frontend/04_02_app_startup.md b/docs/src/frontend/04_02_app_startup.md index 9785603..764f8c6 100644 --- a/docs/src/frontend/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_02_reusable_components.md b/docs/src/frontend/04_03_02_reusable_components.md index 9fe73d3..3e92f76 100644 --- a/docs/src/frontend/04_03_02_reusable_components.md +++ b/docs/src/frontend/04_03_02_reusable_components.md @@ -320,4 +320,29 @@ pub fn FilmModal<'a>(cx: Scope<'a, FilmModalProps>) -> Element<'a> { 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. \ No newline at end of file +- `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_04_01_global_state.md b/docs/src/frontend/04_04_01_global_state.md new file mode 100644 index 0000000..08b0161 --- /dev/null +++ b/docs/src/frontend/04_04_01_global_state.md @@ -0,0 +1,92 @@ +# Global state + +Let's start simple. We will create a global state that will be responsible to store our Film Modal visibility. + +For accomplish this we will use the something similar as React Context. This hook allows us to create a context that will be available for all the components that are inside the context provider. So we will create a `use_shared_state_provider` that will be placed inside our `App` component. + +It should initialize the value 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 we can use the `use_shared_state` hook to get the state and update it. So let's also add this hook where we need to be able to read or update 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" + } + } + } + )) +} +``` + +`film_modal.rs` +```diff +... +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; ++ } +... +} +``` + +In this case we also can see other concept of Dioxus, dynamic rendering. Basically we are rendering the component only if the condition is true. +> **Note:** Dynamic rendering is a technique that allows you to render different content depending on a condition. You can find more information on [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; + } + } + } + }) +} +``` + 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..58286c7 --- /dev/null +++ b/docs/src/frontend/04_04_02_local_state.md @@ -0,0 +1,26 @@ +# Local state + +When we are talking about local state is better to talk about **Component** state. Fortunately, Dioxus provides a simple way to manage the state of a component using `use_state` hook. + +The particularity of this hook are the following: +- To initialize the state you must pass a closure that returns the initial state +```rust +let mut count = use_state(cx, || 0); +``` +- `use_state` gives you the current value with `get()` and a way to update it with `set()` +- Is important to know that on every value update will trigger a component re-render + +Let's start by updating our `main.rs`, concretely the `App` component. We will add some local state that will be placed at the top of our app, and can be passed to components as props. + +```diff +... + +fn App(cx: Scope) -> Element { ++ let films = use_state::>>(cx, || None); ++ let selected_film = use_state::>(cx, || None); ++ let force_get_films = use_state(cx, || ()); + +... + +} +``` \ No newline at end of file diff --git a/docs/src/frontend/04_04_state_management.md b/docs/src/frontend/04_04_state_management.md index 1fbbdc6..f9a6f05 100644 --- a/docs/src/frontend/04_04_state_management.md +++ b/docs/src/frontend/04_04_state_management.md @@ -1,2 +1,20 @@ -# State management +# 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 + From 458074bf3d2ff4e4076a2a95eea0f50bbf4e1e3d Mon Sep 17 00:00:00 2001 From: piny4man Date: Sun, 2 Jul 2023 00:49:15 +0200 Subject: [PATCH 9/9] docs: state management --- .../frontend/04_03_02_reusable_components.md | 4 +- docs/src/frontend/04_04_01_global_state.md | 27 +- docs/src/frontend/04_04_02_local_state.md | 249 +++++++++++++++++- 3 files changed, 255 insertions(+), 25 deletions(-) diff --git a/docs/src/frontend/04_03_02_reusable_components.md b/docs/src/frontend/04_03_02_reusable_components.md index 3e92f76..62e4915 100644 --- a/docs/src/frontend/04_03_02_reusable_components.md +++ b/docs/src/frontend/04_03_02_reusable_components.md @@ -217,11 +217,9 @@ As the grand finale of our components building phase, we're constructing the Fil `film_modal.rs` ```rust use dioxus::prelude::*; -use shared::models::Film; -use uuid::Uuid; use crate::components::Button; -use crate::models::{ButtonType, FilmModalVisibility}; +use crate::models::{ButtonType}; #[derive(Props)] pub struct FilmModalProps<'a> { diff --git a/docs/src/frontend/04_04_01_global_state.md b/docs/src/frontend/04_04_01_global_state.md index 08b0161..3cd6803 100644 --- a/docs/src/frontend/04_04_01_global_state.md +++ b/docs/src/frontend/04_04_01_global_state.md @@ -1,10 +1,10 @@ -# Global state +# Implementing Global State -Let's start simple. We will create a global state that will be responsible to store our Film Modal visibility. +To begin, let's create a global state responsible for managing the visibility of our Film Modal. -For accomplish this we will use the something similar as React Context. This hook allows us to create a context that will be available for all the components that are inside the context provider. So we will create a `use_shared_state_provider` that will be placed inside our `App` component. +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. -It should initialize the value using a closure. +The value should be initialized using a closure. `main.rs` ```diff @@ -23,8 +23,7 @@ fn App(cx: Scope) -> Element { } ``` -Now we can use the `use_shared_state` hook to get the state and update it. So let's also add this hook where we need to be able to read or update the Film Modal visibility. - +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 @@ -35,7 +34,6 @@ pub fn Header(cx: Scope) -> Element { ... - Button { button_type: ButtonType::Primary, onclick: move |_| { @@ -49,9 +47,14 @@ pub fn Header(cx: Scope) -> Element { } ``` +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(); @@ -63,8 +66,8 @@ pub fn FilmModal<'a>(cx: Scope<'a, FilmModalProps>) -> Element<'a> { } ``` -In this case we also can see other concept of Dioxus, dynamic rendering. Basically we are rendering the component only if the condition is true. -> **Note:** Dynamic rendering is a technique that allows you to render different content depending on a condition. You can find more information on [Dioxus Dynamic Rendering documentation](https://dioxuslabs.com/docs/0.3/guide/en/interactivity/dynamic_rendering.html) +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 @@ -80,8 +83,8 @@ fn App(cx: Scope) -> Element { main { ... FilmModal { - on_create_or_update: move |_| {}, - on_cancel: move |_| { + on_create_or_update: move |_| {}, + on_cancel: move |_| { + is_modal_visible.write().0 = false; } } @@ -89,4 +92,4 @@ fn App(cx: Scope) -> Element { }) } ``` - +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 index 58286c7..8f6c9ac 100644 --- a/docs/src/frontend/04_04_02_local_state.md +++ b/docs/src/frontend/04_04_02_local_state.md @@ -1,26 +1,255 @@ # Local state -When we are talking about local state is better to talk about **Component** state. Fortunately, Dioxus provides a simple way to manage the state of a component using `use_state` hook. +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: -The particularity of this hook are the following: -- To initialize the state you must pass a closure that returns the initial state +- State initialization is achieved by passing a closure that returns the initial state. ```rust let mut count = use_state(cx, || 0); ``` -- `use_state` gives you the current value with `get()` and a way to update it with `set()` -- Is important to know that on every value update will trigger a component re-render +- The `use_state` hook provides the current value via `get()` and enables its modification using `set()`. +- Each value update triggers a component re-render. -Let's start by updating our `main.rs`, concretely the `App` component. We will add some local state that will be placed at the top of our app, and can be passed to components as props. +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 { -+ let films = use_state::>>(cx, || None); -+ let selected_film = use_state::>(cx, || None); -+ let force_get_films = use_state(cx, || ()); + 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; + } + } + }) } -``` \ No newline at end of file +``` + +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" + } + } + } + + } + )) +} +```