mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: add directives with use:
(#1821)
This commit is contained in:
parent
9a70898b09
commit
c87328f5cf
15 changed files with 370 additions and 14 deletions
2
examples/directives/.cargo/config.toml
Normal file
2
examples/directives/.cargo/config.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[build]
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
17
examples/directives/Cargo.toml
Normal file
17
examples/directives/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "directives"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
web-sys = { version = "0.3", features = ["Clipboard", "Navigator"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = "0.3"
|
||||
gloo-timers = { version = "0.3", features = ["futures"] }
|
5
examples/directives/Makefile.toml
Normal file
5
examples/directives/Makefile.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
7
examples/directives/README.md
Normal file
7
examples/directives/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Leptos Directives Example
|
||||
|
||||
This example showcases a basic leptos app that shows how to write and use directives.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
7
examples/directives/index.html
Normal file
7
examples/directives/index.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
3
examples/directives/rust-toolchain.toml
Normal file
3
examples/directives/rust-toolchain.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
51
examples/directives/src/lib.rs
Normal file
51
examples/directives/src/lib.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use leptos::{ev::click, html::AnyElement, *};
|
||||
|
||||
pub fn highlight(el: HtmlElement<AnyElement>) {
|
||||
let mut highlighted = false;
|
||||
|
||||
let _ = el.clone().on(click, move |_| {
|
||||
highlighted = !highlighted;
|
||||
|
||||
if highlighted {
|
||||
let _ = el.clone().style("background-color", "yellow");
|
||||
} else {
|
||||
let _ = el.clone().style("background-color", "transparent");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
|
||||
let content = content.to_string();
|
||||
|
||||
let _ = el.clone().on(click, move |evt| {
|
||||
evt.prevent_default();
|
||||
evt.stop_propagation();
|
||||
|
||||
let _ = window()
|
||||
.navigator()
|
||||
.clipboard()
|
||||
.expect("navigator.clipboard to be available")
|
||||
.write_text(&content);
|
||||
|
||||
let _ = el.clone().inner_html(format!("Copied \"{}\"", &content));
|
||||
});
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SomeComponent() -> impl IntoView {
|
||||
view! {
|
||||
<p>Some paragraphs</p>
|
||||
<p>that can be clicked</p>
|
||||
<p>in order to highlight them</p>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let data = "Hello World!";
|
||||
|
||||
view! {
|
||||
<a href="#" use:copy_to_clipboard=data>"Copy \"" {data} "\" to clipboard"</a>
|
||||
<SomeComponent use:highlight />
|
||||
}
|
||||
}
|
8
examples/directives/src/main.rs
Normal file
8
examples/directives/src/main.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use directives::App;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| view! { <App/> })
|
||||
}
|
58
examples/directives/tests/web.rs
Normal file
58
examples/directives/tests/web.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use gloo_timers::future::sleep;
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use directives::App;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_directives() {
|
||||
mount_to_body(|| view! { <App/> });
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
let document = leptos::document();
|
||||
let paragraphs = document.query_selector_all("p").unwrap();
|
||||
|
||||
assert_eq!(paragraphs.length(), 3);
|
||||
|
||||
for i in 0..paragraphs.length() {
|
||||
println!("i: {}", i);
|
||||
let p = paragraphs
|
||||
.item(i)
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
p.style().get_property_value("background-color").unwrap(),
|
||||
""
|
||||
);
|
||||
|
||||
p.click();
|
||||
|
||||
assert_eq!(
|
||||
p.style().get_property_value("background-color").unwrap(),
|
||||
"yellow"
|
||||
);
|
||||
|
||||
p.click();
|
||||
|
||||
assert_eq!(
|
||||
p.style().get_property_value("background-color").unwrap(),
|
||||
"transparent"
|
||||
);
|
||||
}
|
||||
|
||||
let a = document
|
||||
.query_selector("a")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
assert_eq!(a.inner_html(), "Copy \"Hello World!\" to clipboard");
|
||||
|
||||
a.click();
|
||||
assert_eq!(a.inner_html(), "Copied \"Hello World!\"");
|
||||
}
|
83
leptos_dom/src/directive.rs
Normal file
83
leptos_dom/src/directive.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use crate::{html::AnyElement, HtmlElement};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Trait for a directive handler function.
|
||||
/// This is used so it's possible to use functions with one or two
|
||||
/// parameters as directive handlers.
|
||||
///
|
||||
/// You can use directives like the following.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::{*, html::AnyElement};
|
||||
///
|
||||
/// // This doesn't take an attribute value
|
||||
/// fn my_directive(el: HtmlElement<AnyElement>) {
|
||||
/// // do sth
|
||||
/// }
|
||||
///
|
||||
/// // This requires an attribute value
|
||||
/// fn another_directive(el: HtmlElement<AnyElement>, params: i32) {
|
||||
/// // do sth
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent() -> impl IntoView {
|
||||
/// view! {
|
||||
/// // no attribute value
|
||||
/// <div use:my_directive></div>
|
||||
///
|
||||
/// // with an attribute value
|
||||
/// <div use:another_directive=8></div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A directive is just syntactic sugar for
|
||||
///
|
||||
/// ```ignore
|
||||
/// let node_ref = create_node_ref();
|
||||
///
|
||||
/// create_effect(move |_| {
|
||||
/// if let Some(el) = node_ref.get() {
|
||||
/// directive_func(el, possibly_some_param);
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// A directive can be a function with one or two parameters.
|
||||
/// The first is the element the directive is added to and the optional
|
||||
/// second is the parameter that is provided in the attribute.
|
||||
pub trait Directive<T: ?Sized, P> {
|
||||
/// Calls the handler function
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P);
|
||||
}
|
||||
|
||||
impl<F> Directive<(HtmlElement<AnyElement>,), ()> for F
|
||||
where
|
||||
F: Fn(HtmlElement<AnyElement>),
|
||||
{
|
||||
fn run(&self, el: HtmlElement<AnyElement>, _: ()) {
|
||||
self(el)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, P> Directive<(HtmlElement<AnyElement>, P), P> for F
|
||||
where
|
||||
F: Fn(HtmlElement<AnyElement>, P),
|
||||
{
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
|
||||
self(el, param);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, P> Directive<T, P> for Rc<dyn Directive<T, P>> {
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
|
||||
(**self).run(el, param)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, P> Directive<T, P> for Box<dyn Directive<T, P>> {
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
|
||||
(**self).run(el, param);
|
||||
}
|
||||
}
|
|
@ -62,15 +62,16 @@ cfg_if! {
|
|||
}
|
||||
|
||||
use crate::{
|
||||
create_node_ref,
|
||||
ev::EventDescriptor,
|
||||
hydration::HydrationCtx,
|
||||
macro_helpers::{
|
||||
Attribute, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
|
||||
},
|
||||
Element, Fragment, IntoView, NodeRef, Text, View,
|
||||
Directive, Element, Fragment, IntoView, NodeRef, Text, View,
|
||||
};
|
||||
use leptos_reactive::Oco;
|
||||
use std::fmt;
|
||||
use leptos_reactive::{create_effect, Oco};
|
||||
use std::{fmt, rc::Rc};
|
||||
|
||||
/// Trait which allows creating an element tag.
|
||||
pub trait ElementDescriptor: ElementDescriptorBounds {
|
||||
|
@ -508,7 +509,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
|||
use once_cell::unsync::OnceCell;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
task::{Poll, Waker},
|
||||
};
|
||||
|
||||
|
@ -1146,6 +1146,28 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<El: ElementDescriptor + Clone + 'static> HtmlElement<El> {
|
||||
/// Bind the directive to the element.
|
||||
#[inline(always)]
|
||||
pub fn directive<T: ?Sized, P: Clone + 'static>(
|
||||
self,
|
||||
handler: impl Directive<T, P> + 'static,
|
||||
param: P,
|
||||
) -> Self {
|
||||
let node_ref = create_node_ref::<El>();
|
||||
|
||||
let handler = Rc::new(handler);
|
||||
|
||||
let _ = create_effect(move |_| {
|
||||
if let Some(el) = node_ref.get() {
|
||||
Rc::clone(&handler).run(el.into_any(), param.clone());
|
||||
}
|
||||
});
|
||||
|
||||
self.node_ref(node_ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "trace", name = "<HtmlElement />", skip_all, fields(tag = %self.element.name())))]
|
||||
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
pub extern crate tracing;
|
||||
|
||||
mod components;
|
||||
mod directive;
|
||||
mod events;
|
||||
pub mod helpers;
|
||||
pub mod html;
|
||||
|
@ -25,8 +26,10 @@ pub mod ssr;
|
|||
pub mod ssr_in_order;
|
||||
pub mod svg;
|
||||
mod transparent;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
pub use components::*;
|
||||
pub use directive::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub use events::add_event_helper;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
|
@ -46,9 +49,9 @@ pub use node_ref::*;
|
|||
use once_cell::unsync::Lazy as LazyCell;
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, fmt};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::cell::RefCell;
|
||||
use std::{borrow::Cow, fmt, rc::Rc};
|
||||
pub use transparent::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
@ -135,7 +138,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<N> IntoView for std::rc::Rc<dyn Fn() -> N>
|
||||
impl<N> IntoView for Rc<dyn Fn() -> N>
|
||||
where
|
||||
N: IntoView + 'static,
|
||||
{
|
||||
|
@ -709,8 +712,7 @@ impl View {
|
|||
|
||||
/// Adds an event listener, analogous to [`HtmlElement::on`].
|
||||
///
|
||||
/// This method will attach an event listener to **all** child
|
||||
/// [`HtmlElement`] children.
|
||||
/// This method will attach an event listener to **all** children
|
||||
#[inline(always)]
|
||||
pub fn on<E: ev::EventDescriptor + 'static>(
|
||||
self,
|
||||
|
@ -755,7 +757,7 @@ impl View {
|
|||
let event_handler = Rc::new(RefCell::new(event_handler));
|
||||
|
||||
c.children.iter().cloned().for_each(|c| {
|
||||
let event_handler = event_handler.clone();
|
||||
let event_handler = Rc::clone(&event_handler);
|
||||
|
||||
_ = c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
|
||||
});
|
||||
|
@ -775,6 +777,65 @@ impl View {
|
|||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a directive analogous to [`HtmlElement::directive`].
|
||||
///
|
||||
/// This method will attach directive to **all** child
|
||||
/// [`HtmlElement`] children.
|
||||
#[inline(always)]
|
||||
pub fn directive<T, P>(
|
||||
self,
|
||||
handler: impl Directive<T, P> + 'static,
|
||||
param: P,
|
||||
) -> Self
|
||||
where
|
||||
T: ?Sized + 'static,
|
||||
P: Clone + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
trace!("calling directive()");
|
||||
let span = ::tracing::Span::current();
|
||||
let handler = move |e, p| {
|
||||
let _guard = span.enter();
|
||||
handler.run(e, p);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.directive_impl(Box::new(handler), param)
|
||||
}
|
||||
|
||||
fn directive_impl<T, P>(
|
||||
self,
|
||||
handler: Box<dyn Directive<T, P>>,
|
||||
param: P,
|
||||
) -> Self
|
||||
where
|
||||
T: ?Sized + 'static,
|
||||
P: Clone + 'static,
|
||||
{
|
||||
cfg_if! { if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
match &self {
|
||||
Self::Element(el) => {
|
||||
let _ = el.clone().into_html_element().directive(handler, param);
|
||||
}
|
||||
Self::Component(c) => {
|
||||
let handler = Rc::from(handler);
|
||||
|
||||
for child in c.children.iter().cloned() {
|
||||
let _ = child.directive(Rc::clone(&handler), param.clone());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
let _ = handler;
|
||||
let _ = param;
|
||||
}}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(debug_assertions, instrument)]
|
||||
|
|
|
@ -6,7 +6,7 @@ use super::{
|
|||
is_self_closing, is_svg_element, parse_event_name,
|
||||
slot_helper::{get_slot, slot_to_tokens},
|
||||
};
|
||||
use crate::attribute_value;
|
||||
use crate::{attribute_value, view::directive_call_from_attribute_node};
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
|
@ -383,6 +383,8 @@ pub(crate) fn attribute_to_tokens(
|
|||
quote! {
|
||||
.#node_ref(#value)
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("use:") {
|
||||
directive_call_from_attribute_node(node, name)
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(node);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ use super::{
|
|||
client_builder::{fragment_to_tokens, TagType},
|
||||
event_from_attribute_node,
|
||||
};
|
||||
use crate::view::directive_call_from_attribute_node;
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote};
|
||||
use rstml::node::{NodeAttribute, NodeElement};
|
||||
|
@ -34,6 +35,7 @@ pub(crate) fn component_to_tokens(
|
|||
&& !attr.key.to_string().starts_with("clone:")
|
||||
&& !attr.key.to_string().starts_with("on:")
|
||||
&& !attr.key.to_string().starts_with("attr:")
|
||||
&& !attr.key.to_string().starts_with("use:")
|
||||
})
|
||||
.map(|attr| {
|
||||
let name = &attr.key;
|
||||
|
@ -82,6 +84,19 @@ pub(crate) fn component_to_tokens(
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let directives = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("use:")
|
||||
.map(|ident| directive_call_from_attribute_node(attr, ident))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let events_and_directives =
|
||||
events.into_iter().chain(directives).collect::<Vec<_>>();
|
||||
|
||||
let dyn_attrs = attrs
|
||||
.filter(|attr| attr.key.to_string().starts_with("attr:"))
|
||||
.filter_map(|attr| {
|
||||
|
@ -192,12 +207,12 @@ pub(crate) fn component_to_tokens(
|
|||
/* #[cfg(debug_assertions)]
|
||||
IdeTagHelper::add_component_completion(&mut component, node); */
|
||||
|
||||
if events.is_empty() {
|
||||
if events_and_directives.is_empty() {
|
||||
component
|
||||
} else {
|
||||
quote! {
|
||||
#component.into_view()
|
||||
#(#events)*
|
||||
#(#events_and_directives)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{attribute_value, Mode};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use rstml::node::{KeyedAttribute, Node, NodeElement, NodeName};
|
||||
use syn::{spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprPath, Lit};
|
||||
|
||||
|
@ -531,3 +531,18 @@ pub(crate) fn event_from_attribute_node(
|
|||
};
|
||||
(event_type, handler)
|
||||
}
|
||||
|
||||
pub(crate) fn directive_call_from_attribute_node(
|
||||
attr: &KeyedAttribute,
|
||||
directive_name: &str,
|
||||
) -> TokenStream {
|
||||
let handler = format_ident!("{directive_name}", span = attr.key.span());
|
||||
|
||||
let param = if let Some(value) = attr.value() {
|
||||
quote! { #value.into() }
|
||||
} else {
|
||||
quote! { () }
|
||||
};
|
||||
|
||||
quote! { .directive(#handler, #param) }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue