wip: updates to router

This commit is contained in:
Jonathan Kelley 2021-11-22 15:22:42 -05:00
parent a5f05d73ac
commit bab21a0aa1
8 changed files with 208 additions and 217 deletions

View file

@ -3,8 +3,11 @@ This example is a simple iOS-style calculator. This particular example can run a
This calculator version uses React-style state management. All state is held as individual use_states.
*/
use std::sync::Arc;
use dioxus::events::*;
use dioxus::prelude::*;
use separator::Separatable;
fn main() {
dioxus::desktop::launch(APP, |cfg| cfg);
@ -15,48 +18,12 @@ const APP: FC<()> = |cx, _| {
let operator = use_state(cx, || None as Option<&'static str>);
let display_value = use_state(cx, || String::from(""));
let clear_display = display_value == "0";
let clear_text = if clear_display { "C" } else { "AC" };
let toggle_percent = move |_| todo!();
let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str());
let input_dot = move || display_value.modify().push_str(".");
let perform_operation = move || {
if let Some(op) = operator.as_ref() {
let rhs = display_value.parse::<f64>().unwrap();
let new_val = match *op {
"+" => *cur_val + rhs,
"-" => *cur_val - rhs,
"*" => *cur_val * rhs,
"/" => *cur_val / rhs,
_ => unreachable!(),
};
cur_val.set(new_val);
display_value.set(new_val.to_string());
operator.set(None);
}
};
let toggle_sign = move |_| {
if display_value.starts_with("-") {
display_value.set(display_value.trim_start_matches("-").to_string())
} else {
display_value.set(format!("-{}", *display_value))
}
};
let toggle_percent = move |_| todo!();
let clear_key = move |_| {
display_value.set("0".to_string());
if !clear_display {
operator.set(None);
cur_val.set(0.0);
}
};
let keydownhandler = move |evt: KeyboardEvent| match evt.key_code {
rsx!(cx, div {
class: "calculator",
onkeydown: move |evt| match evt.key_code {
KeyCode::Add => operator.set(Some("+")),
KeyCode::Subtract => operator.set(Some("-")),
KeyCode::Divide => operator.set(Some("/")),
@ -77,23 +44,41 @@ const APP: FC<()> = |cx, _| {
}
}
_ => {}
};
use separator::Separatable;
let formatted_display = cur_val.separated_string();
rsx!(cx, div {
class: "calculator", onkeydown: {keydownhandler}
div { class: "calculator-display", "{formatted_display}" }
}
div { class: "calculator-display", {[format_args!("{}", cur_val.separated_string())]} }
div { class: "input-keys"
div { class: "function-keys"
CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" }
CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "±"}
CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"}
CalculatorKey {
{[if display_value == "0" { "C" } else { "AC" }]}
name: "key-clear",
onclick: move |_| {
display_value.set("0".to_string());
if display_value != "0" {
operator.set(None);
cur_val.set(0.0);
}
}
}
CalculatorKey {
"±"
name: "key-sign",
onclick: move |_| {
if display_value.starts_with("-") {
display_value.set(display_value.trim_start_matches("-").to_string())
} else {
display_value.set(format!("-{}", *display_value))
}
},
}
CalculatorKey {
"%"
onclick: {toggle_percent}
name: "key-percent",
}
}
div { class: "digit-keys"
CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" }
CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "" }
CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), "" }
{(1..9).map(|k| rsx!{
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" }
@ -104,7 +89,25 @@ const APP: FC<()> = |cx, _| {
CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some("*")) "×" }
CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some("-")) "" }
CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some("+")) "+" }
CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" }
CalculatorKey {
"="
name: "key-equals",
onclick: move |_| {
if let Some(op) = operator.as_ref() {
let rhs = display_value.parse::<f64>().unwrap();
let new_val = match *op {
"+" => *cur_val + rhs,
"-" => *cur_val - rhs,
"*" => *cur_val * rhs,
"/" => *cur_val / rhs,
_ => unreachable!(),
};
cur_val.set(new_val);
display_value.set(new_val.to_string());
operator.set(None);
}
},
}
}
}
})
@ -113,7 +116,7 @@ const APP: FC<()> = |cx, _| {
#[derive(Props)]
struct CalculatorKeyProps<'a> {
name: &'static str,
onclick: &'a dyn Fn(MouseEvent),
onclick: &'a dyn Fn(Arc<MouseEvent>),
children: Element,
}

