todomvc example

This commit is contained in:
Greg Johnston 2024-02-18 21:22:03 -05:00
parent 1a7da39fb7
commit a8adf8eea2
28 changed files with 867 additions and 156 deletions

41
TODO.md
View file

@ -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

View file

@ -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>

View file

@ -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>
}
}

View file

@ -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)
>

View file

@ -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);
}

View file

@ -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
View 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 dont 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();
}
}

View file

@ -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;

View file

@ -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

View file

@ -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> {

View file

@ -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]

View file

@ -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();

View file

@ -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,
)),

View file

@ -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>;

View file

@ -78,6 +78,7 @@ where
}
}
});
Self { value, inner }
}
}

View file

@ -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::*};
}

View file

@ -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)]

View file

@ -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
View 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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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),
}
}
}

View file

@ -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),
}
}
}

View file

@ -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)]

View file

@ -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,
{

View file

@ -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)]

View file

@ -10,7 +10,7 @@ pub mod prelude {
custom::CustomAttribute,
global::{
ClassAttribute, GlobalAttributes, OnAttribute,
PropAttribute, StyleAttribute,
OnTargetAttribute, PropAttribute, StyleAttribute,
},
},
element::{ElementChild, InnerHtmlAttribute},