Search code examples
rustyew

Rust Yew.rs dropdown component


I have some code for dropdown link.

It works fine, but I'd like this code to work as usual. When we click somewhere outside or click the link inside, dropdown should be closed. How to implement this thing? If I don't want use Bootstrap js but only css.

use yew::prelude::*;

#[function_component(App)]
pub fn app() -> Html {
    let is_open = use_state(|| false);

    let onclick = {
        let is_open = is_open.clone();
        Callback::from(move |_| is_open.set(!*is_open))
    };

    html! {
        <main>
            <div class="dropdown">
                <a
                    class={classes!("btn", "btn-secondary", "dropdown-toggle")}
                    href="#" role="button"
                    data-bs-toggle="dropdown"
                    aria-expanded="false"
                    {onclick}
                >
                 {"Dropdown link"}
                </a>

                <ul class={classes!("dropdown-menu", (*is_open).clone().then(|| Some("show")))}>
                 <li><a class="dropdown-item" href="#">{"Action"}</a></li>
                 <li><a class="dropdown-item" href="#">{"Another action"}</a></li>
                 <li><a class="dropdown-item" href="#">{"Something else here"}</a></li>
                </ul>
            </div>
        </main>
    }
}


Solution

  • You need to do two things:

    1. Attach the event handler to "the whole page", i.e. body to close your dropdown when clicked
    2. Stop propagation of the click event from your dropdown to this event handler on body. Otherwise, the event will bubble up to body as well, and you will get an open+close, effectively not allowing you to open the dropdown.

    Add the following dependencies for ease:

    gloo = "0.10.0"
    wasm-bindgen = "0.2.87"
    

    And change your component as follows:

    use std::rc::Rc;
    
    use gloo::utils::document;
    use wasm_bindgen::{closure::Closure, JsCast};
    use yew::prelude::*;
    
    #[function_component(App)]
    pub fn app() -> Html {
        let is_open = use_state(|| false);
    
        let onclick = {
            let is_open = is_open.clone();
            Callback::from(move |e: MouseEvent| {
                e.stop_propagation();
                is_open.set(!*is_open)
            })
        };
    
        {
            let is_open = is_open.clone();
            // Make a callback function that closes the dropdown
            let close = Rc::new(Closure::<dyn Fn()>::new({
                move || {
                    is_open.set(false);
                }
            }));
    
            // When the component is mounted, attach the event listener to body
            use_effect_with_deps(
                move |_| {
                    let body = document().body().unwrap();
                    body.add_event_listener_with_callback("click", (*close).as_ref().unchecked_ref())
                        .unwrap();
    
                    // Clean up the event listener when the component is unmounted
                    let close_clone = close.clone();
                    move || {
                        body.remove_event_listener_with_callback(
                            "click",
                            (*close_clone).as_ref().unchecked_ref(),
                        )
                        .unwrap();
                    }
                },
                (),
            );
        }
    
        html! {
            <main>
                <div class="dropdown">
                    <a
                        class={classes!("btn", "btn-secondary", "dropdown-toggle")}
                        href="#" role="button"
                        data-bs-toggle="dropdown"
                        aria-expanded="false"
                        {onclick}
                    >
                     {"Dropdown link"}
                    </a>
    
                    <ul class={classes!("dropdown-menu", (*is_open).clone().then(|| Some("show")))}>
                     <li><a class="dropdown-item" href="#">{"Action"}</a></li>
                     <li><a class="dropdown-item" href="#">{"Another action"}</a></li>
                     <li><a class="dropdown-item" href="#">{"Something else here"}</a></li>
                    </ul>
                </div>
            </main>
        }
    }