spair 0.0.9

A framework for single-page application in Rust
Documentation
use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::{JsCast, UnwrapThrowExt};

use crate::component::{Comp, Component};

struct SpairRouter {
    router: Box<dyn std::any::Any>,
    current_url: Rc<RefCell<Option<String>>>,
    _pop_state_closure: Option<wasm_bindgen::closure::Closure<dyn Fn(web_sys::PopStateEvent)>>,
}

impl SpairRouter {
    fn execute_routing<R: Router>(&self) {
        let location = match get_new_location(&self.current_url) {
            Some(location) => location,
            None => return,
        };
        if let Some(router) = self.router.downcast_ref::<R>() {
            router.routing(location);
        }
    }
}

thread_local! {
    static ROUTER: Rc<RefCell<SpairRouter>> = Rc::new(RefCell::new(SpairRouter{
        router: Box::new(()),
        current_url: Rc::new(RefCell::new(None)),
        _pop_state_closure: None,
    }));
}

pub trait Router: std::any::Any {
    fn routing(&self, location: web_sys::Location);
}

pub trait Routes {
    type Router: Router;
    fn url(&self) -> String;
    fn update_address_bar(&self) {
        crate::utils::window()
            .history()
            .expect_throw("Unable to get history")
            .push_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&self.url()))
            .expect_throw("Error on push_state_with_url");
    }

    fn execute_routing(&self) {
        self.update_address_bar();
        crate::component::update_component(self::execute_routing::<Self::Router>);
    }
}

impl Router for () {
    fn routing(&self, _: web_sys::Location) {}
}

impl Routes for () {
    type Router = ();
    fn url(&self) -> String {
        String::new()
    }
}

pub fn set_router<R: Router>(r: R) {
    ROUTER.with(|router| {
        if let Ok(mut router) = router.try_borrow_mut() {
            router.router = Box::new(r);
            router._pop_state_closure = Some(register_pop_state_event::<R>());
        }
    });
}

pub fn execute_routing<R: Router>() {
    ROUTER.with(|router| {
        if let Ok(router) = router.try_borrow() {
            router.execute_routing::<R>();
        }
    });
}

pub fn register_routing_callback<C: Component>(comp: &Comp<C>) {
    modify_router::<C, _>(|router| C::register_routing_callback(router, comp))
}

pub fn remove_routing_callback<C: Component>() {
    modify_router::<C, _>(C::remove_routing_callback)
}

pub fn modify_router<C: Component, F: FnOnce(&mut <<C as Component>::Routes as Routes>::Router)>(
    f: F,
) {
    ROUTER.with(|router| {
        if let Ok(mut router) = router.try_borrow_mut() {
            if let Some(router) = router
                .router
                .downcast_mut::<<<C as Component>::Routes as Routes>::Router>()
            {
                f(router);
            }
        }
    })
}

fn register_pop_state_event<R: Router>(
) -> wasm_bindgen::closure::Closure<dyn Fn(web_sys::PopStateEvent)> {
    let closure = move |_: web_sys::PopStateEvent| {
        ROUTER.with(|router| {
            if let Ok(router) = router.try_borrow() {
                router.execute_routing::<R>();
            }
        })
    };

    let closure = wasm_bindgen::closure::Closure::wrap(
        Box::new(closure) as Box<dyn Fn(web_sys::PopStateEvent)>
    );

    crate::utils::register_event_listener_on_window("popstate", closure.as_ref().unchecked_ref());

    closure
}

fn get_new_location(current_url: &Rc<RefCell<Option<String>>>) -> Option<web_sys::Location> {
    let location = crate::utils::window().location();
    let new_url = location
        .href()
        .expect_throw("Unable to get window.location.href");
    let mut current_url = current_url
        .try_borrow_mut()
        .expect_throw("Multiple mutable borrow on current_url");
    let new_url = Some(new_url);
    if *current_url != new_url {
        *current_url = new_url;
        Some(location)
    } else {
        None
    }
}