diff --git a/docs/book.toml b/docs/book.toml index f67762b..e6028f9 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -10,7 +10,7 @@ create-missing = true [output.html] default-theme = "light" -preferred-dark-theme = "coal" +preferred-dark-theme = "ayu" # copy-fonts = true # additional-css = ["custom.css", "custom2.css"] # additional-js = ["custom.js"] diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 61dd723..bca7ea8 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -21,12 +21,14 @@ - [Watch Mode](./02_11_watch_mode.md) # Frontend -- [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) +- [Frontend](./frontend/03_frontend.md) + - [Setup](./frontend/03_01_setup.md) + - [Starting the Application](./frontend/03_02_app_startup.md) + - [Components](./frontend/03_03_components.md) + - [Layout Components](./frontend/03_03_01_layout.md) + - [Reusable Components](./frontend/03_03_02_reusable_components.md) + - [State management](./frontend/03_04_state_management.md) + - [Global state](./frontend/03_04_01_global_state.md) + - [Local state](./frontend/03_04_02_local_state.md) + - [App effects](./frontend/03_04_03_effects.md) + - [Event handlers](./frontend/03_05_event_handlers.md) diff --git a/docs/src/frontend/04_01_setup.md b/docs/src/frontend/03_01_setup.md similarity index 100% rename from docs/src/frontend/04_01_setup.md rename to docs/src/frontend/03_01_setup.md diff --git a/docs/src/frontend/04_02_app_startup.md b/docs/src/frontend/03_02_app_startup.md similarity index 80% rename from docs/src/frontend/04_02_app_startup.md rename to docs/src/frontend/03_02_app_startup.md index 764f8c6..072b45b 100644 --- a/docs/src/frontend/04_02_app_startup.md +++ b/docs/src/frontend/03_02_app_startup.md @@ -87,3 +87,34 @@ dioxus serve ``` Now, your development environment is up and running. Changes you make to your source code will automatically be reflected in the served application, thanks to the watching capabilities of both the Tailwind compiler and the Dioxus server. You're now ready to start building your Dioxus application! + +## Logging + +For applications that run in the browser, having a logging mechanism can be very useful for debugging and understanding the application's behavior. + +The first step towards this involves installing the `wasm-logger` crate. You can do this by running the following command: + +```bash +cargo add wasm-logger +``` + +Once `wasm-logger` is installed, you need to initialize it in your `main.rs` file. Here's how you can do it: + +`main.rs` +```diff +... +fn main() { ++ wasm_logger::init(wasm_logger::Config::default().module_prefix("front")); + // launch the web app + dioxus_web::launch(App); +} +... +``` + +With the logger initialized, you can now log messages to your browser's console. The following is an example of how you can log an informational message: + +```rust +log::info!("Message on my console"); +``` + +By using this logging mechanism, you can make your debugging process more straightforward and efficient. \ No newline at end of file diff --git a/docs/src/frontend/04_03_01_layout.md b/docs/src/frontend/03_03_01_layout.md similarity index 100% rename from docs/src/frontend/04_03_01_layout.md rename to docs/src/frontend/03_03_01_layout.md diff --git a/docs/src/frontend/04_03_02_reusable_components.md b/docs/src/frontend/03_03_02_reusable_components.md similarity index 100% rename from docs/src/frontend/04_03_02_reusable_components.md rename to docs/src/frontend/03_03_02_reusable_components.md diff --git a/docs/src/frontend/04_03_components.md b/docs/src/frontend/03_03_components.md similarity index 100% rename from docs/src/frontend/04_03_components.md rename to docs/src/frontend/03_03_components.md diff --git a/docs/src/frontend/04_04_01_global_state.md b/docs/src/frontend/03_04_01_global_state.md similarity index 100% rename from docs/src/frontend/04_04_01_global_state.md rename to docs/src/frontend/03_04_01_global_state.md diff --git a/docs/src/frontend/04_04_02_local_state.md b/docs/src/frontend/03_04_02_local_state.md similarity index 97% rename from docs/src/frontend/04_04_02_local_state.md rename to docs/src/frontend/03_04_02_local_state.md index 8f6c9ac..a631768 100644 --- a/docs/src/frontend/04_04_02_local_state.md +++ b/docs/src/frontend/03_04_02_local_state.md @@ -19,6 +19,13 @@ In the `main.rs` file, the `App` component needs to be updated to introduce some `main.rs` ```diff +... +-use components::{FilmModal, Footer, Header}; ++use components::{FilmCard, FilmModal, Footer, Header}; +use dioxus::prelude::*; +use models::FilmModalVisibility; ++use shared::models::Film; + ... fn App(cx: Scope) -> Element { @@ -97,7 +104,8 @@ use crate::models::{ButtonType, FilmModalVisibility}; #[derive(Props)] pub struct FilmModalProps<'a> { - on_create_or_update: EventHandler<'a, MouseEvent>, +- on_create_or_update: EventHandler<'a, MouseEvent>, ++ on_create_or_update: EventHandler<'a, Film>, on_cancel: EventHandler<'a, MouseEvent>, + #[props(!optional)] + film: Option, diff --git a/docs/src/frontend/03_04_03_effects.md b/docs/src/frontend/03_04_03_effects.md new file mode 100644 index 0000000..fe6e44b --- /dev/null +++ b/docs/src/frontend/03_04_03_effects.md @@ -0,0 +1,128 @@ +# App Effects + +Alright folks, we've got our state management all set up. Now, the magic happens! We need to synchronize the values of that state when different parts of our app interact with our users. + +Imagine our first call to the API to fetch our freshly minted films, or the moment when we open the Film Modal in edit mode. We need to pre-populate the form with the values of the film we're about to edit. + +No sweat, we've got the `use_effect` hook to handle this. This useful hook allows us to execute a function when a value changes, or when the component is mounted or unmounted. Pretty cool, huh? + +Now, let's break down the key parts of the `use_effect` hook: +- It should be nestled inside a closure function. +- If we're planning to use a `use_state` hook inside it, we need to `clone()` it or pass the ownership using `to_owned()` to the closure function. +- The parameters inside the `use_effect()` function include the Scope of our app (`cx`), the `dependencies` that will trigger the effect again, and a `future` that will spring into action when the effect is triggered. + +Here's a quick look at how it works: + +```rust +{ + let some_state = some_state.clone(); + use_effect(cx, change_dependency, |_| async move { + // Do something with some_state or something else + }) +} +``` + +Sure, here's a revised version with a more formal tone: + +## Film Modal + +We will begin by adapting our `FilmModal` component. This will be modified to pre-populate the form with the values of the film that is currently being edited. To accomplish this, we will use the `use_effect` hook. + +```diff +... + +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, + }); + ++ { ++ let draft_film = draft_film.clone(); ++ use_effect(cx, &cx.props.film, |film| async move { ++ match film { ++ Some(film) => draft_film.set(film), ++ None => 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, ++ }), ++ } ++ }); ++ } + + ... +} +``` + +In essence, we are initiating an effect when the `film` property changes. If the `film` property is `Some(film)`, we set the `draft_film` state to the value of the `film` property. If the `film` property is `None`, we set the `draft_film` state to a new `Film` initial object. + +## App Component + +Next, we will adapt our `App` component to fetch the films from the API when the app is mounted or when we need to force the API to update the list of films. We'll accomplish this by modifying `force_get_films`. As this state has no type or initial value, it is solely used to trigger the effect. + +We will also add HTTP request configurations to enable these functions. We will use the `reqwest` crate for this purpose, which can be added to our `Cargo.toml` file or installed with the following command: + +```bash +cargo add reqwest +``` + +To streamline future requests, we will create a `films_endpoint()` function to return the URL of our API endpoint. + +Here are the necessary modifications for the `App` component: + +```diff +... + ++const API_ENDPOINT: &str = "api/v1"; + ++fn films_endpoint() -> String { ++ let window = web_sys::window().expect("no global `window` exists"); ++ let location = window.location(); ++ let host = location.host().expect("should have a host"); ++ let protocol = location.protocol().expect("should have a protocol"); ++ let endpoint = format!("{}//{}/{}", protocol, host, API_ENDPOINT); ++ format!("{}/films", endpoint) ++} + ++async fn get_films() -> Vec { ++ log::info!("Fetching films from {}", films_endpoint()); ++ reqwest::get(&films_endpoint()) ++ .await ++ .unwrap() ++ .json::>() ++ .await ++ .unwrap() ++} + +fn App(cx: Scope) -> Element { + ... + let force_get_films = use_state(cx, || ()); + ++ { ++ let films = films.clone(); + + ++ use_effect(cx, force_get_films, |_| async move { ++ let existing_films = get_films().await; ++ if existing_films.is_empty() { ++ films.set(None); ++ } else { ++ films.set(Some(existing_films)); ++ } ++ }); ++ } +} +``` + +What we have done here is trigger an effect whenever there is a need to fetch films from our API. We then evaluate whether there are any films available. If there are, we set the `films` state to these existing films. If not, we set the `films` state to `None`. This allows us to enhance our `App` component with additional functionality. \ No newline at end of file diff --git a/docs/src/frontend/04_04_state_management.md b/docs/src/frontend/03_04_state_management.md similarity index 100% rename from docs/src/frontend/04_04_state_management.md rename to docs/src/frontend/03_04_state_management.md diff --git a/docs/src/frontend/03_05_event_handlers.md b/docs/src/frontend/03_05_event_handlers.md new file mode 100644 index 0000000..9f0bcda --- /dev/null +++ b/docs/src/frontend/03_05_event_handlers.md @@ -0,0 +1,199 @@ +# Event Handlers + +Event handlers are crucial elements in an interactive application. These functions are invoked in response to certain user events like mouse clicks, keyboard input, or form submissions. + +In the final section of this guide, we will introduce interactivity to our application by implementing creation, updating, and deletion of film actions. For this, we will be spawning `futures` using `cx.spawn` and `async move` closures. It is crucial to remember that `use_state` values should be cloned before being used in `async move` closures. + +## delete_film Function + +This function will be triggered when a user clicks the delete button of a film card. It will send a `DELETE` request to our API and subsequently call `force_get_films` to refresh the list of films. In the event of a successful operation, a message will be logged to the console. If an error occurs, the error will be logged instead. + +```rust +let delete_film = move |filmId| { + let force_get_films = force_get_films.clone(); + cx.spawn({ + async move { + let response = reqwest::Client::new() + .delete(&format!("{}/{}", &films_endpoint(), filmId)) + .send() + .await; + match response { + Ok(_data) => { + log::info!("Film deleted"); + force_get_films.set(()); + } + Err(err) => { + log::info!("Error deleting film: {:?}", err); + } + } + } + }); +}; +``` + +## create_or_update_film Function + +This function is invoked when the user clicks the create or update button of the film modal. It sends a `POST` or `PUT` request to our API, followed by a call to `force_get_films` to update the list of films. The decision to edit or create a film depends on whether the `selected_film` state is `Some(film)` or `None`. + +In case of success, a console message is logged, the `selected_film` state is reset, and the modal is hidden. If an error occurs, the error is logged. + +```rust +let create_or_update_film = move |film: Film| { + let force_get_films = force_get_films.clone(); + let current_selected_film = selected_film.clone(); + let is_modal_visible = is_modal_visible.clone(); + + cx.spawn({ + async move { + let response = if current_selected_film.get().is_some() { + reqwest::Client::new() + .put(&films_endpoint()) + .json(&film) + .send() + .await + } else { + reqwest::Client::new() + .post(&films_endpoint()) + .json(&film) + .send() + .await + }; + match response { + Ok(_data) => { + log::info!("Film created"); + current_selected_film.set(None); + is_modal_visible.write().0 = false; + force_get_films.set(()); + } + Err(err) => { + log::info!("Error creating film: {:?}", err); + } + } + } + }); +}; +``` + +## Final Adjustments + +All the subsequent modifications will be implemented on our `App` component. + +`main.rs` +```diff +... + +fn App(cx: Scope) -> Element { + ... + { + let films = films.clone(); + use_effect(cx, force_get_films, |_| async move { + let existing_films = get_films().await; + if existing_films.is_empty() { + films.set(None); + } else { + films.set(Some(existing_films)); + + + } + }); + } + ++ let delete_film = move |filmId| { ++ let force_get_films = force_get_films.clone(); ++ cx.spawn({ ++ async move { ++ let response = reqwest::Client::new() ++ .delete(&format!("{}/{}", &films_endpoint(), filmId)) ++ .send() ++ .await; ++ match response { ++ Ok(_data) => { ++ log::info!("Film deleted"); ++ force_get_films.set(()); ++ } ++ Err(err) => { ++ log::info!("Error deleting film: {:?}", err); ++ } ++ } ++ } ++ }); ++ }; + ++ let create_or_update_film = move |film: Film| { ++ let force_get_films = force_get_films.clone(); ++ let current_selected_film = selected_film.clone(); ++ let is_modal_visible = is_modal_visible.clone(); ++ cx.spawn({ ++ async move { ++ let response = if current_selected_film.get().is_some() { ++ reqwest::Client::new() ++ .put(&films_endpoint()) ++ .json(&film) ++ .send() ++ .await ++ } else { ++ reqwest::Client::new() ++ .post(&films_endpoint()) ++ .json(&film) ++ .send() ++ .await ++ }; ++ match response { ++ Ok(_data) => { ++ log::info!("Film created"); ++ current_selected_film.set(None); ++ is_modal_visible.write().0 = false; ++ force_get_films.set(()); ++ } ++ Err(err) => { ++ log::info!("Error creating film: {:?}", err); ++ } ++ } ++ } ++ }); ++ }; + + cx.render(rsx! { + ... + section { + class: "md:container md:mx-auto md:py-8 flex-1", + rsx!( + if let Some(films) = films.get() { + 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 |_| {} ++ on_delete: move |_| { ++ delete_film(film.id); ++ } + } + ) + })} + } + ) + } + } + FilmModal { + film: selected_film.get().clone(), +- on_create_or_update: move |new_film| {}, ++ on_create_or_update: move |new_film| { ++ create_or_update_film(new_film); ++ }, + on_cancel: move |_| { + selected_film.set(None); + is_modal_visible.write().0 = false; + } + } + }) +} +``` + +Upon successful implementation of the above changes, the application should now have the capability to create, update, and delete films. \ No newline at end of file diff --git a/docs/src/frontend/04_frontend.md b/docs/src/frontend/03_frontend.md similarity index 100% rename from docs/src/frontend/04_frontend.md rename to docs/src/frontend/03_frontend.md