feat: add directives with use: (#1821)

This commit is contained in:
Marc-Stefan Cassola 2023-10-19 21:15:36 +01:00 committed by GitHub
parent 9a70898b09
commit c87328f5cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 370 additions and 14 deletions

View file

@ -0,0 +1,2 @@
[build]
rustflags = ["--cfg=web_sys_unstable_apis"]

View 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"] }

View file

@ -0,0 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]

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

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

View file

@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"

View 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 />
}
}

View 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/> })
}

View 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!\"");
}

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

View file

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

View file

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

View file

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

View file

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

View file

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