View file

@ -206,43 +206,43 @@ pub fn routable_derive_impl(input: Routable) -> TokenStream {
// static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None);
// }
#[automatically_derived]
impl ::dioxus::router::Routable for #ident {
#from_path
#to_path
// #[automatically_derived]
// impl ::dioxus::router::Routable for #ident {
// #from_path
// #to_path
fn routes() -> ::std::vec::Vec<&'static str> {
::std::vec![#(#ats),*]
}
// // fn routes() -> ::std::vec::Vec<&'static str> {
// // ::std::vec![#(#ats),*]
// // }
fn not_found_route() -> ::std::option::Option<Self> {
#not_found_route
}
// fn current_route() -> ::std::option::Option<Self> {
// #cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
// fn not_found_route() -> ::std::option::Option<Self> {
// #not_found_route
// }
fn recognize(pathname: &str) -> ::std::option::Option<Self> {
todo!()
// ::std::thread_local! {
// static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
// }
// let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
// {
// let route = ::std::clone::Clone::clone(&route);
// #cache_thread_local_ident.with(move |val| {
// *val.borrow_mut() = route;
// });
// }
// route
}
// // fn current_route() -> ::std::option::Option<Self> {
// // #cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
// // }
// fn cleanup() {
// #cache_thread_local_ident.with(move |val| {
// *val.borrow_mut() = ::std::option::Option::None;
// });
// fn recognize(pathname: &str) -> ::std::option::Option<Self> {
// todo!()
// // ::std::thread_local! {
// // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
// // }
// // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
// // {
// // let route = ::std::clone::Clone::clone(&route);
// // #cache_thread_local_ident.with(move |val| {
// // *val.borrow_mut() = route;
// // });
// // }
// // route
// }
// // fn cleanup() {
// // #cache_thread_local_ident.with(move |val| {
// // *val.borrow_mut() = ::std::option::Option::None;
// // });
// // }
// }
}
}
}

View file

