Merge pull request #12 from thaw-ui/feat/binder

Feat/binder
This commit is contained in:
luoxiaozero 2023-11-12 02:35:22 +08:00 committed by GitHub
commit 34dfe2e553
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 534 additions and 188 deletions

View file

@ -1,6 +1,5 @@
.thaw-auto-complete__menu { .thaw-auto-complete__menu {
position: relative; width: 100%;
display: inline-block;
max-height: 200px; max-height: 200px;
padding: 5px; padding: 5px;
background-color: var(--thaw-background-color); background-color: var(--thaw-background-color);
@ -9,7 +8,6 @@
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
overflow: auto; overflow: auto;
z-index: 2000;
} }
.thaw-auto-complete__menu-item { .thaw-auto-complete__menu-item {
padding: 6px 5px; padding: 6px 5px;

View file

@ -1,6 +1,11 @@
mod theme; mod theme;
use crate::{mount_style, teleport::Teleport, use_theme, utils::StoredMaybeSignal, Input, Theme}; use crate::{
components::{Binder, Follower, FollowerPlacement, FollowerWidth},
mount_style, use_theme,
utils::StoredMaybeSignal,
Input, Theme,
};
use leptos::*; use leptos::*;
pub use theme::AutoCompleteTheme; pub use theme::AutoCompleteTheme;
@ -37,93 +42,66 @@ pub fn AutoComplete(
let is_show_menu = create_rw_signal(false); let is_show_menu = create_rw_signal(false);
let auto_complete_ref = create_node_ref::<html::Div>(); let auto_complete_ref = create_node_ref::<html::Div>();
let auto_complete_menu_ref = create_node_ref::<html::Div>();
let options = StoredMaybeSignal::from(options); let options = StoredMaybeSignal::from(options);
let show_menu = move || {
is_show_menu.set(true);
let rect = auto_complete_ref
.get_untracked()
.unwrap()
.get_bounding_client_rect();
let auto_complete_menu = auto_complete_menu_ref.get_untracked().unwrap();
_ = auto_complete_menu
.style("width", format!("{}px", rect.width()))
.style(
"transform",
format!(
"translateX({}px) translateY({}px)",
rect.x(),
rect.y() + rect.height()
),
);
};
let allow_value = move |_| { let allow_value = move |_| {
if !is_show_menu.get_untracked() { if !is_show_menu.get_untracked() {
show_menu(); is_show_menu.set(true);
} }
true true
}; };
view! { view! {
<div class="thaw-auto-complete" ref=auto_complete_ref> <Binder target_ref=auto_complete_ref>
<Input <div class="thaw-auto-complete" ref=auto_complete_ref>
value <Input
placeholder value
on_focus=move |_| show_menu() placeholder
on_blur=move |_| is_show_menu.set(false) on_focus=move |_| is_show_menu.set(true)
allow_value on_blur=move |_| is_show_menu.set(false)
/> allow_value
</div> />
<Teleport>
<div
class="thaw-auto-complete__menu"
style=move || {
if is_show_menu.get() {
menu_css_vars.get()
} else {
"display: none;".to_string()
}
}
ref=auto_complete_menu_ref
>
{move || {
options
.get()
.into_iter()
.map(|v| {
let AutoCompleteOption { value: option_value, label } = v;
let on_click = move |_| {
if clear_after_select.get_untracked() {
value.set(String::new());
} else {
value.set(option_value.clone());
}
if let Some(on_select) = on_select {
on_select.call(option_value.clone());
}
is_show_menu.set(false);
};
let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default();
};
view! {
<div
class="thaw-auto-complete__menu-item"
on:click=on_click
on:mousedown=on_mousedown
>
{label}
</div>
}
})
.collect_view()
}}
</div> </div>
</Teleport> <Follower slot show=is_show_menu placement=FollowerPlacement::BottomStart width=FollowerWidth::Target>
<div
class="thaw-auto-complete__menu"
style=move || menu_css_vars.get()
>
{move || {
options
.get()
.into_iter()
.map(|v| {
let AutoCompleteOption { value: option_value, label } = v;
let on_click = move |_| {
if clear_after_select.get_untracked() {
value.set(String::new());
} else {
value.set(option_value.clone());
}
if let Some(on_select) = on_select {
on_select.call(option_value.clone());
}
is_show_menu.set(false);
};
let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default();
};
view! {
<div
class="thaw-auto-complete__menu-item"
on:click=on_click
on:mousedown=on_mousedown
>
{label}
</div>
}
})
.collect_view()
}}
</div>
</Follower>
</Binder>
} }
} }

