From 7afc92e857636e82b37c6c8a12c95f9a1d851862 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 10 Apr 2024 00:01:05 -0500 Subject: [PATCH] Kobold Router --- .gitignore | 1 + Cargo.lock | 134 +++++++++++++------- crates/kobold_router/Cargo.toml | 25 ++++ crates/kobold_router/js/util.js | 19 +++ crates/kobold_router/src/internal.rs | 10 ++ crates/kobold_router/src/lib.rs | 183 +++++++++++++++++++++++++++ examples/router/Cargo.toml | 18 +++ examples/router/index.html | 35 +++++ examples/router/src/main.rs | 121 ++++++++++++++++++ 9 files changed, 497 insertions(+), 49 deletions(-) create mode 100644 crates/kobold_router/Cargo.toml create mode 100644 crates/kobold_router/js/util.js create mode 100644 crates/kobold_router/src/internal.rs create mode 100644 crates/kobold_router/src/lib.rs create mode 100644 examples/router/Cargo.toml create mode 100644 examples/router/index.html create mode 100644 examples/router/src/main.rs diff --git a/.gitignore b/.gitignore index f54e7994..f29d153e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ examples/*/dist examples/*/Cargo.lock crates/*/Cargo.lock .DS_Store +.idea diff --git a/Cargo.lock b/Cargo.lock index 9387dd15..45af5723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "base64" @@ -22,9 +22,9 @@ checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "castaway" @@ -43,9 +43,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "compact_str" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff0805f79ecb1b35163f3957a6934ea8d04fcd36ef98b52e7316f63e72e73d1" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" dependencies = [ "castaway", "cfg-if", @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "gloo-utils" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" dependencies = [ "js-sys", "serde", @@ -135,15 +135,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -240,6 +240,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "kobold_router" +version = "0.1.0" +dependencies = [ + "kobold", + "matchit", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "kobold_stateful_example" version = "0.1.0" @@ -259,18 +271,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.142" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" -version = "0.4.17" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "logos" @@ -295,6 +304,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "matchit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e8fcd7bd6025a951597d6ba2f8e48a121af7e262f2b52a006a09c8d61f9304" + [[package]] name = "once_cell" version = "1.19.0" @@ -312,18 +327,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "rlsf" @@ -337,43 +352,64 @@ dependencies = [ "svgbobdoc", ] +[[package]] +name = "router" +version = "0.1.0" +dependencies = [ + "kobold", + "kobold_router", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.58", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -412,9 +448,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -423,35 +459,35 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", ] [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "wasm-bindgen" @@ -480,9 +516,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -521,9 +557,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/crates/kobold_router/Cargo.toml b/crates/kobold_router/Cargo.toml new file mode 100644 index 00000000..aece9098 --- /dev/null +++ b/crates/kobold_router/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "kobold_router" +version = "0.1.0" +authors = ["Bailey Townsend"] +edition = "2021" +license = "MPL-2.0" +readme = "../../README.md" +keywords = ["web", "wasm", "router", "kobold"] +categories = ["wasm", "web-programming"] +description = "A router for web applications written with Kobold" +repository = "https://github.com/maciejhirsz/kobold" +documentation = "https://docs.rs/kobold" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +kobold = { path = "../kobold" } +matchit = "0.8.0" +serde = { version = "1.0.197", features = ["derive"] } +serde-wasm-bindgen = "0.4" +wasm-bindgen = "0.2.84" + +[dependencies.web-sys] +version = "0.3" +features = ["Window", "Location", "History"] diff --git a/crates/kobold_router/js/util.js b/crates/kobold_router/js/util.js new file mode 100644 index 00000000..e17df618 --- /dev/null +++ b/crates/kobold_router/js/util.js @@ -0,0 +1,19 @@ +export function changeRouteView(view) { + const routerView = document.getElementById("routerView"); + routerView.innerHTML = ""; + routerView.appendChild(view); +} + +export function setupPushStateEvent() { + let _wr = function (type) { + let orig = history[type]; + return function () { + let rv = orig.apply(this, arguments); + let e = new Event(type); + e.arguments = arguments; + window.dispatchEvent(e); + return rv; + }; + }; + history.pushState = _wr('pushState'); +} \ No newline at end of file diff --git a/crates/kobold_router/src/internal.rs b/crates/kobold_router/src/internal.rs new file mode 100644 index 00000000..834ca39d --- /dev/null +++ b/crates/kobold_router/src/internal.rs @@ -0,0 +1,10 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "/js/util.js")] +extern "C" { + #[wasm_bindgen(js_name = "changeRouteView")] + pub(crate) fn change_route_view(view: &JsValue); + + #[wasm_bindgen(js_name = "setupPushStateEvent")] + pub(crate) fn setup_push_state_event(); +} diff --git a/crates/kobold_router/src/lib.rs b/crates/kobold_router/src/lib.rs new file mode 100644 index 00000000..52906358 --- /dev/null +++ b/crates/kobold_router/src/lib.rs @@ -0,0 +1,183 @@ +use kobold::dom::Mountable; +use kobold::internal::In; +use kobold::prelude::*; +use matchit::Match; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use wasm_bindgen::{closure::Closure, JsCast, JsValue, UnwrapThrowExt}; + +mod internal; + +/// Routes type +type Routes = matchit::Router>; +type Params = HashMap; + +/// A web router for Kobold +pub struct Router { + router: Rc>, +} + +/// Get the current path via web_sys +pub fn get_path() -> String { + web_sys::window() + .expect_throw("no window") + .location() + .pathname() + .expect_throw("no pathname") +} + +///Error handling for [`get_param`](Router::get_param) +#[derive(Debug)] +pub enum ParamError { + CouldNotFindParam, + CouldNotParseParam, + ParamsNotSet, +} + +/// Implement of [Router] +impl Router { + pub fn new() -> Self { + let router = Rc::new(RefCell::new(matchit::Router::new())); + Router { router } + } + + /// Add a route to the router + pub fn add_route(&mut self, route: &str, view: F) + where + F: Fn() + 'static, + { + self.router + .borrow_mut() + .insert(route, Box::new(move || view())) + .expect_throw("Failed to insert route"); + } + + /// Starts and hosts your web app with a router + pub fn start(&mut self) { + kobold::start(view! { +
+ }); + + let local_router = Rc::clone(&self.router); + + let window = web_sys::window().expect_throw("no window"); + //This is what decides what is render and triggered by pushState + let conditonal_router_render: Closure = Closure::new( + move || match local_router.borrow().at(get_path().as_str()) { + Ok(Match { + value: render_view_fn, + params, + }) => { + let history = web_sys::window() + .expect_throw("no window") + .history() + .expect_throw("no history"); + + let params = params + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::(); + + match serde_wasm_bindgen::to_value(¶ms) { + Ok(new_state) => { + history + .replace_state(&new_state, "") + .expect_throw("failed to replace state"); + } + Err(_) => {} + } + + //Runs the Fn() to render out the view + render_view_fn(); + } + //TODO add ability to load your own 404 page. Possibly view a macro, or raw html + Err(_) => start_route(view! { +

"404"

+ }), + }, + ); + + //Sets up a listener for pushState events + internal::setup_push_state_event(); + window + .add_event_listener_with_callback( + "pushState", + conditonal_router_render.as_ref().unchecked_ref(), + ) + .unwrap(); + //runs same above closure for back, forward,and refresh buttons + window.set_onpopstate(Some(conditonal_router_render.as_ref().unchecked_ref())); + + conditonal_router_render.forget(); + + navigate(get_path().as_str()) + } +} + +/// Start a route with a view +pub fn start_route(view: impl View) { + use std::mem::MaybeUninit; + use std::pin::pin; + + let product = pin!(MaybeUninit::uninit()); + let product = In::pinned(product, move |p| view.build(p)); + + internal::change_route_view(product.js()) +} + +/// Navigate to a new path/route +pub fn navigate(path: &str) { + web_sys::window() + .expect_throw("no window") + .history() + .expect_throw("no history") + .push_state_with_url(&JsValue::NULL, "", Some(path)) + .expect_throw("failed to push state"); +} + +/// Get the value of a parameter from the current route +pub fn get_param(key: &str) -> Result { + let history_state = web_sys::window() + .expect_throw("no window") + .history() + .expect_throw("no history") + .state() + .expect_throw("no state"); + + match serde_wasm_bindgen::from_value::(history_state) { + Ok(params) => match params.get(key) { + Some(value) => match value.parse::() { + Ok(value) => Ok(value), + Err(_) => Err(ParamError::CouldNotParseParam), + }, + None => Err(ParamError::CouldNotFindParam), + }, + Err(_) => Err(ParamError::ParamsNotSet), + } +} + +#[component(class?: "")] +// Creates a link needed for routing with kobold_router +pub fn link<'a>(route: &'a str, class: &'a str, children: impl View + 'a) -> impl View + 'a { + let route = String::from(route); + // TODO Not sure if clone is the best solution, but also need the route for the href tag for browser decoration + let href = route.clone(); + //TODO work on implmenting a fence for event listeners + let onclick = move |event: MouseEvent<_>| { + navigate(&route); + event.prevent_default(); + }; + + view! { + {children} + } +} + +/// Allows short hand for creating a fn +#[macro_export] +macro_rules! route_view { + ($view:expr) => { + || crate::start_route($view) + }; +} diff --git a/examples/router/Cargo.toml b/examples/router/Cargo.toml new file mode 100644 index 00000000..f0631484 --- /dev/null +++ b/examples/router/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "router" +version = "0.1.0" +edition = "2021" + + +[dependencies] +kobold = { path = "../../crates/kobold" } +kobold_router = { path = "../../crates/kobold_router" } +wasm-bindgen = "0.2.84" +web-sys = { version = "0.3.69", features = [ + "Document", + "HtmlInputElement", + "console", + "Window", + "Location", + "History", +] } diff --git a/examples/router/index.html b/examples/router/index.html new file mode 100644 index 00000000..f38d5777 --- /dev/null +++ b/examples/router/index.html @@ -0,0 +1,35 @@ + + + + + + Kobold Router example + + + + + + + + + + \ No newline at end of file diff --git a/examples/router/src/main.rs b/examples/router/src/main.rs new file mode 100644 index 00000000..0b2777e9 --- /dev/null +++ b/examples/router/src/main.rs @@ -0,0 +1,121 @@ +use kobold::prelude::*; +use kobold::View; +use kobold_router::get_param; +use kobold_router::link; +use kobold_router::route_view; +use kobold_router::start_route; +use kobold_router::Router; +use wasm_bindgen::JsValue; +use web_sys::console::error_1; +use web_sys::HtmlInputElement; + +#[component] +fn inventory() -> impl View + 'static { + let attempt_to_get_id = get_param::("id"); + + let id = match attempt_to_get_id { + Ok(id) => Some(id), + Err(err) => { + error_1(&JsValue::from_str(&format!("{:?}", err))); + None + } + }; + + view! { + + } +} + +#[component] +fn id_listing(id: Option) -> impl View + 'static { + view! { +

"ID: "{ id }

+ } +} + +#[component] +fn router_example<'a>(state: &'a Hook, route_number: &'a str) -> impl View + 'a { + let onchange = event!(|state, e: Event| { + let input = e.current_target(); + match input.value().parse::() { + Ok(id) => state.update_inventory(id), + Err(_) => error_1(&JsValue::from_str("Could not parse input value")), + } + }); + + view! { +

"This route number "{ route_number }"!"

+
+ "Click to go to route one" +
+ "Click to go to route two" +
+ "Enter an inventory id" +
+ +
+ {ref state.inventory_url} + + + } +} + +fn route_one(state: &Hook) -> impl View + '_ { + view! { +
+ +
+ } +} + +fn route_two(state: &Hook) -> impl View + '_ { + view! { +
+ +
+ } +} + +fn get_router() -> Router { + let mut router = Router::new(); + router.add_route( + "/", + route_view!(view! { +

{"Welcome to the router example!"}

+ "View your first route here!" + + }), + ); + + router.add_route("/one", route_view!(stateful(State::default, route_one))); + router.add_route("/two", route_view!(stateful(State::default, route_two))); + router.add_route("/inventory/{id}", route_view!(view!())); + + router +} + +fn main() { + let mut router = get_router(); + router.start(); +} + +struct State { + inventory: Option, + inventory_url: String, +} + +impl State { + pub fn update_inventory(&mut self, id: usize) { + self.inventory = Some(id); + self.inventory_url = format!("/inventory/{}", id); + } +} + +impl Default for State { + fn default() -> Self { + State { + inventory: None, + inventory_url: String::from("No link yet"), + } + } +}