Compare commits

...

6 Commits

Author SHA1 Message Date
Ben Wishovich
322041917d fix issue with redirects in server fns creating multiple Location headers (#550) 2023-02-20 08:55:47 -05:00
Ikko Eltociear Ashimine
a2eaf9b3ee fix: typo in hydration docs(#552)
identifer -> identifier
2023-02-20 07:09:33 -05:00
Chrislearn Young
4032bfc210 fix: document docs typo (#553) 2023-02-20 07:08:51 -05:00
Greg Johnston
4ff08f042b change: pass Scope as argument into Resource::read() and Resource::with() (#542) 2023-02-19 19:52:31 -05:00
Greg Johnston
ce4b0ecbe1 fix: more work on hydration IDs with <Suspense/> (#545) 2023-02-18 21:20:40 -05:00
Greg Johnston
6c31d09eb2 revert PR #538 (#544) 2023-02-18 18:39:08 -05:00
29 changed files with 415 additions and 321 deletions

View File

@@ -97,10 +97,10 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|_| get_server_count(),
);
let value = move || counter.read().map(|count| count.unwrap_or(0)).unwrap_or(0);
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter
.read()
.read(cx)
.map(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
@@ -143,7 +143,7 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
let value = move || {
log::debug!("FormCounter looking for value");
counter
.read()
.read(cx)
.map(|n| n.ok())
.flatten()
.map(|n| n)

View File

@@ -60,7 +60,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
let cats_view = move || {
cats.with(|data| {
cats.with(cx, |data| {
data.iter()
.flatten()
.map(|cat| view! { cx, <img src={cat}/> })

View File

@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link =
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -82,7 +82,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
fallback=move || view! { cx, <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.read() {
{move || match stories.read(cx) {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {

View File

@@ -17,13 +17,13 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || story.read().map(|story| match story {
{move || story.read(cx).map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
<div class="item-view">

View File

@@ -19,7 +19,7 @@ pub fn User(cx: Scope) -> impl IntoView {
view! { cx,
<div class="user-view">
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || user.read().map(|user| match user {
{move || user.read(cx).map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
Some(user) => view! { cx,
<div>

View File

@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link =
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -82,7 +82,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
fallback=move || view! { cx, <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.read() {
{move || match stories.read(cx) {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {

View File

@@ -17,13 +17,13 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || story.read().map(|story| match story {
{move || story.read(cx).map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
<div class="item-view">

View File

@@ -19,7 +19,7 @@ pub fn User(cx: Scope) -> impl IntoView {
view! { cx,
<div class="user-view">
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || user.read().map(|user| match user {
{move || user.read(cx).map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
Some(user) => view! { cx,
<div>

View File

@@ -71,9 +71,10 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
});
let location = use_location(cx);
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
let contacts =
create_resource(cx, move || location.search.get(), get_contacts);
let contacts = move || {
contacts.read().map(|contacts| {
contacts.read(cx).map(|contacts| {
// this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
contacts
.into_iter()
@@ -126,12 +127,15 @@ pub fn Contact(cx: Scope) -> impl IntoView {
get_contact,
);
let contact_display = move || match contact.read() {
let contact_display = move || match contact.read(cx) {
// None => loading, but will be caught by Suspense fallback
// I'm only doing this explicitly for the example
None => None,
// Some(None) => has loaded and found no contact
Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }.into_any()),
Some(None) => Some(
view! { cx, <p>"No contact with this ID was found."</p> }
.into_any(),
),
// Some(Some) => has loaded and found a contact
Some(Some(contact)) => Some(
view! { cx,

View File

@@ -39,7 +39,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
let posts =
create_resource(cx, || (), |_| async { list_post_metadata().await });
let posts_view = move || {
posts.with(|posts| posts
posts.with(cx, |posts| posts
.clone()
.map(|posts| {
posts.iter()
@@ -82,7 +82,7 @@ fn Post(cx: Scope) -> impl IntoView {
});
let post_view = move || {
post.with(|post| {
post.with(cx, |post| {
post.clone().map(|post| {
view! { cx,
// render content

View File

@@ -140,7 +140,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
{move || {
let existing_todos = {
move || {
todos.read()
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]

View File

@@ -159,7 +159,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
{move || {
let existing_todos = {
move || {
todos.read()
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]

View File

@@ -251,11 +251,6 @@ async fn handle_server_fns_inner(
res_options_inner.headers.clone(),
);
if let Some(header_ref) = res.headers_mut()
{
header_ref.extend(res_headers.drain());
};
if accept_header == Some("application/json")
|| accept_header
== Some(
@@ -285,6 +280,12 @@ async fn handle_server_fns_inner(
Some(status) => res.status(status),
None => res,
};
// This must be after the default referrer
// redirect so that it overwrites the one above
if let Some(header_ref) = res.headers_mut()
{
header_ref.extend(res_headers.drain());
};
match serialized {
Payload::Binary(data) => res
.header(

View File

@@ -28,7 +28,7 @@ use std::rc::Rc;
/// <div>
/// <Suspense fallback=move || view! { cx, <p>"Loading (Suspense Fallback)..."</p> }>
/// {move || {
/// cats.read().map(|data| match data {
/// cats.read(cx).map(|data| match data {
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
/// Some(cats) => view! { cx,
/// <div>{
@@ -69,7 +69,8 @@ where
let orig_child = Rc::new(children);
let current_id = HydrationCtx::peek();
let before_me = HydrationCtx::peek();
let current_id = HydrationCtx::next_component();
let child = DynChild::new({
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
@@ -142,5 +143,7 @@ where
_ => unreachable!(),
};
HydrationCtx::continue_from(before_me);
leptos_dom::View::Suspense(current_id, core_component)
}

View File

@@ -35,7 +35,7 @@ use std::{cell::RefCell, rc::Rc};
/// set_pending=set_pending.into()
/// >
/// {move || {
/// cats.read().map(|data| match data {
/// cats.read(cx).map(|data| match data {
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
/// Some(cats) => view! { cx,
/// <div>{

View File

@@ -4,55 +4,55 @@ use leptos::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let pending_thing = create_resource(
cx,
|| false,
|_| async {
if cfg!(feature = "ssr") {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_local(async {
std::thread::sleep(std::time::Duration::from_millis(10));
tx.send(());
});
rx.await;
} else {
}
true
},
);
let pending_thing = create_resource(
cx,
|| false,
|_| async {
if cfg!(feature = "ssr") {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_local(async {
std::thread::sleep(std::time::Duration::from_millis(10));
tx.send(());
});
rx.await;
} else {
}
true
},
);
view! { cx,
view! { cx,
<div>
<div>
<div>
"This is some text"
</div>
// <Suspense fallback=move || view! { cx, <p>"Loading..."</p> }>
{move || pending_thing.read().map(|n| view! { cx, <ComponentA/> })}
// </Suspense>
"This is some text"
</div>
}
// <Suspense fallback=move || view! { cx, <p>"Loading..."</p> }>
{move || pending_thing.read().map(|n| view! { cx, <ComponentA/> })}
// </Suspense>
</div>
}
}
#[component]
pub fn ComponentA(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, "Hello?".to_string());
let (counter, set_counter) = create_signal(cx, 0);
let (value, set_value) = create_signal(cx, "Hello?".to_string());
let (counter, set_counter) = create_signal(cx, 0);
// Test to make sure hydration isn't broken by
// something like this
//let _ = [div(cx)].into_view(cx);
// Test to make sure hydration isn't broken by
// something like this
//let _ = [div(cx)].into_view(cx);
div(cx)
.id("the-div")
.child(
input(cx)
.attr("type", "text")
.prop("value", (cx, value))
.on(ev::input, move |e| set_value(event_target_value(&e))),
)
.child(input(cx).attr("type", "text").prop("value", value))
.child(p(cx).child("Value: ").child(value))
.into_view(cx)
div(cx)
.id("the-div")
.child(
input(cx)
.attr("type", "text")
.prop("value", (cx, value))
.on(ev::input, move |e| set_value(event_target_value(&e))),
)
.child(input(cx).attr("type", "text").prop("value", value))
.child(p(cx).child("Value: ").child(value))
.into_view(cx)
}
#[cfg(feature = "hydrate")]
@@ -61,11 +61,11 @@ use wasm_bindgen::prelude::wasm_bindgen;
#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
console_error_panic_hook::set_once();
gloo::console::debug!("starting WASM");
gloo::console::debug!("starting WASM");
leptos::mount_to_body(move |cx| {
view! { cx, <App/> }
});
leptos::mount_to_body(move |cx| {
view! { cx, <App/> }
});
}

View File

@@ -9,7 +9,6 @@ gloo = { version = "0.8", features = ["futures"] }
leptos = { path = "../../../leptos", features = ["tracing"] }
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-subscriber-wasm = "0.1"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"

View File

@@ -1,42 +1,102 @@
#![allow(warnings)]
#[macro_use]
extern crate tracing;
mod utils;
use leptos::*;
use tracing_subscriber::prelude::*;
use tracing::field::debug;
use tracing_subscriber::util::SubscriberInitExt;
fn main() {
tracing_subscriber::fmt()
.with_writer(tracing_subscriber_wasm::MakeConsoleWriter::default())
.without_time()
.with_max_level(tracing::Level::TRACE)
.pretty()
.with_target(false)
.init();
console_error_panic_hook::set_once();
mount_to_body(app);
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
.without_time()
.with_file(true)
.with_line_number(true)
.with_target(false)
.with_writer(utils::MakeConsoleWriter)
.with_ansi(false)
.pretty()
.finish()
.init();
mount_to_body(view_fn);
}
#[instrument]
fn app(cx: Scope) -> impl IntoView {
let (data, set_data) = create_signal(cx, vec![1, 3, 5]);
fn view_fn(cx: Scope) -> impl IntoView {
let view = view! { cx,
<For
each=|| vec![0, 1, 2, 3, 4, 5, 6, 7]
key=|i| *i
view=|cx, i| view! { cx, {i} }
/>
}
.into_view(cx);
let handle_change = move |_| {
set_data.update(|data| {
if [1, 3, 5] == data[..] {
*data = vec![0, 1, 2, 3, 4, 5, 6];
} else {
*data = vec![1, 3, 5];
}
})
};
let (a, set_a) = create_signal(cx, view.clone());
let (b, set_b) = create_signal(cx, view);
view! { cx,
<button on:click=handle_change>"Reverse"</button>
let (is_a, set_is_a) = create_signal(cx, true);
<For
each=data
key=|item| *item
view=|cx, i| view! { cx, <h3>{i}</h3> }
/>
let handle_toggle = move |_| {
trace!("toggling");
if is_a() {
set_b(a());
set_is_a(false);
} else {
set_a(a());
set_is_a(true);
}
};
let a_tag = view! { cx, <svg::a/> };
view! { cx,
<>
<div>
<button on:click=handle_toggle>"Toggle"</button>
</div>
<svg>{a_tag}</svg>
<Example/>
<A child=Signal::from(a) />
<A child=Signal::from(b) />
</>
}
}
#[component]
fn A(cx: Scope, child: Signal<View>) -> impl IntoView {
move || child()
}
#[component]
fn Example(cx: Scope) -> impl IntoView {
trace!("rendering <Example/>");
let (value, set_value) = create_signal(cx, 10);
let memo = create_memo(cx, move |_| value() * 2);
let derived = Signal::derive(cx, move || value() * 3);
create_effect(cx, move |_| {
trace!("logging value of derived..., {}", derived.get());
});
set_timeout(
move || set_value.update(|v| *v += 1),
std::time::Duration::from_millis(50),
);
view! { cx,
<h1>"Example"</h1>
<button on:click=move |_| set_value.update(|value| *value += 1)>
"Click me"
</button>
}
}

View File

@@ -0,0 +1,47 @@
pub struct MakeConsoleWriter;
use std::io::{self, Write};
use tracing_subscriber::fmt::MakeWriter;
impl<'a> MakeWriter<'a> for MakeConsoleWriter {
type Writer = ConsoleWriter;
fn make_writer(&'a self) -> Self::Writer {
unimplemented!("use make_writer_for instead");
}
fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
ConsoleWriter(*meta.level(), Vec::with_capacity(256))
}
}
pub struct ConsoleWriter(tracing::Level, Vec<u8>);
impl io::Write for ConsoleWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.1.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
use gloo::console;
use tracing::Level;
let data = String::from_utf8(self.1.to_owned())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "data not UTF-8"))?;
match self.0 {
Level::TRACE => console::debug!(&data),
Level::DEBUG => console::debug!(&data),
Level::INFO => console::log!(&data),
Level::WARN => console::warn!(&data),
Level::ERROR => console::error!(&data),
}
Ok(())
}
}
impl Drop for ConsoleWriter {
fn drop(&mut self) {
let _ = self.flush();
}
}

View File

@@ -347,72 +347,60 @@ where
let (children, closing) =
(component.children.clone(), component.closing.node.clone());
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
cfg_if::cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
create_effect(cx, move |prev_hash_run| {
let mut children_borrow = children.borrow_mut();
let mut children_borrow = children.borrow_mut();
let opening = if let Some(Some(child)) = children_borrow.get(0)
{
child.get_opening_node()
} else {
closing.clone()
};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let opening = if let Some(Some(child)) = children_borrow.get(0) {
child.get_opening_node()
} else {
closing.clone()
};
let items = items_fn();
let items = items_fn();
let items = items.into_iter().collect::<SmallVec<[_; 128]>>();
let items = items.into_iter().collect::<SmallVec<[_; 128]>>();
let hashed_items =
items.iter().map(&key_fn).collect::<FxIndexSet<_>>();
let hashed_items =
items.iter().map(&key_fn).collect::<FxIndexSet<_>>();
if let Some(HashRun(prev_hash_run)) = prev_hash_run {
let cmds = diff(&prev_hash_run, &hashed_items);
if let Some(HashRun(prev_hash_run)) = prev_hash_run {
let cmds = diff(&prev_hash_run, &hashed_items);
tracing::debug!("cmds:\n{cmds:#?}");
apply_cmds(
cx,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
&opening,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
&closing,
cmds,
&mut children_borrow,
items.into_iter().map(|t| Some(t)).collect(),
&each_fn
);
} else {
*children_borrow = Vec::with_capacity(items.len());
apply_cmds(
cx,
&opening,
&closing,
cmds,
&mut children_borrow,
items.into_iter().map(|t| Some(t)).collect(),
&each_fn,
);
} else {
children_borrow.clear();
children_borrow.reserve(items.len());
for item in items {
let (each_item, _) = cx.run_child_scope(|cx| EachItem::new(cx, each_fn(cx, item).into_view(cx)));
for item in items {
let (each_item, disposer) = cx.run_child_scope(|cx| {
EachItem::new(cx, each_fn(cx, item).into_view(cx))
});
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mount_child(MountKind::Before(&closing), &each_item);
mount_child(MountKind::Before(&closing), &each_item);
children_borrow.push(Some(each_item));
}
children_borrow.push(Some(each_item));
}
}
HashRun(hashed_items)
HashRun(hashed_items)
});
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
} else {
*component.children.borrow_mut() = (items_fn)()
.into_iter()
.map(|child| {
cx.run_child_scope(|cx| {
Some(EachItem::new(
cx,
(each_fn)(cx, child).into_view(cx),
))
})
.0
})
.collect();
.into_iter()
.map(|child| cx.run_child_scope(|cx| Some(EachItem::new(cx, (each_fn)(cx, child).into_view(cx)))).0)
.collect();
}
}
View::CoreComponent(CoreComponent::Each(component))
@@ -423,7 +411,7 @@ where
#[educe(Debug)]
struct HashRun<T>(#[educe(Debug(ignore))] T);
/// Calculates the operations needed to get from `a` to `b`.
/// Calculates the operations need to get from `a` to `b`.
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn diff<K: Eq + Hash>(from: &FxIndexSet<K>, to: &FxIndexSet<K>) -> Diff {
if from.is_empty() && to.is_empty() {
@@ -436,73 +424,72 @@ fn diff<K: Eq + Hash>(from: &FxIndexSet<K>, to: &FxIndexSet<K>) -> Diff {
}
// Get removed items
let mut removed =
from.difference(to).map(|k| from.get_index_of(k).unwrap());
let mut removed = from.difference(to);
let removed_cmds = removed.clone().map(|idx| DiffOpRemove { at: idx });
let removed_cmds = removed
.clone()
.map(|k| from.get_full(k).unwrap().0)
.map(|idx| DiffOpRemove { at: idx });
// Get added items
let mut added = to.difference(from).map(|k| to.get_index_of(k).unwrap());
let mut added = to.difference(from);
let added_cmds = added.clone().map(|idx| DiffOpAdd {
at: idx,
mode: Default::default(),
});
let added_cmds =
added
.clone()
.map(|k| to.get_full(k).unwrap().0)
.map(|idx| DiffOpAdd {
at: idx,
mode: Default::default(),
});
let mut normalized_idx = 0i64;
let mut next_added_idx = added.next();
let mut next_removed_idx = removed.next();
// Get moved items
let mut normalized_idx = 0;
let mut move_cmds = SmallVec::<[_; 8]>::with_capacity(to.len());
let mut added_idx = added.next().map(|k| to.get_full(k).unwrap().0);
let mut removed_idx = removed.next().map(|k| from.get_full(k).unwrap().0);
let move_cmds = to
.iter()
.enumerate()
.filter_map(|(i, k)| {
let is_added = if let Some(idx) = next_added_idx {
if i == idx {
next_added_idx = added.next();
normalized_idx -= 1;
for (idx, k) in to.iter().enumerate() {
if let Some(added_idx) = added_idx.as_mut().filter(|r_i| **r_i == idx) {
if let Some(next_added) =
added.next().map(|k| to.get_full(k).unwrap().0)
{
*added_idx = next_added;
true
} else {
false
}
} else {
false
};
let is_removed = if let Some(idx) = next_removed_idx {
if i == idx {
next_removed_idx = removed.next();
normalized_idx += 1;
true
} else {
false
}
} else {
false
};
normalized_idx += 1;
if !is_added && !is_removed {
Some((
from.get_index_of(k).unwrap(),
i,
// We need to `-1` because otherwise, we'd be accounting for
// the NEXT iteration, not this current one
normalized_idx - 1,
))
} else {
None
normalized_idx = usize::wrapping_sub(normalized_idx, 1);
}
})
.map(|(from, to, normalized_idx)| DiffOpMove {
from,
to,
move_in_dom: to != normalized_idx as usize,
})
.collect();
}
if let Some(removed_idx) =
removed_idx.as_mut().filter(|r_i| **r_i == idx)
{
normalized_idx = normalized_idx.wrapping_add(1);
if let Some(next_removed) =
removed.next().map(|k| from.get_full(k).unwrap().0)
{
*removed_idx = next_removed;
}
}
if let Some((from_idx, _)) = from.get_full(k) {
if from_idx != normalized_idx {
move_cmds.push(DiffOpMove {
from: from_idx,
to: idx,
move_in_dom: true,
});
} else if from_idx != idx {
move_cmds.push(DiffOpMove {
from: from_idx,
to: idx,
move_in_dom: false,
});
}
}
normalized_idx = normalized_idx.wrapping_add(1);
}
let mut diffs = Diff {
removed: removed_cmds.collect(),
@@ -634,16 +621,17 @@ fn apply_cmds<T, EF, N>(
if opening.previous_sibling().is_none()
&& closing.next_sibling().is_none()
{
if let Some(parent) = closing
let parent = closing
.parent_node()
.map(JsCast::unchecked_into::<web_sys::Element>)
{
#[cfg(debug_assertions)]
parent.append_with_node_2(opening, closing).unwrap();
.expect("could not get closing node")
.unchecked_into::<web_sys::Element>();
parent.set_text_content(Some(""));
#[cfg(not(debug_assertions))]
parent.append_with_node_1(closing).unwrap();
}
#[cfg(debug_assertions)]
parent.append_with_node_2(opening, closing).unwrap();
#[cfg(not(debug_assertions))]
parent.append_with_node_1(closing).unwrap();
} else {
range.set_start_before(opening).unwrap();
range.set_end_before(closing).unwrap();
@@ -676,7 +664,7 @@ fn apply_cmds<T, EF, N>(
for DiffOpAdd { at, mode } in cmds.added {
let item = items[at].take().unwrap();
let (each_item, disposer) = cx.run_child_scope(|cx| {
let (each_item, _) = cx.run_child_scope(|cx| {
let child = each_fn(cx, item).into_view(cx);
EachItem::new(cx, child)
});

View File

@@ -48,7 +48,7 @@ cfg_if! {
}
}
/// A stable identifer within the server-rendering or hydration process.
/// A stable identifier within the server-rendering or hydration process.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct HydrationKey {
/// The key of the previous component.

View File

@@ -760,7 +760,7 @@ pub fn window() -> web_sys::Window {
/// Returns the [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document).
///
/// This is cached as a thread-local variable, so calling `window()` multiple times
/// This is cached as a thread-local variable, so calling `document()` multiple times
/// requires only one call out to JavaScript.
pub fn document() -> web_sys::Document {
DOCUMENT.with(|document| document.clone())

View File

@@ -54,11 +54,11 @@ use std::{
/// // when we read the signal, it contains either
/// // 1) None (if the Future isn't ready yet) or
/// // 2) Some(T) (if the future's already resolved)
/// assert_eq!(cats(), Some(vec!["1".to_string()]));
/// assert_eq!(cats.read(cx), Some(vec!["1".to_string()]));
///
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
/// set_how_many_cats(2);
/// assert_eq!(cats(), Some(vec!["2".to_string()]));
/// assert_eq!(cats.read(cx), Some(vec!["2".to_string()]));
/// # }
/// # }).dispose();
/// ```
@@ -121,7 +121,6 @@ where
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
scope: cx,
value,
set_value,
loading,
@@ -245,7 +244,6 @@ where
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
scope: cx,
value,
set_value,
loading,
@@ -371,14 +369,14 @@ where
/// resource.
///
/// If you want to get the value without cloning it, use [Resource::with].
/// (`value.read()` is equivalent to `value.with(T::clone)`.)
pub fn read(&self) -> Option<T>
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
pub fn read(&self, cx: Scope) -> Option<T>
where
T: Clone,
{
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.read()
resource.read(cx)
})
})
.ok()
@@ -392,10 +390,10 @@ where
///
/// If you want to get the value by cloning it, you can use
/// [Resource::read].
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.with(f)
resource.with(cx, f)
})
})
.ok()
@@ -427,13 +425,16 @@ where
/// Returns a [std::future::Future] that will resolve when the resource has loaded,
/// yield its [ResourceId] and a JSON string.
#[cfg(any(feature = "ssr", doc))]
pub async fn to_serialization_resolver(&self) -> (ResourceId, String)
pub async fn to_serialization_resolver(
&self,
cx: Scope,
) -> (ResourceId, String)
where
T: Serializable,
{
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.to_serialization_resolver(self.id)
resource.to_serialization_resolver(cx, self.id)
})
})
.expect(
@@ -479,11 +480,11 @@ where
/// // when we read the signal, it contains either
/// // 1) None (if the Future isn't ready yet) or
/// // 2) Some(T) (if the future's already resolved)
/// assert_eq!(cats(), Some(vec!["1".to_string()]));
/// assert_eq!(cats.read(cx), Some(vec!["1".to_string()]));
///
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
/// set_how_many_cats(2);
/// assert_eq!(cats(), Some(vec!["2".to_string()]));
/// assert_eq!(cats.read(cx), Some(vec!["2".to_string()]));
/// # }
/// # }).dispose();
/// ```
@@ -531,48 +532,12 @@ where
{
}
#[cfg(not(feature = "stable"))]
impl<S, T> FnOnce<()> for Resource<S, T>
where
S: Clone + 'static,
T: Clone + 'static,
{
type Output = Option<T>;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.read()
}
}
#[cfg(not(feature = "stable"))]
impl<S, T> FnMut<()> for Resource<S, T>
where
S: Clone + 'static,
T: Clone + 'static,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.read()
}
}
#[cfg(not(feature = "stable"))]
impl<S, T> Fn<()> for Resource<S, T>
where
S: Clone + 'static,
T: Clone + 'static,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.read()
}
}
#[derive(Clone)]
pub(crate) struct ResourceState<S, T>
where
S: 'static,
T: 'static,
{
scope: Scope,
value: ReadSignal<Option<T>>,
set_value: WriteSignal<Option<T>>,
pub loading: ReadSignal<bool>,
@@ -590,15 +555,15 @@ where
S: Clone + 'static,
T: 'static,
{
pub fn read(&self) -> Option<T>
pub fn read(&self, cx: Scope) -> Option<T>
where
T: Clone,
{
self.with(T::clone)
self.with(cx, T::clone)
}
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
let suspense_cx = use_context::<SuspenseContext>(self.scope);
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
let suspense_cx = use_context::<SuspenseContext>(cx);
let v = self
.value
@@ -611,21 +576,23 @@ where
let increment = move |_: Option<()>| {
if let Some(s) = &suspense_cx {
let mut contexts = suspense_contexts.borrow_mut();
if !contexts.contains(s) {
contexts.insert(*s);
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
{
if !contexts.contains(s) {
contexts.insert(*s);
// on subsequent reads, increment will be triggered in load()
// because the context has been tracked here
// on the first read, resource is already loading without having incremented
if !has_value {
s.increment();
// on subsequent reads, increment will be triggered in load()
// because the context has been tracked here
// on the first read, resource is already loading without having incremented
if !has_value {
s.increment();
}
}
}
}
};
create_isomorphic_effect(self.scope, increment);
create_isomorphic_effect(cx, increment);
v
}
@@ -685,6 +652,7 @@ where
pub fn resource_to_serialization_resolver(
&self,
cx: Scope,
id: ResourceId,
) -> std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>
where
@@ -694,7 +662,7 @@ where
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let value = self.value;
create_isomorphic_effect(self.scope, move |_| {
create_isomorphic_effect(cx, move |_| {
value.with({
let mut tx = tx.clone();
move |value| {
@@ -731,6 +699,7 @@ pub(crate) trait SerializableResource {
fn to_serialization_resolver(
&self,
cx: Scope,
id: ResourceId,
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>>;
}
@@ -746,9 +715,10 @@ where
fn to_serialization_resolver(
&self,
cx: Scope,
id: ResourceId,
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>> {
let fut = self.resource_to_serialization_resolver(id);
let fut = self.resource_to_serialization_resolver(cx, id);
Box::pin(fut)
}
}

View File

@@ -421,11 +421,12 @@ impl Runtime {
pub(crate) fn serialization_resolvers(
&self,
cx: Scope,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
let f = FuturesUnordered::new();
for (id, resource) in self.resources.borrow().iter() {
if let AnyResource::Serializable(resource) = resource {
f.push(resource.to_serialization_resolver(id));
f.push(resource.to_serialization_resolver(cx, id));
}
}
f

View File

@@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
use crate::{
console_warn,
runtime::{with_runtime, RuntimeId},
suspense::StreamChunk,
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
@@ -266,10 +267,13 @@ impl Scope {
) {
_ = with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
let scope = scopes.get(self.id).expect(
"tried to add property to a scope that has been disposed",
);
f(&mut scope.borrow_mut());
if let Some(scope) = scopes.get(self.id) {
f(&mut scope.borrow_mut());
} else {
console_warn(
"tried to add property to a scope that has been disposed",
)
}
})
}
@@ -353,8 +357,10 @@ impl Scope {
pub fn serialization_resolvers(
&self,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
.unwrap_or_default()
with_runtime(self.runtime, |runtime| {
runtime.serialization_resolvers(*self)
})
.unwrap_or_default()
}
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
@@ -409,7 +415,9 @@ impl Scope {
{
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.pending_fragments)
let f = std::mem::take(&mut shared_context.pending_fragments);
println!("pending_fragments = {}", f.len());
f
})
.unwrap_or_default()
}

View File

@@ -100,7 +100,7 @@ where
/// Updates whether the action is currently pending.
pub fn set_pending(&self, pending: bool) {
self.0.with_value(|a| a.pending.set(pending))
self.0.try_with_value(|a| a.pending.set(pending));
}
/// The URL associated with the action (typically as part of a server function.)

View File

@@ -12,7 +12,7 @@ description = "Router for the Leptos web framework."
leptos = { workspace = true }
cfg-if = "1"
common_macros = "0.1"
gloo-net = "0.2"
gloo-net = { version = "0.2", features = ["http"] }
lazy_static = "1"
linear-map = "1"
log = "0.4"

View File

@@ -1,8 +1,9 @@
use crate::{use_navigate, use_resolved_path, ToHref};
use crate::{use_navigate, use_resolved_path, ToHref, Url};
use leptos::*;
use std::{error::Error, rc::Rc};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::RequestRedirect;
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
type OnResponse = Rc<dyn Fn(&web_sys::Response)>;
@@ -90,12 +91,13 @@ where
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.header("Content-Type", &enctype)
.redirect(RequestRedirect::Follow)
.body(params)
.send()
.await;
match res {
Err(e) => {
log::error!("<Form/> error while POSTing: {e:#?}");
error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
@@ -110,15 +112,22 @@ where
if let Some(on_response) = on_response.clone() {
on_response(resp.as_raw());
}
if resp.status() == 303 {
if let Some(redirect_url) =
resp.headers().get("Location")
{
_ = navigate(
&redirect_url,
Default::default(),
);
// Check all the logical 3xx responses that might
// get returned from a server function
if resp.redirected() {
let resp_url = &resp.url();
match Url::try_from(resp_url.as_str()) {
Ok(url) => {
request_animation_frame(move || {
if let Err(e) = navigate(
&url.pathname,
Default::default(),
) {
warn!("{}", e);
}
});
}
Err(e) => warn!("{}", e),
}
}
}
@@ -207,7 +216,7 @@ where
input.set(Some(data));
action.set_pending(true);
}
Err(e) => log::error!("{e}"),
Err(e) => error!("{e}"),
}
});
@@ -225,15 +234,19 @@ where
.as_string()
.expect("couldn't get String from JsString"),
) {
Ok(res) => value.set(Some(Ok(res))),
Err(e) => value.set(Some(Err(
ServerFnError::Deserialization(e.to_string()),
))),
Ok(res) => {
value.try_set(Some(Ok(res)));
}
Err(e) => {
value.try_set(Some(Err(
ServerFnError::Deserialization(e.to_string()),
)));
}
}
}
Err(e) => log::error!("{e:?}"),
Err(e) => error!("{e:?}"),
};
input.set(None);
input.try_set(None);
action.set_pending(false);
});
});
@@ -293,7 +306,7 @@ where
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
let data = action_input_from_form_data(&form_data);
match data {
Err(e) => log::error!("{e}"),
Err(e) => error!("{e}"),
Ok(input) => {
ev.prevent_default();
multi_action.dispatch(input);

View File

@@ -104,7 +104,7 @@
//! <div>
//! // show the contacts
//! <ul>
//! {move || contacts.read().map(|contacts| view! { cx, <li>"todo contact info"</li> } )}
//! {move || contacts.read(cx).map(|contacts| view! { cx, <li>"todo contact info"</li> } )}
//! </ul>
//!
//! // insert the nested child route here