View file

@ -18,10 +18,6 @@
} }
.thaw-color-picker-popover { .thaw-color-picker-popover {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 240px; width: 240px;
padding: 12px; padding: 12px;
background-color: var(--thaw-background-color); background-color: var(--thaw-background-color);
@ -29,7 +25,6 @@
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
z-index: 1000;
} }
.thaw-color-picker-popover__panel { .thaw-color-picker-popover__panel {

View file

@ -1,7 +1,8 @@
mod color; mod color;
mod theme; mod theme;
use crate::{mount_style, teleport::Teleport, use_theme, Theme}; use crate::components::{Binder, Follower, FollowerPlacement};
use crate::{mount_style, use_theme, Theme};
pub use color::*; pub use color::*;
use leptos::*; use leptos::*;
use leptos::{leptos_dom::helpers::WindowListenerHandle, wasm_bindgen::__rt::IntoJsResult}; use leptos::{leptos_dom::helpers::WindowListenerHandle, wasm_bindgen::__rt::IntoJsResult};
@ -62,18 +63,7 @@ pub fn ColorPicker(#[prop(optional, into)] value: RwSignal<RGBA>) -> impl IntoVi
let trigger_ref = create_node_ref::<html::Div>(); let trigger_ref = create_node_ref::<html::Div>();
let popover_ref = create_node_ref::<html::Div>(); let popover_ref = create_node_ref::<html::Div>();
let show_popover = move |_| { let show_popover = move |_| {
let rect = trigger_ref.get().unwrap().get_bounding_client_rect();
is_show_popover.set(true); is_show_popover.set(true);
if let Some(popover_ref) = popover_ref.get() {
_ = popover_ref.style(
"transform",
format!(
"translateX({}px) translateY({}px)",
rect.x(),
rect.y() + rect.height()
),
);
}
}; };
let timer = window_event_listener(ev::click, move |ev| { let timer = window_event_listener(ev::click, move |ev| {
let el = ev.target(); let el = ev.target();
@ -96,28 +86,24 @@ pub fn ColorPicker(#[prop(optional, into)] value: RwSignal<RGBA>) -> impl IntoVi
on_cleanup(move || timer.remove()); on_cleanup(move || timer.remove());
view! { view! {
<div class="thaw-color-picker-trigger" on:click=show_popover ref=trigger_ref> <Binder target_ref=trigger_ref>
<div class="thaw-color-picker-trigger__content" style=move || style.get()> <div class="thaw-color-picker-trigger" on:click=show_popover ref=trigger_ref>
{move || label.get()} <div class="thaw-color-picker-trigger__content" style=move || style.get()>
{move || label.get()}
</div>
</div> </div>
</div> <Follower slot show=is_show_popover placement=FollowerPlacement::BottomStart>
<Teleport> <div
<div class="thaw-color-picker-popover"
class="thaw-color-picker-popover" ref=popover_ref
ref=popover_ref style=move || popover_css_vars.get()
style=move || { >
if is_show_popover.get() {
popover_css_vars.get()
} else {
"display: none".to_string()
}
}
>
<ColorPanel hue=hue.read_only() sv/> <ColorPanel hue=hue.read_only() sv/>
<HueSlider hue/> <HueSlider hue/>
</div> </div>
</Teleport> </Follower>
</Binder>
} }
} }

View file

@ -0,0 +1,13 @@
.thaw-binder-follower-container {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 0;
z-index: auto;
}
.thaw-binder-follower-content {
position: absolute;
z-index: 2000;
}

View file

@ -0,0 +1,63 @@
use leptos::window;
use web_sys::DomRect;
#[derive(Clone)]
pub enum FollowerPlacement {
// Top,
// Bottom,
// Left,
// Right,
// TopStart,
// TopEnd,
// LeftStart,
// LeftEnd,
// RightStart,
// RightEnd,
BottomStart,
// BottomEnd,
}
impl Copy for FollowerPlacement {}
pub fn get_follower_placement_style(
placement: FollowerPlacement,
target_rect: DomRect,
follower_rect: DomRect,
) -> Option<String> {
// TODO: Implements FollowerPlacement more properties
_ = placement;
let mut style = String::new();
let left = target_rect.x();
let top = {
let follower_height = follower_rect.height();
let target_y = target_rect.y();
let target_height = target_rect.height();
let top = target_y + target_height;
let Some(inner_height) = window_inner_height() else {
return None;
};
if top + follower_height > inner_height && target_y - follower_height >= 0.0 {
target_y - follower_height
} else {
top
}
};
style.push_str(&format!(
"transform: translateX({left}px) translateY({top}px);"
));
Some(style)
}
fn window_inner_height() -> Option<f64> {
let Ok(inner_height) = window().inner_height() else {
return None;
};
let Some(inner_height) = inner_height.as_f64() else {
return None;
};
Some(inner_height)
}

View file

@ -0,0 +1,266 @@
mod get_placement_style;
use crate::{
mount_style,
teleport::Teleport,
utils::{add_event_listener, EventListenerHandle},
};
use get_placement_style::get_follower_placement_style;
pub use get_placement_style::FollowerPlacement;
use leptos::{
html::{AnyElement, ElementDescriptor, ToHtmlElement},
leptos_dom::helpers::WindowListenerHandle,
*,
};
#[slot]
pub struct Follower {
#[prop(into)]
show: MaybeSignal<bool>,
#[prop(optional)]
width: Option<FollowerWidth>,
placement: FollowerPlacement,
children: Children,
}
#[derive(Clone)]
pub enum FollowerWidth {
Target,
Px(u32),
}
impl Copy for FollowerWidth {}
#[component]
pub fn Binder<El: ElementDescriptor + Clone + 'static>(
#[prop(into)] target_ref: NodeRef<El>,
follower: Follower,
children: Children,
) -> impl IntoView {
mount_style("binder", include_str!("./binder.css"));
let Follower {
show: follower_show,
width: follower_width,
placement: follower_placement,
children: follower_children,
} = follower;
let scroll_listener = store_value(None::<Callback<()>>);
let scrollable_element_handle_vec = store_value::<Vec<EventListenerHandle>>(vec![]);
let resize_handle = store_value(None::<WindowListenerHandle>);
let ensure_scroll_listener = move || {
let mut cursor = target_ref.get_untracked().map(|target| target.into_any());
let mut scrollable_element_vec = vec![];
loop {
cursor = get_scroll_parent(cursor);
if let Some(cursor) = cursor.take() {
scrollable_element_vec.push(cursor);
} else {
break;
}
}
let handle_vec = scrollable_element_vec
.into_iter()
.map(|ele| {
add_event_listener(ele, ev::scroll, move |_| {
if let Some(scroll_listener) = scroll_listener.get_value() {
scroll_listener.call(());
}
})
})
.collect();
scrollable_element_handle_vec.set_value(handle_vec);
};
let add_scroll_listener = move |listener: Callback<()>| {
scroll_listener.update_value(|scroll_listener| {
if scroll_listener.is_none() {
ensure_scroll_listener();
}
*scroll_listener = Some(listener);
})
};
let remove_scroll_listener = move |_| {
scrollable_element_handle_vec.update_value(|vec| {
vec.drain(..).for_each(|handle| handle.remove());
});
scroll_listener.set_value(None);
};
let add_resize_listener = move |listener: Callback<()>| {
resize_handle.update_value(move |resize_handle| {
if let Some(handle) = resize_handle.take() {
handle.remove();
}
let handle = window_event_listener(ev::resize, move |_| {
listener.call(());
});
*resize_handle = Some(handle);
});
};
let remove_resize_listener = move |_| {
resize_handle.update_value(move |handle| {
if let Some(handle) = handle.take() {
handle.remove();
}
});
};
on_cleanup(move || {
remove_scroll_listener(());
remove_resize_listener(());
});
view! {
{children()}
<FollowerContainer
show=follower_show
target_ref
width=follower_width
placement=follower_placement
add_scroll_listener
remove_scroll_listener
add_resize_listener
remove_resize_listener
>
{follower_children()}
</FollowerContainer>
}
}
#[component]
fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
show: MaybeSignal<bool>,
target_ref: NodeRef<El>,
width: Option<FollowerWidth>,
placement: FollowerPlacement,
#[prop(into)] add_scroll_listener: Callback<Callback<()>>,
#[prop(into)] remove_scroll_listener: Callback<()>,
#[prop(into)] add_resize_listener: Callback<Callback<()>>,
#[prop(into)] remove_resize_listener: Callback<()>,
children: Children,
) -> impl IntoView {
let content_ref = create_node_ref::<html::Div>();
let content_style = create_rw_signal(String::new());
let sync_position: Callback<()> = Callback::new(move |_| {
let Some(content_ref) = content_ref.get_untracked() else {
return;
};
let Some(target_ref) = target_ref.get_untracked().map(|target| target.into_any()) else {
return;
};
let target_rect = target_ref.get_bounding_client_rect();
let content_rect = content_ref.get_bounding_client_rect();
let mut style = String::new();
if let Some(width) = width {
let width = match width {
FollowerWidth::Target => format!("width: {}px;", target_rect.width()),
FollowerWidth::Px(width) => format!("width: {width}px;"),
};
style.push_str(&width);
}
if let Some(placement_style) =
get_follower_placement_style(placement, target_rect, content_rect)
{
style.push_str(&placement_style);
} else {
logging::error!("Thaw-Binder: get_follower_placement_style return None");
}
content_style.set(style);
});
let is_show = create_memo(move |_| {
if target_ref.get().is_none() {
return false;
}
if content_ref.get().is_none() {
return false;
}
let is_show = show.get();
if is_show {
request_animation_frame(move || {
sync_position.call(());
});
add_scroll_listener.call(sync_position);
add_resize_listener.call(sync_position);
} else {
remove_scroll_listener.call(());
remove_resize_listener.call(());
}
is_show
});
let children = html::div()
.classes("thaw-binder-follower-container")
.style("display", move || (!is_show.get()).then_some("none"))
.child(
html::div()
.classes("thaw-binder-follower-content")
.node_ref(content_ref)
.attr("style", move || content_style.get())
.child(children()),
);
view! {
<Teleport element=children />
}
}
fn get_scroll_parent(element: Option<HtmlElement<AnyElement>>) -> Option<HtmlElement<AnyElement>> {
let Some(element) = element else {
return None;
};
fn get_parent_element(element: HtmlElement<AnyElement>) -> Option<HtmlElement<AnyElement>> {
if element.node_type() == 9 {
None
} else {
element.parent_element().map(|ele| ele.to_leptos_element())
}
}
let Some(parent_element) = get_parent_element(element) else {
return None;
};
if parent_element.node_type() == 9 {
return Some(parent_element);
}
if parent_element.node_type() == 1 {
fn get_overflow(
parent_element: &HtmlElement<AnyElement>,
) -> Option<(String, String, String)> {
let Ok(Some(css_style_declaration)) = window().get_computed_style(parent_element)
else {
return None;
};
let Ok(overflow) = css_style_declaration.get_property_value("overflow") else {
return None;
};
let Ok(overflow_x) = css_style_declaration.get_property_value("overflowX") else {
return None;
};
let Ok(overflow_y) = css_style_declaration.get_property_value("overflowY") else {
return None;
};
Some((overflow, overflow_x, overflow_y))
}
if let Some((overflow, overflow_x, overflow_y)) = get_overflow(&parent_element) {
let overflow = format!("{overflow}{overflow_x}{overflow_y}");
if overflow.contains("auto") {
return Some(parent_element);
}
if overflow.contains("scroll") {
return Some(parent_element);
}
if overflow.contains("overlay") {
return Some(parent_element);
}
}
}
get_scroll_parent(Some(parent_element))
}

View file

@ -1,6 +1,8 @@
mod binder;
mod if_comp; mod if_comp;
mod option_comp; mod option_comp;
pub use binder::*;
pub use if_comp::*; pub use if_comp::*;
use leptos::*; use leptos::*;
pub use option_comp::*; pub use option_comp::*;

View file

@ -1,6 +1,11 @@
mod theme; mod theme;
use crate::{teleport::Teleport, theme::use_theme, utils::mount_style::mount_style, Theme}; use crate::{
components::{Binder, Follower, FollowerPlacement, FollowerWidth},
theme::use_theme,
utils::mount_style::mount_style,
Theme,
};
use leptos::wasm_bindgen::__rt::IntoJsResult; use leptos::wasm_bindgen::__rt::IntoJsResult;
use leptos::*; use leptos::*;
use std::hash::Hash; use std::hash::Hash;
@ -66,20 +71,7 @@ where
let trigger_ref = create_node_ref::<html::Div>(); let trigger_ref = create_node_ref::<html::Div>();
let menu_ref = create_node_ref::<html::Div>(); let menu_ref = create_node_ref::<html::Div>();
let show_menu = move |_| { let show_menu = move |_| {
let rect = trigger_ref.get().unwrap().get_bounding_client_rect();
is_show_menu.set(true); is_show_menu.set(true);
if let Some(menu_ref) = menu_ref.get() {
_ = menu_ref
.style("width", format!("{}px", rect.width()))
.style(
"transform",
format!(
"translateX({}px) translateY({}px) translateX(-50%)",
rect.x() + rect.width() / 2.0,
rect.y() + rect.height()
),
);
}
}; };
let timer = window_event_listener(ev::click, move |ev| { let timer = window_event_listener(ev::click, move |ev| {
let el = ev.target(); let el = ev.target();
@ -111,47 +103,46 @@ where
None => String::new(), None => String::new(),
}); });
view! { view! {
<div class="thaw-select" ref=trigger_ref on:click=show_menu style=move || css_vars.get()> <Binder target_ref=trigger_ref>
<div class="thaw-select" ref=trigger_ref on:click=show_menu style=move || css_vars.get()>
{move || select_option_label.get()} {move || select_option_label.get()}
</div>
<Teleport>
<div
class="thaw-select-menu"
style=move || {
if is_show_menu.get() { menu_css_vars.get() } else { "display: none;".into() }
}
ref=menu_ref
>
<For
each=move || options.get()
key=move |item| item.value.clone()
children=move |item| {
let item = store_value(item);
let onclick = move |_| {
let SelectOption { value: item_value, label: _ } = item.get_value();
value.set(Some(item_value));
is_show_menu.set(false);
};
view! {
<div
class="thaw-select-menu__item"
class=(
"thaw-select-menu__item-selected",
move || value.get() == Some(item.get_value().value),
)
on:click=onclick
>
{item.get_value().label}
</div>
}
}
/>
</div> </div>
</Teleport> <Follower slot show=is_show_menu placement=FollowerPlacement::BottomStart width=FollowerWidth::Target>
<div
class="thaw-select-menu"
style=move || menu_css_vars.get()
ref=menu_ref
>
<For
each=move || options.get()
key=move |item| item.value.clone()
children=move |item| {
let item = store_value(item);
let onclick = move |_| {
let SelectOption { value: item_value, label: _ } = item.get_value();
value.set(Some(item_value));
is_show_menu.set(false);
};
view! {
<div
class="thaw-select-menu__item"
class=(
"thaw-select-menu__item-selected",
move || value.get() == Some(item.get_value().value),
)
on:click=onclick
>
{item.get_value().label}
</div>
}
}
/>
</div>
</Follower>
</Binder>
} }
} }

View file

@ -21,16 +21,12 @@
background-color: var(--thaw-background-color); background-color: var(--thaw-background-color);
box-sizing: border-box; box-sizing: border-box;
padding: 5px; padding: 5px;
position: absolute; width: 100%;
top: 0;
left: 0;
right: 0;
max-height: 200px; max-height: 200px;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
overflow: auto; overflow: auto;
z-index: 1000;
} }
.thaw-select-menu__item { .thaw-select-menu__item {

View file

@ -1,30 +1,36 @@
use cfg_if::cfg_if; use cfg_if::cfg_if;
use leptos::*; use leptos::{html::AnyElement, *};
/// https://github.com/solidjs/solid/blob/main/packages/solid/web/src/index.ts#L56 /// https://github.com/solidjs/solid/blob/main/packages/solid/web/src/index.ts#L56
#[component] #[component]
pub fn Teleport( pub fn Teleport(
#[prop(into, optional)] mount: Option<web_sys::Element>, #[prop(into, optional)] mount: Option<web_sys::Element>,
children: Children, #[prop(optional, into)] element: Option<HtmlElement<AnyElement>>,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView { ) -> impl IntoView {
cfg_if! { if #[cfg(target_arch = "wasm32")] { cfg_if! { if #[cfg(target_arch = "wasm32")] {
use leptos::{ use leptos::wasm_bindgen::JsCast;
wasm_bindgen::JsCast,
leptos_dom::Mountable
};
let mount = mount.unwrap_or_else(|| { let mount = mount.unwrap_or_else(|| {
document() document()
.body() .body()
.expect("body element not to exist") .expect("body element not to exist")
.unchecked_into() .unchecked_into()
}); });
let node = children().into_view();
let node = node.get_mountable_node(); let render_root = if let Some(element) = element {
_ = mount.append_child(&node); element
} else if let Some(children) = children {
html::div().child(children()).into_any()
} else {
return;
};
_ = mount.append_child(&render_root);
on_cleanup(move || { on_cleanup(move || {
_ = mount.remove_child(&node); _ = mount.remove_child(&render_root);
}); });
} else { } else {
_ = mount; _ = mount;
_ = element;
_ = children; _ = children;
}} }}
} }

