Async/Resource support and example

This commit is contained in:
Greg Johnston 2022-08-01 18:02:02 -04:00
parent b21973783d
commit 46ea459988
14 changed files with 233 additions and 20 deletions

View file

@ -1,5 +1,7 @@
- [ ] Async
- [ ] Resource
- [x] Resource
- [ ] Render bug: when doing e.g., a `match` and having multiple branches with separate `template`s, all exist as separate document fragment and replace the fragment, not one another -- find a way to make sure that `Child<'a>` returned from `insert` is _actually_ attached to the DOM
- [ ] Render bug with list reconciliation
- [ ] Suspense
- [ ] Docs (and clippy warning to insist on docs)
- [ ] Read through + understand...

19
examples/fetch/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "fetch"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.58"
leptos = { path = "../../leptos" }
reqwasm = "0.5.0"
serde = { version = "1", features = ["derive"] }
wee_alloc = "0.4"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
</head>
<body></body>
</html>

66
examples/fetch/src/lib.rs Normal file
View file

@ -0,0 +1,66 @@
use anyhow::Result;
use leptos::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cat {
url: String,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
if count > 0 {
log!("fetching cats");
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={}",
count
))
.send()
.await?
.json::<Vec<Cat>>()
.await?
.into_iter()
.map(|cat| cat.url)
.collect::<Vec<_>>();
log!("got cats {res:?}");
Ok(res)
} else {
Ok(vec![])
}
}
pub fn fetch_example(cx: Scope) -> web_sys::Element {
let (cat_count, set_cat_count) = cx.create_signal::<u32>(0);
let cats = cx.create_resource(cat_count.clone(), |count| fetch_cats(*count));
cx.create_effect(move || log!("cats data = {:?}", cats.data.get()));
view! {
<div>
<label>
"How many cats would you like?"
<input type="number"
on:input=move |ev| {
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
log!("set_cat_count {val}");
set_cat_count(|n| *n = val);
}
/>
</label>
{match &**(cats.data.get().guard()) {
None => view! { <p>"Loading..."</p> },
Some(Err(e)) => view! { <pre>"Error: " {e.to_string()}</pre> },
Some(Ok(cats)) => view! {
<div>{
cats.iter()
.map(|src| {
view! {
<img src={src}/>
}
})
.collect::<Vec<_>>()
}</div>
},
}}
</div>
}
}

View file

@ -0,0 +1,9 @@
use fetch::fetch_example;
use leptos::*;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub fn main() {
mount_to_body(fetch_example)
}

View file

