Adding in Url and routing
This commit is contained in:
parent
b1f49afbe2
commit
08f0c85940
|
@ -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)",
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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")
|
||||
}),
|
||||
])
|
||||
}),
|
||||
])
|
||||
}),
|
||||
);
|
||||
|
|
147
src/dom.rs
147
src/dom.rs
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(¤t_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)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue