Adding in Url and routing

This commit is contained in:
Pauan 2018-11-01 01:40:31 -10:00
parent b1f49afbe2
commit 08f0c85940
6 changed files with 375 additions and 89 deletions

2
Cargo.lock generated
View File

@ -10,7 +10,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "dominator"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"discard 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-channel-preview 0.3.0-alpha.9 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -10,7 +10,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "dominator"
version = "0.2.3"
version = "0.3.2"
dependencies = [
"discard 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-channel-preview 0.3.0-alpha.9 (registry+https://github.com/rust-lang/crates.io-index)",
@ -279,7 +279,7 @@ dependencies = [
name = "todomvc"
version = "0.1.0"
dependencies = [
"dominator 0.2.3",
"dominator 0.3.2",
"futures-signals 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -13,16 +13,15 @@ use std::rc::Rc;
use std::cell::Cell;
// TODO replace most of these with dominator
use stdweb::web::{window, document};
use stdweb::web::event::{InputEvent, ClickEvent, HashChangeEvent, KeyDownEvent, ChangeEvent, DoubleClickEvent, BlurEvent};
use stdweb::web::window;
use stdweb::web::html_element::InputElement;
use stdweb::web::HtmlElement;
use stdweb::unstable::TryInto;
use stdweb::traits::*;
use futures_signals::signal::{SignalExt, Mutable};
use futures_signals::signal::{Signal, SignalExt, Mutable};
use futures_signals::signal_vec::{SignalVecExt, MutableVec};
use dominator::{Dom, text};
use dominator::{Dom, text, routing, HtmlElement};
use dominator::events::{InputEvent, ClickEvent, KeyDownEvent, ChangeEvent, DoubleClickEvent, BlurEvent};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -32,15 +31,41 @@ enum Filter {
All,
}
impl Default for Filter {
impl Filter {
fn signal() -> impl Signal<Item = Self> {
routing::url().map(|url| {
match url.hash().as_str() {
"#/active" => Filter::Active,
"#/completed" => Filter::Completed,
_ => Filter::All,
}
})
}
#[inline]
fn default() -> Self {
Filter::All
fn button(kind: Self) -> Dom {
let url = match kind {
Filter::Active => "#/active",
Filter::Completed => "#/completed",
Filter::All => "#/",
};
let text = match kind {
Filter::Active => "Active",
Filter::Completed => "Completed",
Filter::All => "All",
};
routing::link(url, |dom| { dom
.class_signal("selected", Self::signal()
.map(clone!(kind => move |filter| filter == kind)))
.text(text)
})
}
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
struct Todo {
id: u32,
title: Mutable<String>,
@ -51,7 +76,7 @@ struct Todo {
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
struct State {
todo_id: Cell<u32>,
@ -59,9 +84,6 @@ struct State {
new_todo_title: Mutable<String>,
todo_list: MutableVec<Rc<Todo>>,
#[serde(skip)]
filter: Mutable<Filter>,
}
impl State {
@ -70,7 +92,6 @@ impl State {
todo_id: Cell::new(0),
new_todo_title: Mutable::new("".to_owned()),
todo_list: MutableVec::new(),
filter: Mutable::new(Filter::All),
}
}
@ -79,20 +100,12 @@ impl State {
self.todo_list.lock_mut().retain(|x| x.id != todo.id);
}
fn update_filter(&self) {
let hash = document().location().unwrap().hash().unwrap();
self.filter.set_neq(match hash.as_str() {
"#/active" => Filter::Active,
"#/completed" => Filter::Completed,
_ => Filter::All,
});
}
fn deserialize() -> Self {
window().local_storage().get("todos-rust-dominator").and_then(|state_json| {
serde_json::from_str(state_json.as_str()).ok()
}).unwrap_or_else(State::new)
window().local_storage().get("todos-rust-dominator")
.and_then(|state_json| {
serde_json::from_str(state_json.as_str()).ok()
})
.unwrap_or_else(State::new)
}
fn serialize(&self) {
@ -126,51 +139,10 @@ fn get_checked(event: &ChangeEvent) -> bool {
js!( return @{&event.target()}.checked; ).try_into().unwrap()
}
#[inline]
fn simple(kind: &str, children: &mut [Dom]) -> Dom {
html!(kind, {
.children(children)
})
}
#[inline]
fn link(href: &str, t: &str) -> Dom {
html!("a", {
.attribute("href", href)
.text(t)
})
}
fn filter_button(state: Rc<State>, kind: Filter) -> Dom {
html!("a", {
.class_signal("selected", state.filter.signal()
.map(clone!(kind => move |filter| filter == kind)))
.attribute("href", match kind {
Filter::Active => "#/active",
Filter::Completed => "#/completed",
Filter::All => "#/",
})
.text(match kind {
Filter::Active => "Active",
Filter::Completed => "Completed",
Filter::All => "All",
})
})
}
fn main() {
let state = Rc::new(State::deserialize());
state.update_filter();
window().add_event_listener(clone!(state => move |_: HashChangeEvent| {
state.update_filter();
}));
let body = dominator::body();
dominator::append_dom(&body,
@ -274,7 +246,7 @@ fn main() {
.class_signal("completed", todo.completed.signal())
.visible_signal(map_ref!(
let filter = state.filter.signal(),
let filter = Filter::signal(),
let completed = todo.completed.signal() =>
match *filter {
Filter::Active => !completed,
@ -402,15 +374,21 @@ fn main() {
html!("ul", {
.class("filters")
.children(&mut [
simple("li", &mut [
filter_button(state.clone(), Filter::All),
]),
simple("li", &mut [
filter_button(state.clone(), Filter::Active),
]),
simple("li", &mut [
filter_button(state.clone(), Filter::Completed),
]),
html!("li", {
.children(&mut [
Filter::button(Filter::All),
])
}),
html!("li", {
.children(&mut [
Filter::button(Filter::Active),
])
}),
html!("li", {
.children(&mut [
Filter::button(Filter::Completed),
])
}),
])
}),
html!("button", {
@ -443,14 +421,24 @@ fn main() {
html!("p", {
.text("Double-click to edit a todo")
}),
simple("p", &mut [
text("Created by "),
link("https://github.com/Pauan", "Pauan"),
]),
simple("p", &mut [
text("Part of "),
link("http://todomvc.com", "TodoMVC"),
]),
html!("p", {
.children(&mut [
text("Created by "),
html!("a", {
.attribute("href", "https://github.com/Pauan")
.text("Pauan")
}),
])
}),
html!("p", {
.children(&mut [
text("Part of "),
html!("a", {
.attribute("href", "http://todomvc.com")
.text("TodoMVC")
}),
])
}),
])
}),
);

View File

@ -81,6 +81,7 @@ pub struct CssStyleRule(Reference);
/// A reference to an SVG Element.
///
/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
// TODO move this into stdweb
#[derive(Clone, Debug, PartialEq, Eq, ReferenceType)]
#[reference(instance_of = "SVGElement")]
#[reference(subclass_of(EventTarget, Node, Element))]
@ -91,6 +92,152 @@ impl INode for SvgElement {}
impl IElement for SvgElement {}
// TODO move this into stdweb
#[derive(Clone, Debug, PartialEq, Eq, ReferenceType)]
#[reference(instance_of = "URL")]
pub struct Url( Reference );
// TODO create_object_url, revoke_object_url, and search_params
impl Url {
#[inline]
pub fn new( url: &str ) -> Self {
js!( return new URL( @{url} ); ).try_into().unwrap()
}
#[inline]
pub fn hash( &self ) -> String {
js!( return @{&self.0}.hash; ).try_into().unwrap()
}
#[inline]
pub fn set_hash( &self, hash: &str ) {
js! { @(no_return)
@{&self.0}.hash = @{hash};
}
}
#[inline]
pub fn host( &self ) -> String {
js!( return @{&self.0}.host; ).try_into().unwrap()
}
#[inline]
pub fn set_host( &self, host: &str ) {
js! { @(no_return)
@{&self.0}.host = @{host};
}
}
#[inline]
pub fn hostname( &self ) -> String {
js!( return @{&self.0}.hostname; ).try_into().unwrap()
}
#[inline]
pub fn set_hostname( &self, hostname: &str ) {
js! { @(no_return)
@{&self.0}.hostname = @{hostname};
}
}
#[inline]
pub fn href( &self ) -> String {
js!( return @{&self.0}.href; ).try_into().unwrap()
}
#[inline]
pub fn set_href( &self, href: &str ) {
js! { @(no_return)
@{&self.0}.href = @{href};
}
}
#[inline]
pub fn origin( &self ) -> String {
js!( return @{&self.0}.origin; ).try_into().unwrap()
}
#[inline]
pub fn password( &self ) -> String {
js!( return @{&self.0}.password; ).try_into().unwrap()
}
#[inline]
pub fn set_password( &self, password: &str ) {
js! { @(no_return)
@{&self.0}.password = @{password};
}
}
#[inline]
pub fn pathname( &self ) -> String {
js!( return @{&self.0}.pathname; ).try_into().unwrap()
}
#[inline]
pub fn set_pathname( &self, pathname: &str ) {
js! { @(no_return)
@{&self.0}.pathname = @{pathname};
}
}
#[inline]
pub fn port( &self ) -> String {
js!( return @{&self.0}.port; ).try_into().unwrap()
}
#[inline]
pub fn set_port( &self, port: &str ) {
js! { @(no_return)
@{&self.0}.port = @{port};
}
}
#[inline]
pub fn protocol( &self ) -> String {
js!( return @{&self.0}.protocol; ).try_into().unwrap()
}
#[inline]
pub fn set_protocol( &self, protocol: &str ) {
js! { @(no_return)
@{&self.0}.protocol = @{protocol};
}
}
#[inline]
pub fn search( &self ) -> String {
js!( return @{&self.0}.search; ).try_into().unwrap()
}
#[inline]
pub fn set_search( &self, search: &str ) {
js! { @(no_return)
@{&self.0}.search = @{search};
}
}
#[inline]
pub fn username( &self ) -> String {
js!( return @{&self.0}.username; ).try_into().unwrap()
}
#[inline]
pub fn set_username( &self, username: &str ) {
js! { @(no_return)
@{&self.0}.username = @{username};
}
}
}
impl ::std::fmt::Display for Url {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
self.href().fmt(f)
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#Valid%20Namespace%20URIs
pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";

View File

@ -19,6 +19,7 @@ extern crate futures_signals;
extern crate pin_utils;
#[macro_use]
mod macros;
mod callbacks;
mod operations;
@ -28,6 +29,7 @@ mod dom;
pub use dom::*;
pub mod traits;
pub mod animation;
pub mod routing;
pub use stdweb::web::HtmlElement;

149
src/routing.rs Normal file
View File

@ -0,0 +1,149 @@
use dom::{Dom, DomBuilder, Url};
use futures_signals::signal::{Mutable, Signal};
use stdweb::web::{window, HtmlElement, IEventTarget};
use stdweb::web::event::ClickEvent;
use stdweb::traits::IEvent;
use stdweb::Value;
/*pub struct State<A> {
value: Mutable<Option<A>>,
callback: Value,
}
impl<A> State<A> {
pub fn new() -> Self {
// TODO replace with stdweb function
let value = Mutable::new(js!( return history.state; ).try_into().unwrap());
let callback = |state: Option<A>| {
value.set(state);
};
Self {
value,
callback: js!(
var callback = @{callback};
addEventListener("popstate", function (e) {
callback(e.state);
}, true);
return callback;
),
}
}
pub fn set(&self, value: A) {
window().history().replace_state(value, "", None).unwrap();
self.value.set(value);
}
}*/
fn current_url() -> String {
window().location().unwrap().href().unwrap()
}
// TODO inline ?
fn change_url(mutable: &Mutable<Url>) {
let new_url = current_url();
let mut lock = mutable.lock_mut();
// TODO test that this doesn't notify if the URLs are the same
// TODO helper method for this
// TODO can this be made more efficient ?
if lock.href() != new_url {
*lock = Url::new(&new_url);
}
}
struct CurrentUrl {
value: Mutable<Url>,
listener: Value,
}
impl CurrentUrl {
#[inline]
fn new() -> Self {
let value = Mutable::new(Url::new(&current_url()));
let callback = {
let value = value.clone();
move || {
change_url(&value);
}
};
Self {
value,
listener: js!(
function listener(e) {
@{callback}();
}
addEventListener("popstate", listener, true);
return listener;
),
}
}
}
impl Drop for CurrentUrl {
fn drop(&mut self) {
js! { @(no_return)
removeEventListener("popstate", @{&self.listener}, true);
}
}
}
// TODO use thread_local instead ?
lazy_static! {
static ref URL: CurrentUrl = CurrentUrl::new();
}
#[inline]
pub fn url() -> impl Signal<Item = Url> {
URL.value.signal_cloned()
}
// TODO if URL hasn't been created yet, don't create it
#[inline]
pub fn go_to_url(new_url: &str) {
// TODO replace with stdweb function
js! { @(no_return)
// TODO is this the best state object to use ?
history.pushState(null, "", @{new_url});
}
change_url(&URL.value);
}
// TODO somehow use &str rather than String
#[inline]
pub fn on_click_go_to_url<A>(new_url: String) -> impl FnOnce(DomBuilder<A>) -> DomBuilder<A> where A: IEventTarget + Clone + 'static {
#[inline]
move |dom| {
dom.event(move |e: ClickEvent| {
e.prevent_default();
go_to_url(&new_url);
})
}
}
#[inline]
pub fn link<F>(url: &str, f: F) -> Dom where F: FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> {
html!("a", {
.attribute("href", url)
// TODO somehow avoid this allocation
.apply(on_click_go_to_url(url.to_string()))
.apply(f)
})
}