Compare commits

...

12 Commits

Author SHA1 Message Date
Greg Johnston
635825a3fd fix warnings 2023-04-21 14:24:03 -04:00
Greg Johnston
c9d42bf92b cargo fmt 2023-04-21 13:52:47 -04:00
Greg Johnston
d411cbf861 back/forward issues 2023-04-21 13:52:08 -04:00
Greg Johnston
fbaead4081 Merge branch 'main' into router-animationend-check 2023-04-21 12:47:08 -04:00
Greg Johnston
d3a577c365 cargo fmt 2023-04-21 12:45:08 -04:00
Greg Johnston
b80f9e3871 fix: issue with ordering of class attribute and class=("fancy-name-200", true) (closes #907) (#914) 2023-04-21 12:42:35 -04:00
Greg Johnston
328d42656d docs: compile error on mutually-exclusive features (#911) 2023-04-21 12:25:21 -04:00
Logan B. Nielsen
d3d2cbed7e feat: add typed window event listeners (#910) 2023-04-21 11:43:11 -04:00
agilarity
d6f7aedec1 CI: use cargo make to run tests for examples (#904) 2023-04-21 10:33:12 -04:00
Greg Johnston
f672493a5f fix: forward/back animations (closes #881) 2023-04-19 20:48:12 -04:00
Greg Johnston
b1247b59da fix: multiple navigations before animation is complete (closes #873) 2023-04-19 09:01:30 -04:00
Greg Johnston
6f5902aa62 fix: check that the target of the animation event is actually this element 2023-04-17 21:11:27 -04:00
19 changed files with 221 additions and 55 deletions

View File

@@ -75,8 +75,20 @@ command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[tasks.test-examples]
description = "Run all unit and web tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "test-unit-and-web"]
[tasks.verify-examples]
description = "Run all quality checks and tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]
[env]
RUSTFLAGS=""
RUSTFLAGS = ""
[env.github-actions]
RUSTFLAGS="-D warnings"
RUSTFLAGS = "-D warnings"

60
examples/Makefile.toml Normal file
View File

@@ -0,0 +1,60 @@
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
# Emulate workspace
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
#"counters", - FIXME: test compile errors
"counters_stable",
"counter_without_macros",
"error_boundary",
"errors_axum",
"fetch",
"hackernews",
"hackernews_axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"ssr_modes",
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
[tasks.verify]
description = "Run all quality checks and tests"
dependencies = ["check-style", "test-unit-and-web"]
[tasks.test-unit-and-web]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.pre-verify-flow]
[tasks.post-verify-flow]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
[tasks.pre-web-test-flow]
[tasks.web-test]
[tasks.post-web-test-flow]

View File

@@ -1,7 +1,7 @@
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.post-test]
[tasks.web-test]
command = "cargo"
args = ["make", "wasm-pack-test"]

View File

@@ -28,8 +28,6 @@ thiserror = "1.0.38"
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
@@ -44,7 +42,7 @@ ssr = [
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name

View File

@@ -41,11 +41,11 @@ a[aria-current] {
}
.slideIn {
animation: 0.125s slideIn forwards;
animation: 0.25s slideIn forwards;
}
.slideOut {
animation: 0.125s slideOut forwards;
animation: 0.25s slideOut forwards;
}
@keyframes slideIn {
@@ -67,11 +67,11 @@ a[aria-current] {
}
.slideInBack {
animation: 0.125s slideInBack forwards;
animation: 0.25s slideInBack forwards;
}
.slideOutBack {
animation: 0.125s slideOutBack forwards;
animation: 0.25s slideOutBack forwards;
}
@keyframes slideInBack {

View File

@@ -43,8 +43,6 @@ bcrypt = { version = "0.14", optional = true }
async-trait = { version = "0.1.64", optional = true }
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
@@ -65,7 +63,7 @@ ssr = [
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name

View File

@@ -36,6 +36,10 @@ ssr = [
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"

View File

@@ -39,6 +39,10 @@ ssr = [
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"

View File

@@ -25,3 +25,6 @@ default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
[package.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -240,3 +240,24 @@ pub fn component_props_builder<P: Props>(
) -> <P as Props>::Builder {
<P as Props>::builder()
}
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
compile_error!(
"You have both `csr` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
default on `leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
compile_error!(
"You have both `hydrate` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
compile_error!(
"You have both `hydrate` and `csr` enabled as features, which may cause \
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
adding `default-features = false` to your `leptos` dependency."
);

View File

@@ -1,6 +1,6 @@
//! A variety of DOM utility functions.
use crate::{is_server, window};
use crate::{events::typed as ev, is_server, window};
use leptos_reactive::{on_cleanup, Scope};
use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
@@ -438,6 +438,18 @@ pub fn window_event_listener(
}
}
/// Creates a window event listener where the event in the callback is already appropriately cast.
pub fn window_event_listener_with_precast<E: ev::EventDescriptor + 'static>(
event: E,
cb: impl Fn(E::EventType) + 'static,
) where
E::EventType: JsCast,
{
window_event_listener(&event.name(), move |e| {
cb(e.unchecked_into::<E::EventType>())
});
}
#[doc(hidden)]
/// This exists only to enable type inference on event listeners when in SSR mode.
pub fn ssr_event_listener<E: crate::ev::EventDescriptor + 'static>(

View File

@@ -658,6 +658,15 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
/// Adds a class to an element.
///
/// **Note**: In the builder syntax, this will be overwritten by the `class`
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
/// are automatically re-ordered so that this over-writing does not happen.
///
/// # Panics
/// This directly uses the browsers `classList` API, which means it will throw
/// a runtime error if you pass more than a single class name. If you want to
/// pass more than one class name at a time, you can use [HtmlElement::classes].
#[track_caller]
pub fn class(
self,

View File

@@ -842,7 +842,10 @@ fn element_to_tokens(
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
if node.key.to_string().trim().starts_with("class:") {
let name = node.key.to_string();
if name.trim().starts_with("class:")
|| fancy_class_name(&name, cx, node).is_some()
{
None
} else {
Some(attribute_to_tokens(cx, node, global_class))
@@ -853,7 +856,10 @@ fn element_to_tokens(
});
let class_attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
if node.key.to_string().trim().starts_with("class:") {
let name = node.key.to_string();
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
Some(fancy)
} else if name.trim().starts_with("class:") {
Some(attribute_to_tokens(cx, node, global_class))
} else {
None

View File

@@ -166,14 +166,25 @@ pub fn AnimatedOutlet(
animation_class.to_string()
}
};
let node_ref = create_node_ref::<html::Div>(cx);
let animationend = move |ev: AnimationEvent| {
ev.stop_propagation();
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) =
animation.next_state(&current, is_back.get_untracked());
*current_state = next;
});
use wasm_bindgen::JsCast;
if let Some(target) = ev.target() {
let node_ref = node_ref.get();
if node_ref.is_none()
|| target
.unchecked_ref::<web_sys::Node>()
.is_same_node(Some(&*node_ref.unwrap()))
{
ev.stop_propagation();
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) =
animation.next_state(&current, is_back.get_untracked());
*current_state = next;
});
}
}
};
view! { cx,

View File

@@ -55,6 +55,7 @@ pub(crate) struct RouterContextInner {
state: ReadSignal<State>,
set_state: WriteSignal<State>,
pub(crate) is_back: RwSignal<bool>,
pub(crate) path_stack: StoredValue<Vec<String>>,
}
impl std::fmt::Debug for RouterContextInner {
@@ -68,6 +69,7 @@ impl std::fmt::Debug for RouterContextInner {
.field("referrers", &self.referrers)
.field("state", &self.state)
.field("set_state", &self.set_state)
.field("path_stack", &self.path_stack)
.finish()
}
}
@@ -111,7 +113,6 @@ impl RouterContext {
replace: true,
scroll: false,
state: State(None),
back: false,
});
}
}
@@ -154,6 +155,10 @@ impl RouterContext {
let inner = Rc::new(RouterContextInner {
base_path: base_path.into_owned(),
path_stack: store_value(
cx,
vec![location.pathname.get_untracked()],
),
location,
base,
history: Box::new(history),
@@ -203,7 +208,6 @@ impl RouterContextInner {
self: Rc<Self>,
to: &str,
options: &NavigateOptions,
back: bool,
) -> Result<(), NavigationError> {
let cx = self.cx;
let this = Rc::clone(&self);
@@ -231,7 +235,6 @@ impl RouterContextInner {
replace: options.replace,
scroll: options.scroll,
state: self.state.get(),
back,
});
}
let len = self.referrers.borrow().len();
@@ -249,13 +252,17 @@ impl RouterContextInner {
let next_state = state.clone();
move |state| *state = next_state
});
self.path_stack.update_value(|stack| {
stack.push(resolved_to.clone())
});
if referrers.borrow().len() == len {
this.navigate_end(LocationChange {
value: resolved_to,
replace: false,
scroll: true,
state,
back,
})
}
}
@@ -280,6 +287,8 @@ impl RouterContextInner {
#[cfg(not(feature = "ssr"))]
pub(crate) fn handle_anchor_click(self: Rc<Self>, ev: web_sys::Event) {
use wasm_bindgen::JsValue;
let ev = ev.unchecked_into::<web_sys::MouseEvent>();
if ev.default_prevented()
|| ev.button() != 0
@@ -343,7 +352,7 @@ impl RouterContextInner {
leptos_dom::helpers::get_property(a.unchecked_ref(), "state")
.ok()
.and_then(|value| {
if value == wasm_bindgen::JsValue::UNDEFINED {
if value == JsValue::UNDEFINED {
None
} else {
Some(value)
@@ -365,7 +374,6 @@ impl RouterContextInner {
scroll: !a.has_attribute("noscroll"),
state: State(state),
},
false,
) {
leptos::error!("{e:#?}");
}

View File

@@ -121,7 +121,10 @@ pub fn AnimatedRoutes(
create_signal(cx, AnimationState::Finally);
let next_route = router.pathname();
let is_complete = Rc::new(Cell::new(true));
let animation_and_route = create_memo(cx, {
let is_complete = Rc::clone(&is_complete);
move |prev: Option<&(AnimationState, String)>| {
let animation_state = animation_state.get();
let next_route = next_route.get();
@@ -140,7 +143,7 @@ pub fn AnimatedRoutes(
let (next_state, can_advance) = animation
.next_state(prev_state, is_back.get_untracked());
if can_advance {
if can_advance || !is_complete.get() {
(next_state, next_route)
} else {
(next_state, prev_route.to_owned())
@@ -158,8 +161,10 @@ pub fn AnimatedRoutes(
let route_states = route_states(cx, &router, current_route, &root_equal);
let root = root_route(cx, base_route, route_states, root_equal);
let node_ref = create_node_ref::<html::Div>(cx);
html::div(cx)
.node_ref(node_ref)
.attr(
"class",
(cx, move || {
@@ -171,6 +176,7 @@ pub fn AnimatedRoutes(
AnimationState::OutroBack => outro_back.unwrap_or_default(),
AnimationState::IntroBack => intro_back.unwrap_or_default(),
};
is_complete.set(animation_class == finally.unwrap_or_default());
if let Some(class) = &class {
format!("{} {animation_class}", class.get())
} else {
@@ -178,13 +184,21 @@ pub fn AnimatedRoutes(
}
}),
)
.on(leptos::ev::animationend, move |_| {
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) =
animation.next_state(&current, is_back.get_untracked());
*current_state = next;
})
.on(leptos::ev::animationend, move |ev| {
use wasm_bindgen::JsCast;
if let Some(target) = ev.target() {
if target
.unchecked_ref::<web_sys::Node>()
.is_same_node(Some(&*node_ref.get().unwrap()))
{
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) = animation
.next_state(&current, is_back.get_untracked());
*current_state = next;
})
}
}
})
.child(move || root.get())
.into_view(cx)

View File

@@ -62,8 +62,6 @@ pub struct LocationChange {
pub scroll: bool,
/// The [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) that will be added during navigation.
pub state: State,
/// Whether the navigation is a “back” navigation.
pub back: bool,
}
impl Default for LocationChange {
@@ -73,7 +71,6 @@ impl Default for LocationChange {
replace: true,
scroll: true,
state: Default::default(),
back: false,
}
}
}

View File

@@ -35,7 +35,7 @@ pub trait History {
pub struct BrowserIntegration {}
impl BrowserIntegration {
fn current(back: bool) -> LocationChange {
fn current() -> LocationChange {
let loc = leptos_dom::helpers::location();
LocationChange {
value: loc.pathname().unwrap_or_default()
@@ -43,8 +43,7 @@ impl BrowserIntegration {
+ &loc.hash().unwrap_or_default(),
replace: true,
scroll: true,
state: State(None), // TODO
back,
state: State(None),
}
}
}
@@ -53,14 +52,28 @@ impl History for BrowserIntegration {
fn location(&self, cx: Scope) -> ReadSignal<LocationChange> {
use crate::{NavigateOptions, RouterContext};
let (location, set_location) = create_signal(cx, Self::current(false));
let (location, set_location) = create_signal(cx, Self::current());
leptos::window_event_listener("popstate", move |_| {
let router = use_context::<RouterContext>(cx);
if let Some(router) = router {
let path_stack = router.inner.path_stack;
let is_back = router.inner.is_back;
let change = Self::current(true);
is_back.set(true);
let change = Self::current();
let is_navigating_back = path_stack.with_value(|stack| {
stack.len() == 1
|| stack.get(stack.len() - 2) == Some(&change.value)
});
if is_navigating_back {
path_stack.update_value(|stack| {
stack.pop();
});
}
is_back.set(is_navigating_back);
request_animation_frame(move || {
is_back.set(false);
});
@@ -72,11 +85,10 @@ impl History for BrowserIntegration {
scroll: change.scroll,
state: change.state,
},
true,
) {
leptos::error!("{e:#?}");
}
set_location.set(Self::current(true));
set_location.set(Self::current());
} else {
leptos::warn!("RouterContext not found");
}
@@ -97,12 +109,10 @@ impl History for BrowserIntegration {
)
.unwrap_throw();
} else {
// push the "forward direction" marker
let state = &loc.state.to_js_value();
history
.push_state_with_url(
&loc.state.to_js_value(),
"",
Some(&loc.value),
)
.push_state_with_url(state, "", Some(&loc.value))
.unwrap_throw();
}
// scroll to el
@@ -172,7 +182,6 @@ impl History for ServerIntegration {
replace: false,
scroll: true,
state: State(None),
back: false,
},
)
.0

View File

@@ -81,7 +81,7 @@ pub fn use_navigate(
) -> impl Fn(&str, NavigateOptions) -> Result<(), NavigationError> {
let router = use_router(cx);
move |to, options| {
Rc::clone(&router.inner).navigate_from_route(to, &options, false)
Rc::clone(&router.inner).navigate_from_route(to, &options)
}
}