mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
todomvc example
This commit is contained in:
parent
1a7da39fb7
commit
a8adf8eea2
28 changed files with 867 additions and 156 deletions
41
TODO.md
41
TODO.md
|
@ -1,20 +1,35 @@
|
|||
- core examples
|
||||
- [x] counter
|
||||
- [x] counters
|
||||
- [x] fetch
|
||||
- [x] todomvc
|
||||
- [ ] parent\_child
|
||||
- [ ] router
|
||||
- [ ] slots
|
||||
- [ ] hackernews
|
||||
- [ ] counter\_isomorphic
|
||||
- [ ] todo\_app\_sqlite
|
||||
- ErrorBoundary
|
||||
- ssr examples
|
||||
- reactivity
|
||||
- Effects need to be stored (and not mem::forget)
|
||||
- Signal wrappers
|
||||
- SignalDispose implementations on all Copy types
|
||||
- untracked access warnings
|
||||
- callbacks
|
||||
- unsync StoredValue
|
||||
- use this to store Effects
|
||||
- SSR
|
||||
- escaping HTML correctly (attributes + text nodes)
|
||||
- router
|
||||
- nested routes
|
||||
- \_meta package (and use in hackernews)
|
||||
- integrations
|
||||
- update tests
|
||||
- hackernews example
|
||||
- SSR
|
||||
- islands
|
||||
- TODOs
|
||||
- Suspense/Transition/Await components
|
||||
- nicer routing components
|
||||
- async routing (waiting for data to load before navigation)
|
||||
- `<A>` component
|
||||
- figure out rebuilding issues: list (needs new signal IDs) vs. regular rebuild
|
||||
- better import/export API
|
||||
- escaping HTML correctly (attributes + text nodes)
|
||||
- nested routes
|
||||
- Signal wrappers
|
||||
- SignalDispose implementations on all Copy types
|
||||
- untracked access warnings
|
||||
- nicer type than -> `impl RenderHtml<Dom>`
|
||||
- \_meta package (and use in hackernews)
|
||||
- building out examples to find missing features
|
||||
- fix order of el and key so they're the same across traits, in build and rebuild, for attr and property
|
||||
- update streaming SSR tests
|
||||
|
|
|
@ -99,7 +99,7 @@ fn Counter(id: usize, value: ArcRwSignal<i32>) -> impl IntoView {
|
|||
// TODO: implement attribute/prop types for guards
|
||||
move || *value()
|
||||
}
|
||||
on:input=move |ev| { value.set(ev.target().value().parse::<i32>().unwrap_or_default()) }
|
||||
on:input:target=move |ev| { value.set(ev.target().value().parse::<i32>().unwrap_or_default()) }
|
||||
/>
|
||||
<span>{value.clone()}</span>
|
||||
<button on:click=move |_| value.update(move |value| *value += 1)>"+1"</button>
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use leptos::{error::Result, *};
|
||||
use leptos::{
|
||||
prelude::*,
|
||||
reactive_graph::{computed::AsyncDerived, signal::signal},
|
||||
view, IntoView,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -15,38 +19,42 @@ pub enum CatError {
|
|||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
// TODO: leptos::Result
|
||||
async fn fetch_cats(count: CatCount) -> Option<Vec<String>> {
|
||||
if count > 0 {
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={count}",
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.await
|
||||
.ok()?
|
||||
// convert it to JSON
|
||||
.json::<Vec<Cat>>()
|
||||
.await?
|
||||
.await
|
||||
.ok()?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.take(count)
|
||||
.map(|cat| cat.url)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
Some(res)
|
||||
} else {
|
||||
Err(CatError::NonZeroCats.into())
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_example() -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<CatCount>(0);
|
||||
let (cat_count, set_cat_count) = signal::<CatCount>(0);
|
||||
|
||||
// we use local_resource here because
|
||||
// 1) our error type isn't serializable/deserializable
|
||||
// 2) we're not doing server-side rendering in this example anyway
|
||||
// (during SSR, create_resource will begin loading on the server and resolve on the client)
|
||||
let cats = create_local_resource(move || cat_count.get(), fetch_cats);
|
||||
// we use new_unsync here because the reqwasm request type isn't Send
|
||||
// if we were doing SSR, then
|
||||
// 1) we'd want to use a Resource, so the data would be serialized to the client
|
||||
// 2) we'd need to make sure there was a thread-local spawner set up
|
||||
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
|
||||
|
||||
let fallback = move |errors: RwSignal<Errors>| {
|
||||
// TODO ErrorBoundary
|
||||
/*let fallback = move |errors: RwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
errors.with(|errors| {
|
||||
errors
|
||||
|
@ -62,18 +70,20 @@ pub fn fetch_example() -> impl IntoView {
|
|||
<ul>{error_list}</ul>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
};*/
|
||||
|
||||
// the renderer can handle Option<_> and Result<_> states
|
||||
// by displaying nothing for None if the resource is still loading
|
||||
// and by using the ErrorBoundary fallback to catch Err(_)
|
||||
// so we'll just use `.and_then()` to map over the happy path
|
||||
let cats_view = move || {
|
||||
cats.and_then(|data| {
|
||||
data.iter()
|
||||
async move {
|
||||
cats.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|s| view! { <p><img src={s}/></p> })
|
||||
.collect_view()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
.suspend()
|
||||
.transition()
|
||||
.track()
|
||||
.with_fallback(|| view! { <div>"Loading..."</div>})
|
||||
};
|
||||
|
||||
view! {
|
||||
|
@ -84,20 +94,12 @@ pub fn fetch_example() -> impl IntoView {
|
|||
type="number"
|
||||
prop:value=move || cat_count.get().to_string()
|
||||
on:input=move |ev| {
|
||||
let val = event_target_value(&ev).parse::<CatCount>().unwrap_or(0);
|
||||
let val = ev.target().value().parse::<CatCount>().unwrap_or(0);
|
||||
set_cat_count.set(val);
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Transition fallback=move || {
|
||||
view! { <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
<ErrorBoundary fallback>
|
||||
<div>
|
||||
{cats_view}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
{cats_view}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
use leptos::*;
|
||||
use leptos::{
|
||||
callback::{Callback, UnsyncCallback},
|
||||
component,
|
||||
prelude::*,
|
||||
reactive_graph::{
|
||||
owner::{provide_context, use_context},
|
||||
signal::{signal, WriteSignal},
|
||||
},
|
||||
view, IntoView,
|
||||
};
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
// This highlights four different ways that child components can communicate
|
||||
|
@ -16,10 +25,10 @@ struct SmallcapsContext(WriteSignal<bool>);
|
|||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// just some signals to toggle three classes on our <p>
|
||||
let (red, set_red) = create_signal(false);
|
||||
let (right, set_right) = create_signal(false);
|
||||
let (italics, set_italics) = create_signal(false);
|
||||
let (smallcaps, set_smallcaps) = create_signal(false);
|
||||
let (red, set_red) = signal(false);
|
||||
let (right, set_right) = signal(false);
|
||||
let (italics, set_italics) = signal(false);
|
||||
let (smallcaps, set_smallcaps) = signal(false);
|
||||
|
||||
// the newtype pattern isn't *necessary* here but is a good practice
|
||||
// it avoids confusion with other possible future `WriteSignal<bool>` contexts
|
||||
|
@ -27,7 +36,6 @@ pub fn App() -> impl IntoView {
|
|||
provide_context(SmallcapsContext(set_smallcaps));
|
||||
|
||||
view! {
|
||||
|
||||
<main>
|
||||
<p
|
||||
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
|
||||
|
@ -45,10 +53,11 @@ pub fn App() -> impl IntoView {
|
|||
// Button B: pass a closure
|
||||
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
|
||||
|
||||
// TODO -- on:click on components
|
||||
// Button C: use a regular event listener
|
||||
// setting an event listener on a component like this applies it
|
||||
// to each of the top-level elements the component returns
|
||||
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
|
||||
//<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
|
||||
|
||||
// Button D gets its setter from context rather than props
|
||||
<ButtonD/>
|
||||
|
@ -74,15 +83,17 @@ pub fn ButtonA(
|
|||
|
||||
/// Button B receives a closure
|
||||
#[component]
|
||||
pub fn ButtonB(
|
||||
pub fn ButtonB<F>(
|
||||
/// Callback that will be invoked when the button is clicked.
|
||||
#[prop(into)]
|
||||
on_click: Callback<MouseEvent>,
|
||||
) -> impl IntoView {
|
||||
mut on_click: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: FnMut(MouseEvent) + 'static,
|
||||
{
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=move|ev|on_click.call(ev)
|
||||
on:click=on_click
|
||||
>
|
||||
"Toggle Right"
|
||||
</button>
|
||||
|
@ -94,7 +105,6 @@ pub fn ButtonB(
|
|||
#[component]
|
||||
pub fn ButtonC() -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button>
|
||||
"Toggle Italics"
|
||||
</button>
|
||||
|
@ -108,7 +118,6 @@ pub fn ButtonD() -> impl IntoView {
|
|||
let setter = use_context::<SmallcapsContext>().unwrap().0;
|
||||
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
use leptos::{html::Input, leptos_dom::helpers::location_hash, *};
|
||||
use leptos::{
|
||||
leptos_dom::{
|
||||
events,
|
||||
helpers::{location_hash, window, window_event_listener},
|
||||
},
|
||||
prelude::*,
|
||||
reactive_graph::{
|
||||
effect::Effect,
|
||||
owner::{provide_context, use_context},
|
||||
signal::{RwSignal, WriteSignal},
|
||||
},
|
||||
tachys::{html::element::Input, reactive_graph::node_ref::NodeRef},
|
||||
*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use web_sys::KeyboardEvent;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
|
@ -107,11 +121,9 @@ impl Todo {
|
|||
) -> Self {
|
||||
// RwSignal combines the getter and setter in one struct, rather than separating
|
||||
// the getter from the setter. This makes it more convenient in some cases, such
|
||||
// as when we're putting the signals into a struct and passing it around. There's
|
||||
// no real difference: you could use `create_signal` here, or use `create_rw_signal`
|
||||
// everywhere.
|
||||
let title = create_rw_signal(title);
|
||||
let completed = create_rw_signal(completed);
|
||||
// as when we're putting the signals into a struct and passing it around.
|
||||
let title = RwSignal::new(title);
|
||||
let completed = RwSignal::new(completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
|
@ -132,7 +144,7 @@ const ENTER_KEY: u32 = 13;
|
|||
#[component]
|
||||
pub fn TodoMVC() -> impl IntoView {
|
||||
// The `todos` are a signal, since we need to reactively update the list
|
||||
let (todos, set_todos) = create_signal(Todos::default());
|
||||
let (todos, set_todos) = signal(Todos::default());
|
||||
|
||||
// We provide a context that each <Todo/> component can use to update the list
|
||||
// Here, I'm just passing the `WriteSignal`; a <Todo/> doesn't need to read the whole list
|
||||
|
@ -142,16 +154,17 @@ pub fn TodoMVC() -> impl IntoView {
|
|||
provide_context(set_todos);
|
||||
|
||||
// Handle the three filter modes: All, Active, and Completed
|
||||
let (mode, set_mode) = create_signal(Mode::All);
|
||||
window_event_listener(ev::hashchange, move |_| {
|
||||
let (mode, set_mode) = signal(Mode::All);
|
||||
|
||||
window_event_listener(events::hashchange, move |_| {
|
||||
let new_mode =
|
||||
location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode.set(new_mode);
|
||||
});
|
||||
|
||||
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
|
||||
let input_ref = create_node_ref::<Input>();
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let add_todo = move |ev: KeyboardEvent| {
|
||||
let input = input_ref.get().unwrap();
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.key_code();
|
||||
|
@ -191,9 +204,12 @@ pub fn TodoMVC() -> impl IntoView {
|
|||
// the effect reads the `todos` signal, and each `Todo`'s title and completed
|
||||
// status, so it will automatically re-run on any change to the list of tasks
|
||||
//
|
||||
// this is the main point of `create_effect`: to synchronize reactive state
|
||||
// this is the main point of effects: to synchronize reactive state
|
||||
// with something outside the reactive system (like localStorage)
|
||||
create_effect(move |_| {
|
||||
|
||||
// TODO: should be a stored value instead of leaked
|
||||
std::mem::forget(Effect::new(move |_| {
|
||||
leptos::tachys::log("saving todos to localStorage...");
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let json = serde_json::to_string(&todos)
|
||||
.expect("couldn't serialize Todos");
|
||||
|
@ -201,14 +217,16 @@ pub fn TodoMVC() -> impl IntoView {
|
|||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// focus the main input on load
|
||||
create_effect(move |_| {
|
||||
// TODO: should be a stored value instead of leaked
|
||||
std::mem::forget(Effect::new(move |_| {
|
||||
leptos::tachys::log("focusing...");
|
||||
if let Some(input) = input_ref.get() {
|
||||
let _ = input.focus();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
view! {
|
||||
<main>
|
||||
|
@ -280,13 +298,14 @@ pub fn TodoMVC() -> impl IntoView {
|
|||
|
||||
#[component]
|
||||
pub fn Todo(todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(false);
|
||||
let (editing, set_editing) = signal(false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
|
||||
|
||||
// this will be filled by node_ref=input below
|
||||
let todo_input = create_node_ref::<Input>();
|
||||
let todo_input = NodeRef::<Input>::new();
|
||||
|
||||
let save = move |value: &str| {
|
||||
leptos::tachys::log("saving...");
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
|
@ -299,18 +318,18 @@ pub fn Todo(todo: Todo) -> impl IntoView {
|
|||
view! {
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={todo.completed}
|
||||
// TODO
|
||||
//class:editing={editing}
|
||||
//class:completed={move || todo.completed.get()}
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
node_ref=todo_input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={todo.completed}
|
||||
on:input={move |ev| {
|
||||
let checked = event_target_checked(&ev);
|
||||
todo.completed.set(checked);
|
||||
prop:checked={move || todo.completed.get()}
|
||||
on:input:target={move |ev| {
|
||||
todo.completed.set(ev.target().checked());
|
||||
}}
|
||||
/>
|
||||
<label on:dblclick=move |_| {
|
||||
|
@ -328,12 +347,12 @@ pub fn Todo(todo: Todo) -> impl IntoView {
|
|||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !editing.get()}
|
||||
prop:value=todo.title
|
||||
on:focusout=move |ev: web_sys::FocusEvent| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev: web_sys::KeyboardEvent| {
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout:target=move |ev| save(&ev.target().value())
|
||||
on:keyup:target={move |ev| {
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
save(&ev.target().value());
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing.set(false);
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@ leptos_macro = { workspace = true }
|
|||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
leptos-spin-macro = { version = "0.2", optional = true }
|
||||
tracing = "0.1"
|
||||
reactive_graph = { workspace = true }
|
||||
leptos-spin-macro = { version = "0.1", optional = true }
|
||||
paste = "1"
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
tachys = { workspace = true, features = ["reactive_graph"] }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.18"
|
||||
typed-builder-macro = "0.18"
|
||||
serde = { version = "1", optional = true }
|
||||
|
|
329
leptos/src/callback.rs
Normal file
329
leptos/src/callback.rs
Normal file
|
@ -0,0 +1,329 @@
|
|||
//! Callbacks define a standard way to store functions and closures. They are useful
|
||||
//! for component properties, because they can be used to define optional callback functions,
|
||||
//! which generic props don’t support.
|
||||
//!
|
||||
//! # Usage
|
||||
//! Callbacks can be created manually from any function or closure, but the easiest way
|
||||
//! to create them is to use `#[prop(into)]]` when defining a component.
|
||||
//! ```
|
||||
//! # use leptos::*;
|
||||
//! #[component]
|
||||
//! fn MyComponent(
|
||||
//! #[prop(into)] render_number: Callback<i32, String>,
|
||||
//! ) -> impl IntoView {
|
||||
//! view! {
|
||||
//! <div>
|
||||
//! {render_number.call(1)}
|
||||
//! // callbacks can be called multiple times
|
||||
//! {render_number.call(42)}
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
//! // you can pass a closure directly as `render_number`
|
||||
//! fn test() -> impl IntoView {
|
||||
//! view! {
|
||||
//! <MyComponent render_number=|x: i32| x.to_string()/>
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! *Notes*:
|
||||
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
|
||||
//! - Callbacks are most useful when you want optional generic props.
|
||||
//! - All callbacks implement the [`Callable`] trait, and can be invoked with `my_callback.call(input)`. On nightly, you can even do `my_callback(input)`
|
||||
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
|
||||
//!
|
||||
//! # Types
|
||||
//! This modules implements 2 callback types:
|
||||
//! - [`Callback`]
|
||||
//! - [`SyncCallback`]
|
||||
//!
|
||||
//! Use `SyncCallback` when you want the function to be `Sync` and `Send`.
|
||||
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
#![cfg_attr(feature = "nightly", feature(auto_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(negative_impls))]
|
||||
|
||||
use reactive_graph::owner::StoredValue;
|
||||
use std::{fmt, rc::Rc, sync::Arc};
|
||||
|
||||
/// A wrapper trait for calling callbacks.
|
||||
pub trait Callable<In: 'static, Out: 'static = ()> {
|
||||
/// calls the callback with the specified argument.
|
||||
fn call(&self, input: In) -> Out;
|
||||
}
|
||||
|
||||
/// Callbacks define a standard way to store functions and closures.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::{Callable, Callback};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: Callback<i32, String>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {render_number.call(42)}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn test() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <MyComponent render_number=move |x: i32| x.to_string()/>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub struct UnsyncCallback<In: 'static, Out: 'static = ()>(
|
||||
Rc<dyn Fn(In) -> Out>,
|
||||
);
|
||||
|
||||
impl<In> fmt::Debug for UnsyncCallback<In> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str("Callback")
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Clone for UnsyncCallback<In, Out> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(Rc::clone(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> UnsyncCallback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
|
||||
where
|
||||
F: Fn(In) -> Out + 'static,
|
||||
{
|
||||
Self(Rc::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
|
||||
fn call(&self, input: In) -> Out {
|
||||
(self.0)(input)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_fn {
|
||||
($ty:ident) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<F, In, T, Out> From<F> for $ty<In, Out>
|
||||
where
|
||||
F: Fn(In) -> T + Send + Sync + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
In: Send + Sync + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self::new(move |x| f(x).into())
|
||||
}
|
||||
}
|
||||
|
||||
paste::paste! {
|
||||
#[cfg(feature = "nightly")]
|
||||
auto trait [<NotRaw $ty>] {}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<A, B> ![<NotRaw $ty>] for $ty<A, B> {}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<F, In, T, Out> From<F> for $ty<In, Out>
|
||||
where
|
||||
F: Fn(In) -> T + Send + Sync + [<NotRaw $ty>] + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
In: Send + Sync + 'static
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self::new(move |x| f(x).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO
|
||||
//impl_from_fn!(UnsyncCallback);
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<In, Out> FnOnce<(In,)> for UnsyncCallback<In, Out> {
|
||||
type Output = Out;
|
||||
|
||||
extern "rust-call" fn call_once(self, args: (In,)) -> Self::Output {
|
||||
Callable::call(&self, args.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<In, Out> FnMut<(In,)> for UnsyncCallback<In, Out> {
|
||||
extern "rust-call" fn call_mut(&mut self, args: (In,)) -> Self::Output {
|
||||
Callable::call(&*self, args.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<In, Out> Fn<(In,)> for UnsyncCallback<In, Out> {
|
||||
extern "rust-call" fn call(&self, args: (In,)) -> Self::Output {
|
||||
Callable::call(self, args.0)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO update these docs to swap the two
|
||||
/// A callback type that is `Send` and `Sync` if its input type is `Send` and `Sync`.
|
||||
/// Otherwise, you can use exactly the way you use [`Callback`].
|
||||
pub struct Callback<In, Out = ()>(
|
||||
StoredValue<Arc<dyn Fn(In) -> Out + Send + Sync>>,
|
||||
)
|
||||
where
|
||||
In: 'static,
|
||||
Out: 'static;
|
||||
|
||||
impl<In, Out> fmt::Debug for Callback<In, Out> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str("SyncCallback")
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
|
||||
fn call(&self, input: In) -> Out {
|
||||
self.0
|
||||
.with_value(|f| f(input))
|
||||
.expect("called a callback that has been disposed")
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Clone for Callback<In, Out> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
pub fn new<F>(fun: F) -> Self
|
||||
where
|
||||
F: Fn(In) -> Out + Send + Sync + 'static,
|
||||
{
|
||||
Self(StoredValue::new(Arc::new(fun)))
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_fn!(Callback);
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<In, Out> FnOnce<(In,)> for Callback<In, Out>
|
||||
where
|
||||
In: Send + Sync + 'static,
|
||||
Out: 'static,
|
||||
{
|
||||
type Output = Out;
|
||||
|
||||
extern "rust-call" fn call_once(self, args: (In,)) -> Self::Output {
|
||||
Callable::call(&self, args.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<In, Out> FnMut<(In,)> for Callback<In, Out>
|
||||
where
|
||||
In: Send + Sync + 'static,
|
||||
Out: 'static,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, args: (In,)) -> Self::Output {
|
||||
Callable::call(&*self, args.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<In, Out> Fn<(In,)> for Callback<In, Out>
|
||||
where
|
||||
In: Send + Sync + 'static,
|
||||
Out: 'static,
|
||||
{
|
||||
extern "rust-call" fn call(&self, args: (In,)) -> Self::Output {
|
||||
Callable::call(self, args.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
callback::{Callback, UnsyncCallback},
|
||||
create_runtime,
|
||||
};
|
||||
|
||||
struct NoClone {}
|
||||
|
||||
#[test]
|
||||
fn clone_callback() {
|
||||
let rt = create_runtime();
|
||||
let callback =
|
||||
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback.clone();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_sync_callback() {
|
||||
let rt = create_runtime();
|
||||
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback.clone();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_from() {
|
||||
let rt = create_runtime();
|
||||
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_from_html() {
|
||||
let rt = create_runtime();
|
||||
use leptos::{
|
||||
html::{AnyElement, HtmlElement},
|
||||
*,
|
||||
};
|
||||
|
||||
let _callback: UnsyncCallback<String, HtmlElement<AnyElement>> =
|
||||
(|x: String| {
|
||||
view! {
|
||||
<h1>{x}</h1>
|
||||
}
|
||||
})
|
||||
.into();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_from() {
|
||||
let rt = create_runtime();
|
||||
let _callback: Callback<(), String> = (|()| "test").into();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_from_html() {
|
||||
use leptos::{
|
||||
html::{AnyElement, HtmlElement},
|
||||
*,
|
||||
};
|
||||
|
||||
let rt = create_runtime();
|
||||
|
||||
let _callback: Callback<String, HtmlElement<AnyElement>> =
|
||||
(|x: String| {
|
||||
view! {
|
||||
<h1>{x}</h1>
|
||||
}
|
||||
})
|
||||
.into();
|
||||
|
||||
rt.dispose();
|
||||
}
|
||||
}
|
|
@ -139,6 +139,11 @@
|
|||
//! # }
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
#![cfg_attr(feature = "nightly", feature(auto_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(negative_impls))]
|
||||
|
||||
extern crate self as leptos;
|
||||
|
||||
pub mod prelude {
|
||||
|
@ -146,6 +151,7 @@ pub mod prelude {
|
|||
pub use tachys::prelude::*;
|
||||
}
|
||||
|
||||
pub mod callback;
|
||||
pub mod children;
|
||||
pub mod component;
|
||||
mod for_loop;
|
||||
|
@ -165,6 +171,7 @@ pub use typed_builder;
|
|||
pub use typed_builder_macro;
|
||||
mod into_view;
|
||||
pub use into_view::IntoView;
|
||||
pub use leptos_dom;
|
||||
pub use tachys;
|
||||
|
||||
mod mount;
|
||||
|
|
|
@ -13,7 +13,6 @@ use web_sys::HtmlElement;
|
|||
pub mod helpers;
|
||||
pub use tachys::html::event as events;
|
||||
|
||||
|
||||
/*#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
|
|
|
@ -366,7 +366,8 @@ fn attribute_to_tokens(
|
|||
fn event_to_tokens(name: &str, node: &KeyedAttribute) -> TokenStream {
|
||||
let handler = attribute_value(node);
|
||||
|
||||
let (event_type, is_custom, is_force_undelegated) = parse_event_name(name);
|
||||
let (event_type, is_custom, is_force_undelegated, is_targeted) =
|
||||
parse_event_name(name);
|
||||
|
||||
let event_name_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => {
|
||||
|
@ -379,19 +380,20 @@ fn event_to_tokens(name: &str, node: &KeyedAttribute) -> TokenStream {
|
|||
_ => unreachable!(),
|
||||
};
|
||||
let undelegated_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
|
||||
if last.to_string() == "undelegated" {
|
||||
Some(last)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
NodeName::Punctuated(parts) => {
|
||||
parts.iter().find(|part| part.to_string() == "undelegated")
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = if is_targeted {
|
||||
Ident::new("on_target", on.span()).to_token_stream()
|
||||
} else {
|
||||
on.to_token_stream()
|
||||
};
|
||||
let event_type = if is_custom {
|
||||
event_type
|
||||
} else if let Some(ev_name) = event_name_ident {
|
||||
|
@ -597,12 +599,13 @@ fn is_ambiguous_element(tag: &str) -> bool {
|
|||
tag == "a" || tag == "script" || tag == "title"
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
if let Some(event_name) = event_name.strip_suffix(":undelegated") {
|
||||
(event_name, true)
|
||||
} else {
|
||||
(event_name, false)
|
||||
}
|
||||
fn parse_event(event_name: &str) -> (String, bool, bool) {
|
||||
let is_undelegated = event_name.contains(":undelegated");
|
||||
let is_targeted = event_name.contains(":target");
|
||||
let event_name = event_name
|
||||
.replace(":undelegated", "")
|
||||
.replace(":target", "");
|
||||
(event_name, is_undelegated, is_targeted)
|
||||
}
|
||||
|
||||
/// Escapes Rust keywords that are also HTML attribute names
|
||||
|
@ -768,12 +771,12 @@ const TYPED_EVENTS: [&str; 126] = [
|
|||
|
||||
const CUSTOM_EVENT: &str = "Custom";
|
||||
|
||||
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool, bool) {
|
||||
let (name, is_force_undelegated, is_targeted) = parse_event(name);
|
||||
|
||||
let (event_type, is_custom) = TYPED_EVENTS
|
||||
.binary_search(&name)
|
||||
.map(|_| (name, false))
|
||||
.binary_search(&name.as_str())
|
||||
.map(|_| (name.as_str(), false))
|
||||
.unwrap_or((CUSTOM_EVENT, true));
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
|
@ -785,7 +788,7 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
|||
} else {
|
||||
event_type
|
||||
};
|
||||
(event_type, is_custom, is_force_undelegated)
|
||||
(event_type, is_custom, is_force_undelegated, is_targeted)
|
||||
}
|
||||
|
||||
fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
|
||||
|
|
|
@ -9,6 +9,7 @@ or_poisoned = { workspace = true }
|
|||
futures = "0.3"
|
||||
pin-project-lite = "0.2"
|
||||
rustc-hash = "1.1.0"
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
slotmap = "1"
|
||||
thiserror = "1"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
|
@ -20,6 +21,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
|
|||
|
||||
[features]
|
||||
nightly = []
|
||||
serde = ["dep:serde"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
use core::fmt::Debug;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock, Weak},
|
||||
};
|
||||
|
@ -94,6 +95,20 @@ impl<T> Debug for ArcMemo<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for ArcMemo<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.inner, &other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for ArcMemo<T> {}
|
||||
|
||||
impl<T> Hash for ArcMemo<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::ptr::hash(&Arc::as_ptr(&self.inner), state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> ReactiveNode for ArcMemo<T> {
|
||||
fn mark_dirty(&self) {
|
||||
self.inner.mark_dirty();
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
AnySource, AnySubscriber, ReactiveNode, Source, Subscriber,
|
||||
ToAnySource, ToAnySubscriber,
|
||||
},
|
||||
owner::{Stored, StoredData},
|
||||
owner::{StoredData, StoredValue},
|
||||
signal::guards::{Mapped, Plain, ReadGuard},
|
||||
traits::{DefinedAt, ReadUntracked},
|
||||
unwrap_signal,
|
||||
|
@ -20,7 +20,7 @@ use std::{
|
|||
pub struct AsyncDerived<T: Send + Sync + 'static> {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
inner: Stored<ArcAsyncDerived<T>>,
|
||||
inner: StoredValue<ArcAsyncDerived<T>>,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> StoredData for AsyncDerived<T> {
|
||||
|
@ -45,7 +45,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
|
|||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(ArcAsyncDerived::new(fun)),
|
||||
inner: StoredValue::new(ArcAsyncDerived::new(fun)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
|
|||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(ArcAsyncDerived::new_with_initial(
|
||||
inner: StoredValue::new(ArcAsyncDerived::new_with_initial(
|
||||
initial_value,
|
||||
fun,
|
||||
)),
|
||||
|
@ -75,7 +75,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
|
|||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(ArcAsyncDerived::new_unsync(fun)),
|
||||
inner: StoredValue::new(ArcAsyncDerived::new_unsync(fun)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
|
|||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(ArcAsyncDerived::new_unsync_with_initial(
|
||||
inner: StoredValue::new(ArcAsyncDerived::new_unsync_with_initial(
|
||||
initial_value,
|
||||
fun,
|
||||
)),
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use super::{inner::MemoInner, ArcMemo};
|
||||
use crate::{
|
||||
owner::{Stored, StoredData},
|
||||
owner::{StoredData, StoredValue},
|
||||
signal::guards::{Mapped, Plain, ReadGuard},
|
||||
traits::{DefinedAt, ReadUntracked, Track},
|
||||
};
|
||||
use std::{fmt::Debug, panic::Location};
|
||||
use std::{fmt::Debug, hash::Hash, panic::Location};
|
||||
|
||||
pub struct Memo<T: Send + Sync + 'static> {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
inner: Stored<ArcMemo<T>>,
|
||||
inner: StoredValue<ArcMemo<T>>,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Memo<T> {
|
||||
|
@ -25,7 +25,7 @@ impl<T: Send + Sync + 'static> Memo<T> {
|
|||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(ArcMemo::new(fun)),
|
||||
inner: StoredValue::new(ArcMemo::new(fun)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,20 @@ impl<T: Send + Sync + 'static> Debug for Memo<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> PartialEq for Memo<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Eq for Memo<T> {}
|
||||
|
||||
impl<T: Send + Sync + 'static> Hash for Memo<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> StoredData for Memo<T> {
|
||||
type Data = ArcMemo<T>;
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ where
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self { value, inner }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,8 @@ pub mod effect;
|
|||
pub mod graph;
|
||||
pub mod owner;
|
||||
pub mod selector;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
pub mod signal;
|
||||
pub mod traits;
|
||||
|
||||
|
@ -92,5 +94,5 @@ pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
|||
pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::traits::*;
|
||||
pub use crate::{owner::StoredData, traits::*};
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
mod arena;
|
||||
mod context;
|
||||
use arena::NodeId;
|
||||
pub use arena::{Stored, StoredData};
|
||||
pub use arena::{StoredData, StoredValue};
|
||||
pub use context::*;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
|
@ -3,6 +3,7 @@ use or_poisoned::OrPoisoned;
|
|||
use slotmap::{new_key_type, SlotMap};
|
||||
use std::{
|
||||
any::Any,
|
||||
hash::Hash,
|
||||
marker::PhantomData,
|
||||
sync::{OnceLock, RwLock},
|
||||
};
|
||||
|
@ -18,20 +19,35 @@ pub(crate) fn map(
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Stored<T> {
|
||||
pub struct StoredValue<T> {
|
||||
node: NodeId,
|
||||
ty: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Copy for Stored<T> {}
|
||||
impl<T> Copy for StoredValue<T> {}
|
||||
|
||||
impl<T> Clone for Stored<T> {
|
||||
impl<T> Clone for StoredValue<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Stored<T>
|
||||
impl<T> PartialEq for StoredValue<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.node == other.node && self.ty == other.ty
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for StoredValue<T> {}
|
||||
|
||||
impl<T> Hash for StoredValue<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.node.hash(state);
|
||||
self.ty.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StoredValue<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
|
|
123
reactive_graph/src/serde.rs
Normal file
123
reactive_graph/src/serde.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||
traits::With,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
impl<T: Send + Sync + Serialize + 'static> Serialize for ReadSignal<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + Serialize + 'static> Serialize for RwSignal<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + Serialize + 'static> Serialize for Memo<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize + 'static> Serialize for ArcReadSignal<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize + 'static> Serialize for ArcRwSignal<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + Serialize + 'static> Serialize for ArcMemo<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO MaybeSignal
|
||||
impl<T: Serialize> Serialize for MaybeSignal<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO MaybeProp
|
||||
impl<T: Serialize> Serialize for MaybeProp<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match &self.0 {
|
||||
None | Some(MaybeSignal::Static(None)) => {
|
||||
None::<T>.serialize(serializer)
|
||||
}
|
||||
Some(MaybeSignal::Static(Some(value))) => {
|
||||
value.serialize(serializer)
|
||||
}
|
||||
Some(MaybeSignal::Dynamic(signal)) => {
|
||||
signal.with(|value| value.serialize(serializer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Signal
|
||||
impl<T: Clone + Serialize> Serialize for Signal<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.get().serialize(serializer)
|
||||
}
|
||||
}*/
|
||||
|
||||
/* Deserialization for signal types */
|
||||
|
||||
impl<'de, T: Send + Sync + Deserialize<'de>> Deserialize<'de> for RwSignal<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
T::deserialize(deserializer).map(RwSignal::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for ArcRwSignal<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
T::deserialize(deserializer).map(ArcRwSignal::new)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO MaybeSignal
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
};
|
||||
use core::fmt::{Debug, Formatter, Result};
|
||||
use std::{
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
@ -40,6 +41,20 @@ impl<T> Debug for ArcReadSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for ArcReadSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.value, &other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for ArcReadSignal<T> {}
|
||||
|
||||
impl<T> Hash for ArcReadSignal<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::ptr::hash(&Arc::as_ptr(&self.value), state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcReadSignal<T> {
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
};
|
||||
use core::fmt::{Debug, Formatter, Result};
|
||||
use std::{
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
@ -42,6 +43,20 @@ impl<T> Debug for ArcRwSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for ArcRwSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.value, &other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for ArcRwSignal<T> {}
|
||||
|
||||
impl<T> Hash for ArcRwSignal<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::ptr::hash(&Arc::as_ptr(&self.value), state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcRwSignal<T> {
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
};
|
||||
use core::fmt::{Debug, Formatter, Result};
|
||||
use std::{
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
@ -38,6 +39,20 @@ impl<T> Debug for ArcWriteSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for ArcWriteSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.value, &other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for ArcWriteSignal<T> {}
|
||||
|
||||
impl<T> Hash for ArcWriteSignal<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::ptr::hash(&Arc::as_ptr(&self.value), state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcWriteSignal<T> {
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
|
|
|
@ -5,12 +5,13 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
graph::SubscriberSet,
|
||||
owner::{Stored, StoredData},
|
||||
owner::{StoredData, StoredValue},
|
||||
traits::{DefinedAt, IsDisposed, ReadUntracked},
|
||||
unwrap_signal,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
@ -18,7 +19,7 @@ use std::{
|
|||
pub struct ReadSignal<T: Send + Sync + 'static> {
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static Location<'static>,
|
||||
pub(crate) inner: Stored<ArcReadSignal<T>>,
|
||||
pub(crate) inner: StoredValue<ArcReadSignal<T>>,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Copy for ReadSignal<T> {}
|
||||
|
@ -38,6 +39,21 @@ impl<T: Send + Sync + 'static> Debug for ReadSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> PartialEq for ReadSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Eq for ReadSignal<T> {}
|
||||
|
||||
impl<T: Send + Sync + 'static> Hash for ReadSignal<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.defined_at.hash(state);
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> DefinedAt for ReadSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -93,7 +109,7 @@ impl<T: Send + Sync + 'static> From<ArcReadSignal<T>> for ReadSignal<T> {
|
|||
ReadSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(value),
|
||||
inner: StoredValue::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
graph::{ReactiveNode, SubscriberSet},
|
||||
owner::{Stored, StoredData},
|
||||
owner::{StoredData, StoredValue},
|
||||
traits::{DefinedAt, IsDisposed, ReadUntracked, Trigger, UpdateUntracked},
|
||||
unwrap_signal,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
@ -18,7 +19,7 @@ use std::{
|
|||
pub struct RwSignal<T: Send + Sync + 'static> {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
inner: Stored<ArcRwSignal<T>>,
|
||||
inner: StoredValue<ArcRwSignal<T>>,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> RwSignal<T> {
|
||||
|
@ -30,7 +31,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
|
|||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(ArcRwSignal::new(value)),
|
||||
inner: StoredValue::new(ArcRwSignal::new(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +40,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
|
|||
ReadSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(
|
||||
inner: StoredValue::new(
|
||||
self.get_value()
|
||||
.map(|inner| inner.read_only())
|
||||
.unwrap_or_else(unwrap_signal!(self)),
|
||||
|
@ -52,7 +53,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
|
|||
WriteSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(
|
||||
inner: StoredValue::new(
|
||||
self.get_value()
|
||||
.map(|inner| inner.write_only())
|
||||
.unwrap_or_else(unwrap_signal!(self)),
|
||||
|
@ -73,7 +74,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
|
|||
Some(Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(ArcRwSignal {
|
||||
inner: StoredValue::new(ArcRwSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
value: Arc::clone(&read.value),
|
||||
|
@ -106,6 +107,20 @@ impl<T: Send + Sync + 'static> Debug for RwSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> PartialEq for RwSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Eq for RwSignal<T> {}
|
||||
|
||||
impl<T: Send + Sync + 'static> Hash for RwSignal<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> DefinedAt for RwSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -178,7 +193,7 @@ impl<T: Send + Sync + 'static> From<ArcRwSignal<T>> for RwSignal<T> {
|
|||
RwSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: Stored::new(value),
|
||||
inner: StoredValue::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use super::ArcWriteSignal;
|
||||
use crate::{
|
||||
owner::{Stored, StoredData},
|
||||
owner::{StoredData, StoredValue},
|
||||
traits::{DefinedAt, IsDisposed, Trigger, UpdateUntracked},
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use std::panic::Location;
|
||||
use std::{hash::Hash, panic::Location};
|
||||
|
||||
pub struct WriteSignal<T: Send + Sync + 'static> {
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static Location<'static>,
|
||||
pub(crate) inner: Stored<ArcWriteSignal<T>>,
|
||||
pub(crate) inner: StoredValue<ArcWriteSignal<T>>,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Copy for WriteSignal<T> {}
|
||||
|
@ -29,6 +29,20 @@ impl<T: Send + Sync + 'static> Debug for WriteSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> PartialEq for WriteSignal<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Eq for WriteSignal<T> {}
|
||||
|
||||
impl<T: Send + Sync + 'static> Hash for WriteSignal<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> DefinedAt for WriteSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
attribute::*,
|
||||
class::{class, Class, IntoClass},
|
||||
element::ElementType,
|
||||
event::{on, EventDescriptor, On, TargetedEvent},
|
||||
event::{on, on_target, EventDescriptor, On, Targeted},
|
||||
property::{property, IntoProperty, Property},
|
||||
style::{style, IntoStyle, Style},
|
||||
},
|
||||
|
@ -304,8 +304,7 @@ where
|
|||
E: EventDescriptor + 'static,
|
||||
E::EventType: 'static,
|
||||
E::EventType: From<Rndr::Event>,
|
||||
F: FnMut(TargetedEvent<E::EventType, <Self as ElementType>::Output, Rndr>)
|
||||
+ 'static,
|
||||
F: FnMut(E::EventType) + 'static,
|
||||
Rndr: DomRenderer,
|
||||
Self: Sized + AddAttribute<On<Rndr>, Rndr>,
|
||||
{
|
||||
|
@ -319,6 +318,27 @@ where {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait OnTargetAttribute<E, F, T, Rndr>
|
||||
where
|
||||
Self: ElementType,
|
||||
E: EventDescriptor + 'static,
|
||||
E::EventType: 'static,
|
||||
E::EventType: From<Rndr::Event>,
|
||||
F: FnMut(Targeted<E::EventType, <Self as ElementType>::Output, Rndr>)
|
||||
+ 'static,
|
||||
Rndr: DomRenderer,
|
||||
Self: Sized + AddAttribute<On<Rndr>, Rndr>,
|
||||
{
|
||||
fn on_target(
|
||||
self,
|
||||
event: E,
|
||||
cb: F,
|
||||
) -> <Self as AddAttribute<On<Rndr>, Rndr>>::Output
|
||||
where {
|
||||
self.add_attr(on_target(event, cb))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Rndr, V> GlobalAttributes<Rndr, V> for T
|
||||
where
|
||||
T: AddAttribute<Attr<Accesskey, V, Rndr>, Rndr>
|
||||
|
@ -386,7 +406,19 @@ where
|
|||
E: EventDescriptor + 'static,
|
||||
E::EventType: 'static,
|
||||
E::EventType: From<Rndr::Event>,
|
||||
F: FnMut(TargetedEvent<E::EventType, <Self as ElementType>::Output, Rndr>)
|
||||
F: FnMut(E::EventType) + 'static,
|
||||
Rndr: DomRenderer,
|
||||
{
|
||||
}
|
||||
|
||||
impl<T, E, F, Rndr> OnTargetAttribute<E, F, Self, Rndr> for T
|
||||
where
|
||||
Self: ElementType,
|
||||
T: AddAttribute<On<Rndr>, Rndr>,
|
||||
E: EventDescriptor + 'static,
|
||||
E::EventType: 'static,
|
||||
E::EventType: From<Rndr::Event>,
|
||||
F: FnMut(Targeted<E::EventType, <Self as ElementType>::Output, Rndr>)
|
||||
+ 'static,
|
||||
Rndr: DomRenderer,
|
||||
{
|
||||
|
|
|
@ -3,16 +3,25 @@ use crate::{
|
|||
renderer::{CastFrom, DomRenderer},
|
||||
view::{Position, ToTemplate},
|
||||
};
|
||||
use std::{borrow::Cow, fmt::Debug, marker::PhantomData};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::Debug,
|
||||
marker::PhantomData,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
|
||||
pub struct TargetedEvent<E, T, R> {
|
||||
pub struct Targeted<E, T, R> {
|
||||
event: E,
|
||||
el_ty: PhantomData<T>,
|
||||
rndr: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<E, T, R> TargetedEvent<E, T, R> {
|
||||
impl<E, T, R> Targeted<E, T, R> {
|
||||
pub fn into_inner(self) -> E {
|
||||
self.event
|
||||
}
|
||||
|
||||
pub fn target(&self) -> T
|
||||
where
|
||||
T: CastFrom<R::Element>,
|
||||
|
@ -25,9 +34,23 @@ impl<E, T, R> TargetedEvent<E, T, R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E, T, R> From<E> for TargetedEvent<E, T, R> {
|
||||
impl<E, T, R> Deref for Targeted<E, T, R> {
|
||||
type Target = E;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.event
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T, R> DerefMut for Targeted<E, T, R> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.event
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T, R> From<E> for Targeted<E, T, R> {
|
||||
fn from(event: E) -> Self {
|
||||
TargetedEvent {
|
||||
Targeted {
|
||||
event,
|
||||
el_ty: PhantomData,
|
||||
rndr: PhantomData,
|
||||
|
@ -35,10 +58,7 @@ impl<E, T, R> From<E> for TargetedEvent<E, T, R> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn on<E, T, R>(
|
||||
event: E,
|
||||
mut cb: impl FnMut(TargetedEvent<E::EventType, T, R>) + 'static,
|
||||
) -> On<R>
|
||||
pub fn on<E, R>(event: E, mut cb: impl FnMut(E::EventType) + 'static) -> On<R>
|
||||
where
|
||||
E: EventDescriptor + 'static,
|
||||
E::EventType: 'static,
|
||||
|
@ -50,8 +70,7 @@ where
|
|||
setup: Box::new(move |el| {
|
||||
let cb = Box::new(move |ev: R::Event| {
|
||||
let ev = E::EventType::from(ev);
|
||||
let targeted_event = TargetedEvent::<_, T, R>::from(ev);
|
||||
cb(targeted_event);
|
||||
cb(ev);
|
||||
}) as Box<dyn FnMut(R::Event)>;
|
||||
|
||||
if E::BUBBLES && cfg!(feature = "delegation") {
|
||||
|
@ -69,6 +88,19 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn on_target<E, T, R>(
|
||||
event: E,
|
||||
mut cb: impl FnMut(Targeted<E::EventType, T, R>) + 'static,
|
||||
) -> On<R>
|
||||
where
|
||||
E: EventDescriptor + 'static,
|
||||
E::EventType: 'static,
|
||||
R: DomRenderer,
|
||||
E::EventType: From<R::Event>,
|
||||
{
|
||||
on(event, move |ev| cb(ev.into()))
|
||||
}
|
||||
|
||||
pub struct On<R: DomRenderer> {
|
||||
name: Cow<'static, str>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
|
|
|
@ -10,7 +10,7 @@ pub mod prelude {
|
|||
custom::CustomAttribute,
|
||||
global::{
|
||||
ClassAttribute, GlobalAttributes, OnAttribute,
|
||||
PropAttribute, StyleAttribute,
|
||||
OnTargetAttribute, PropAttribute, StyleAttribute,
|
||||
},
|
||||
},
|
||||
element::{ElementChild, InnerHtmlAttribute},
|
||||
|
|
Loading…
Reference in a new issue