View file

@ -0,0 +1,50 @@
use leptos::{html::AnyElement, *};
use wasm_bindgen::{prelude::Closure, JsCast};
pub fn add_event_listener<E: ev::EventDescriptor + 'static>(
target: HtmlElement<AnyElement>,
event: E,
cb: impl Fn(E::EventType) + 'static,
) -> EventListenerHandle
where
E::EventType: JsCast,
{
add_event_listener_untyped(target, &event.name(), move |e| {
cb(e.unchecked_into::<E::EventType>())
})
}
pub struct EventListenerHandle(Box<dyn FnOnce()>);
impl std::fmt::Debug for EventListenerHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("EventListenerHandle").finish()
}
}
impl EventListenerHandle {
pub fn remove(self) {
(self.0)()
}
}
fn add_event_listener_untyped(
target: HtmlElement<AnyElement>,
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) -> EventListenerHandle {
fn wel(
target: HtmlElement<AnyElement>,
cb: Box<dyn FnMut(web_sys::Event)>,
event_name: &str,
) -> EventListenerHandle {
let cb = Closure::wrap(cb).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
let event_name = event_name.to_string();
EventListenerHandle(Box::new(move || {
_ = target.remove_event_listener_with_callback(&event_name, cb.unchecked_ref());
}))
}
wel(target, Box::new(cb), event_name)
}

View file

@ -1,9 +1,11 @@
// mod callback; // mod callback;
mod component_ref; mod component_ref;
mod event_listener;
pub mod mount_style; pub mod mount_style;
pub mod signal; pub mod signal;
mod stored_maybe_signal; mod stored_maybe_signal;
// pub use callback::AsyncCallback; // pub use callback::AsyncCallback;
pub use component_ref::ComponentRef; pub use component_ref::ComponentRef;
pub use event_listener::*;
pub use stored_maybe_signal::*; pub use stored_maybe_signal::*;