@ -294,23 +294,6 @@ pub fn pick_up_text_node(parent: &web_sys::HtmlElement, node_idx: usize) -> Opti
None
}
#[cfg(target_arch = "wasm32")]
pub fn spawn_local<F>(fut: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(fut)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn spawn_local<F>(_fut: F)
where
F: Future<Output = ()> + 'static,
{
// noop for now; useful for ignoring any async tasks on the server side
// could be replaced with a Tokio dependency
}
pub fn remove_event_listeners(el: &web_sys::Element) {
let clone = el.clone_node().unwrap_throw();
replace_with(el, clone.unchecked_ref());

View file

@ -64,6 +64,7 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
} else if b_end == b_start {
// Remove.
for node in &a[a_start..a_end] {
crate::debug_warn!("unwrap at 68");
if map.is_none() || !map.as_ref().unwrap().contains_key(&NodeWrapper(&node)) {
parent.remove_child(node);
}
@ -97,6 +98,7 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
.collect();
map = Some(tmp);
}
crate::debug_warn!("unwrap at 102");
let map = map.as_ref().unwrap();
if let Some(&index) = map.get(&NodeWrapper(&a[a_start])) {

View file

@ -126,6 +126,13 @@ pub fn insert_expression<'a>(
mut current: Child<'a>,
before: Option<&web_sys::Node>,
) -> Child<'a> {
crate::warn!(
"insert {:?} on {} to replace {:?}",
new_value,
parent.node_name(),
current
);
if new_value == &current {
current
} else {
@ -170,6 +177,11 @@ pub fn insert_expression<'a>(
}
}
Child::Node(old_node) => {
crate::warn!(
"replacing old node with new node\n\nparents are {} and {}",
old_node.parent_node().unwrap().node_name(),
node.parent_node().unwrap().node_name()
);
replace_with(old_node.unchecked_ref(), node);
Child::Node(node.clone())
}

View file

@ -4,4 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
bumpalo = "3"
bumpalo = "3"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"

View file

@ -5,15 +5,19 @@
#![feature(unboxed_closures)]
mod effect;
mod resource;
mod root_context;
mod scope;
mod scope_arena;
mod signal;
mod spawn;
pub use effect::*;
pub use resource::*;
pub use root_context::*;
pub use scope::*;
pub use signal::*;
pub use spawn::*;
#[cfg(test)]
mod tests {

View file

@ -0,0 +1,67 @@
use std::{future::Future, rc::Rc};
use crate::{spawn_local, ReadSignal, Scope, WriteSignal};
pub struct Resource<S, T, Fu>
where
S: 'static,
T: 'static,
Fu: Future<Output = T>,
{
pub data: ReadSignal<Option<T>>,
set_data: WriteSignal<Option<T>>,
source: ReadSignal<S>,
fetcher: Rc<dyn Fn(&S) -> Fu>,
}
impl<S, T, Fu> Resource<S, T, Fu>
where
S: 'static,
T: 'static,
Fu: Future<Output = T> + 'static,
{
pub fn new(cx: Scope, source: ReadSignal<S>, fetcher: impl Fn(&S) -> Fu + 'static) -> Self {
// create signals to handle response
let (data, set_data) = cx.create_signal_owned(None);
let fetcher = Rc::new(fetcher);
cx.create_effect({
let source = source.clone();
let set_data = set_data.clone();
let fetcher = Rc::clone(&fetcher);
move || {
let fut = (fetcher)(&source.get());
let set_data = set_data.clone();
spawn_local(async move {
let res = fut.await;
set_data.update(move |data| *data = Some(res));
});
}
});
// return the Resource synchronously
Self {
data,
set_data,
source,
fetcher,
}
}
pub fn refetch(&self) {
let source = self.source.clone();
let set_data = self.set_data.clone();
let fetcher = Rc::clone(&self.fetcher);
let fut = (fetcher)(&source.get());
spawn_local(async move {
let res = fut.await;
set_data.update(move |data| *data = Some(res));
});
}
pub fn mutate(&self, update_fn: impl FnOnce(&mut Option<T>)) {
self.set_data.update(update_fn);
}
}

View file

@ -1,7 +1,8 @@
use crate::scope_arena::ScopeArena;
use crate::{EffectInner, SignalState};
use crate::{EffectInner, Resource, SignalState};
use super::{root_context::RootContext, Effect, ReadSignal, WriteSignal};
use std::future::Future;
use std::{
any::{Any, TypeId},
cell::RefCell,
@ -84,6 +85,19 @@ impl<'a, 'b> BoundedScope<'a, 'b> {
self.inner.use_context()
}
pub fn create_resource<S, T, Fu>(
self,
source: ReadSignal<S>,
fetcher: impl Fn(&S) -> Fu + 'static,
) -> &'a Resource<S, T, Fu>
where
S: 'static,
T: 'static,
Fu: Future<Output = T> + 'static,
{
self.create_ref(Resource::new(self, source, fetcher))
}
pub fn child_scope<F>(self, f: F) -> ScopeDisposer<'a>
where
F: for<'child_lifetime> FnOnce(BoundedScope<'child_lifetime, 'a>),

View file

@ -1,4 +1,5 @@
use std::{
borrow::Borrow,
cell::{Ref, RefCell},
collections::HashSet,
rc::{Rc, Weak},
@ -76,6 +77,12 @@ pub struct ReadSignalRef<'a, T> {
guard: Ref<'a, T>,
}
impl<'a, T> ReadSignalRef<'a, T> {
pub fn guard(&self) -> &Ref<'a, T> {
&self.guard
}
}
impl<'a, T> Deref for ReadSignalRef<'a, T> {
type Target = T;

View file

@ -0,0 +1,18 @@
use std::future::Future;
#[cfg(target_arch = "wasm32")]
pub fn spawn_local<F>(fut: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(fut)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn spawn_local<F>(_fut: F)
where
F: Future<Output = ()> + 'static,
{
// noop for now; useful for ignoring any async tasks on the server side
// could be replaced with a Tokio dependency
}