@ -74,7 +74,6 @@ pub struct SelfReferentialItems<'a> {
pub(crate) borrowed_props: Vec<&'a VComponent<'a>>,
pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended>,
pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>,
pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
}
/// A component's unique identifier.
@ -284,24 +283,6 @@ impl Scope {
}
}
/// Push an effect to be ran after the component has been successfully mounted to the dom
/// Returns the effect's position in the stack
pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize {
// this is some tricker to get around not being able to actually call fnonces
let mut slot = Some(effect);
let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()());
// wrap it in a type that will actually drop the contents
let boxed_fut = unsafe { BumpBox::from_raw(fut) };
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
let mut items = self.items.borrow_mut();
items.pending_effects.push(self_ref_fut);
items.pending_effects.len() - 1
}
/// Pushes the future onto the poll queue to be polled
/// The future is forcibly dropped if the component is not ready by the next render
pub fn push_task<'src, F: Future<Output = ()>>(
@ -483,13 +464,6 @@ impl Scope {
}
}
// run the list of effects
pub(crate) fn run_effects(&mut self) {
for mut effect in self.items.get_mut().pending_effects.drain(..) {
effect();
}
}
pub fn root_node(&self) -> &VNode {
let node = *self.wip_frame().nodes.borrow().get(0).unwrap();
unsafe { std::mem::transmute(&*node) }

View file

@ -188,7 +188,6 @@ impl ScopeArena {
borrowed_props: Default::default(),
suspended_nodes: Default::default(),
tasks: Default::default(),
pending_effects: Default::default(),
}),
});
@ -226,14 +225,12 @@ impl ScopeArena {
let SelfReferentialItems {
borrowed_props,
listeners,
pending_effects,
suspended_nodes,
tasks,
} = scope.items.get_mut();
borrowed_props.clear();
listeners.clear();
pending_effects.clear();
suspended_nodes.clear();
tasks.clear();
@ -328,14 +325,12 @@ impl ScopeArena {
// just forget about our suspended nodes while we're at it
items.suspended_nodes.clear();
items.tasks.clear();
items.pending_effects.clear();
// guarantee that we haven't screwed up - there should be no latent references anywhere
debug_assert!(items.listeners.is_empty());
debug_assert!(items.borrowed_props.is_empty());
debug_assert!(items.suspended_nodes.is_empty());
debug_assert!(items.tasks.is_empty());
debug_assert!(items.pending_effects.is_empty());
// Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders.
scope.wip_frame().nodes.borrow_mut().clear();

View file

@ -409,7 +409,7 @@ class Interpreter {
}
function main() {
let root = window.document.getElementById("_dioxusroot");
let root = window.document.getElementById("main");
window.interpreter = new Interpreter(root);
console.log(window.interpreter);

View file

@ -5,41 +5,35 @@ use dioxus_router::*;
fn main() {
console_error_panic_hook::set_once();
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
dioxus_web::launch(App, |c| c);
}
#[derive(Clone, Debug, PartialEq)]
enum Route {
static App: FC<()> = |cx, props| {
#[derive(Clone, Debug, PartialEq)]
enum Route {
Home,
About,
NotFound,
}
static App: FC<()> = |cx, props| {
let route = use_router(cx, Route::parse);
match route {
Route::Home => rsx!(cx, div { "Home" }),
Route::About => rsx!(cx, div { "About" }),
Route::NotFound => rsx!(cx, div { "NotFound" }),
}
};
impl ToString for Route {
fn to_string(&self) -> String {
match self {
Route::Home => "/".to_string(),
Route::About => "/about".to_string(),
Route::NotFound => "/404".to_string(),
}
}
}
impl Route {
fn parse(s: &str) -> Self {
match s {
let route = use_router(cx, |s| match s {
"/" => Route::Home,
"/about" => Route::About,
_ => Route::NotFound,
});
cx.render(rsx! {
div {
{match route {
Route::Home => rsx!(h1 { "Home" }),
Route::About => rsx!(h1 { "About" }),
Route::NotFound => rsx!(h1 { "NotFound" }),
}}
nav {
Link { to: Route::Home, href: |_| "/".to_string() }
Link { to: Route::About, href: |_| "/about".to_string() }
}
}
}
})
};

View file

@ -6,23 +6,24 @@ use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_core_macro::{format_args_f, rsx, Props};
use dioxus_html as dioxus_elements;
use wasm_bindgen::JsValue;
use web_sys::Event;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{window, Event};
use crate::utils::fetch_base_url;
use crate::utils::strip_slash_suffix;
pub trait Routable: 'static + Send + Clone + ToString + PartialEq {}
impl<T> Routable for T where T: 'static + Send + Clone + ToString + PartialEq {}
pub trait Routable: 'static + Send + Clone + PartialEq {}
impl<T> Routable for T where T: 'static + Send + Clone + PartialEq {}
pub struct RouterService<R: Routable> {
historic_routes: RefCell<Vec<R>>,
history_service: web_sys::History,
historic_routes: Vec<R>,
history_service: RefCell<web_sys::History>,
base_ur: RefCell<Option<String>>,
}
impl<R: Routable> RouterService<R> {
fn push_route(&self, r: R) {
self.historic_routes.borrow_mut().push(r);
todo!()
// self.historic_routes.borrow_mut().push(r);
}
fn get_current_route(&self) -> &str {
@ -65,41 +66,73 @@ impl<R: Routable> RouterService<R> {
/// This hould only be used once per app
///
/// You can manually parse the route if you want, but the derived `parse` method on `Routable` will also work just fine
pub fn use_router<R: Routable>(cx: Context, parse: impl FnMut(&str) -> R) -> &R {
pub fn use_router<R: Routable>(cx: Context, mut parse: impl FnMut(&str) -> R + 'static) -> &R {
// for the web, attach to the history api
cx.use_hook(
|f| {
//
use gloo::events::EventListener;
let base_url = fetch_base_url();
let base = window()
.unwrap()
.document()
.unwrap()
.query_selector("base[href]")
.ok()
.flatten()
.and_then(|base| {
let base = JsCast::unchecked_into::<web_sys::HtmlBaseElement>(base).href();
let url = web_sys::Url::new(&base).unwrap();
if url.pathname() != "/" {
Some(strip_slash_suffix(&base).to_string())
} else {
None
}
});
let location = window().unwrap().location();
let pathname = location.pathname().unwrap();
let initial_route = parse(&pathname);
let service: RouterService<R> = RouterService {
historic_routes: RefCell::new(vec![]),
history_service: web_sys::window().unwrap().history().expect("no history"),
base_ur: RefCell::new(base_url),
historic_routes: vec![initial_route],
history_service: RefCell::new(
web_sys::window().unwrap().history().expect("no history"),
),
base_ur: RefCell::new(base),
};
// let base = base_url();
// let url = route.to_path();
// pending_routes: RefCell::new(vec![]),
// service.history_service.push_state(data, title);
cx.provide_state(service);
// cx.provide_state(service);
let regenerate = cx.schedule_update();
// when "back" is called by the user, we want to to re-render the component
// // when "back" is called by the user, we want to to re-render the component
let listener = EventListener::new(&web_sys::window().unwrap(), "popstate", move |_| {
//
regenerate();
});
service
},
|f| {
|state| {
let base = state.base_ur.borrow();
if let Some(base) = base.as_ref() {
//
},
);
let path = format!("{}{}", base, state.get_current_route());
}
let history = state.history_service.borrow();
todo!()
// let router = use_router_service::<R>(cx)?;
// Some(cfg(router.get_current_route()))
// history.state.historic_routes.last().unwrap()
//
},
)
}
pub fn use_router_service<R: Routable>(cx: Context) -> Option<&Rc<RouterService<R>>> {
@ -109,6 +142,23 @@ pub fn use_router_service<R: Routable>(cx: Context) -> Option<&Rc<RouterService<
#[derive(Props)]
pub struct LinkProps<R: Routable> {
to: R,
/// The url that gets pushed to the history stack
///
/// You can either put it your own inline method or just autoderive the route using `derive(Routable)`
///
/// ```rust
///
/// Link { to: Route::Home, href: |_| "home".to_string() }
///
/// // or
///
/// Link { to: Route::Home, href: Route::as_url }
///
/// ```
href: fn(&R) -> String,
#[builder(default)]
children: Element,
}
@ -116,7 +166,7 @@ pub fn Link<R: Routable>(cx: Context, props: &LinkProps<R>) -> Element {
let service = use_router_service::<R>(cx)?;
cx.render(rsx! {
a {
href: format_args!("{}", props.to.to_string()),
href: format_args!("{}", (props.href)(&props.to)),
onclick: move |_| service.push_route(props.to.clone()),
{&props.children},
}

View file

@ -1,31 +1,6 @@
use wasm_bindgen::JsCast;
use web_sys::window;
pub fn fetch_base_url() -> Option<String> {
match window()
.unwrap()
.document()
.unwrap()
.query_selector("base[href]")
{
Ok(Some(base)) => {
let base = JsCast::unchecked_into::<web_sys::HtmlBaseElement>(base).href();
let url = web_sys::Url::new(&base).unwrap();
let base = url.pathname();
let base = if base != "/" {
strip_slash_suffix(&base)
} else {
return None;
};
Some(base.to_string())
}
_ => None,
}
}
pub(crate) fn strip_slash_suffix(path: &str) -> &str {
path.strip_suffix('/').unwrap_or(path)
}