preliminary work on directives (not useful yet until we have an ElementExt that allows you to do things declaratively from an Element

This commit is contained in:
Greg Johnston 2024-06-24 20:43:39 -04:00
parent fe7c7c3a99
commit 3ebea79e05
5 changed files with 340 additions and 7 deletions

View file

@ -1,7 +1,10 @@
use leptos::{ev::click, html::AnyElement, prelude::*};
use leptos::{ev::click, html::a, prelude::*};
use web_sys::{Element, HtmlElement};
// no extra parameter
pub fn highlight(el: HtmlElement<AnyElement>) {
pub fn highlight(el: Element, param: ()) {
// TODO
/*
let mut highlighted = false;
let _ = el.clone().on(click, move |_| {
@ -13,13 +16,16 @@ pub fn highlight(el: HtmlElement<AnyElement>) {
let _ = el.clone().style("background-color", "transparent");
}
});
*/
}
// one extra parameter
pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
pub fn copy_to_clipboard(el: Element, content: &'static str) {
// TODO
let content = content.to_string();
leptos::logging::log!("running copy_to_clipboard directive");
let _ = el.clone().on(click, move |evt| {
/*let _ = el.clone().on(click, move |evt| {
evt.prevent_default();
evt.stop_propagation();
@ -30,7 +36,7 @@ pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
.write_text(&content);
let _ = el.clone().inner_html(format!("Copied \"{}\"", &content));
});
});*/
}
// custom parameter
@ -52,7 +58,9 @@ impl From<()> for Amount {
}
// .into() will automatically be called on the parameter
pub fn add_dot(el: HtmlElement<AnyElement>, amount: Amount) {
pub fn add_dot(el: Element, amount: Amount) {
// TODO
/*
_ = el.clone().on(click, move |_| {
el.set_inner_text(&format!(
"{}{}",
@ -60,6 +68,7 @@ pub fn add_dot(el: HtmlElement<AnyElement>, amount: Amount) {
".".repeat(amount.0)
))
})
*/
}
#[component]
@ -78,7 +87,7 @@ pub fn App() -> impl IntoView {
view! {
<a href="#" use:copy_to_clipboard=data>"Copy \"" {data} "\" to clipboard"</a>
// automatically applies the directive to every root element in `SomeComponent`
<SomeComponent use:highlight />
//<SomeComponent use:highlight />
// no value will default to `().into()`
<button use:add_dot>"Add a dot"</button>
// `5.into()` automatically called

View file

@ -409,6 +409,8 @@ 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:") {
event_to_tokens(name, node)
} else if let Some(name) = name.strip_prefix("class:") {
@ -489,6 +491,16 @@ pub(crate) fn attribute_absolute(
quote! { ::leptos::tachys::html::attribute::#key(#value) },
)
}
} else if id == "use" {
let key = &parts[1];
let param = if let Some(value) = node.value() {
quote!(::std::convert::Into::into(#value))
} else {
quote_spanned!(node.key.span()=> ().into())
};
Some(
quote! { ::leptos::tachys::html::directive::directive((#key, #param)) },
)
} else if id == "style" || id == "class" {
let key = &node.key.to_string();
let key = key
@ -1020,3 +1032,18 @@ pub(crate) fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
),
}
}
pub(crate) fn directive_call_from_attribute_node(
attr: &KeyedAttribute,
directive_name: &str,
) -> TokenStream {
let handler = syn::Ident::new(directive_name, attr.key.span());
let param = if let Some(value) = attr.value() {
quote!(::std::convert::Into::into(#value))
} else {
quote_spanned!(attr.key.span()=> ().into())
};
quote! { .directive(#handler, #[allow(clippy::useless_conversion)] #param) }
}

View file

@ -0,0 +1,295 @@
use super::attribute::{Attribute, NextAttribute};
use crate::{
prelude::AddAnyAttr,
renderer::Renderer,
view::{Position, ToTemplate},
};
use send_wrapper::SendWrapper;
use std::{marker::PhantomData, sync::Arc};
pub trait DirectiveAttribute<T, P, D, Rndr>
where
D: IntoDirective<T, P, Rndr>,
Rndr: Renderer,
{
type Output;
fn directive(self, handler: D, param: P) -> Self::Output;
}
impl<V, T, P, D, Rndr> DirectiveAttribute<T, P, D, Rndr> for V
where
V: AddAnyAttr<Rndr>,
D: IntoDirective<T, P, Rndr>,
P: Clone + 'static,
T: 'static,
Rndr: Renderer,
{
type Output = <Self as AddAnyAttr<Rndr>>::Output<Directive<T, D, P, Rndr>>;
fn directive(self, handler: D, param: P) -> Self::Output {
self.add_any_attr(directive(handler, param))
}
}
#[inline(always)]
pub fn directive<T, P, D, R>(handler: D, param: P) -> Directive<T, D, P, R>
where
D: IntoDirective<T, P, R>,
R: Renderer,
{
Directive(SendWrapper::new(DirectiveInner {
handler,
param,
t: PhantomData,
rndr: PhantomData,
}))
}
#[derive(Debug)]
pub struct Directive<T, D, P, R>(SendWrapper<DirectiveInner<T, D, P, R>>);
impl<T, D, P, R> Clone for Directive<T, D, P, R>
where
P: Clone + 'static,
D: Clone,
{
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
#[derive(Debug)]
pub struct DirectiveInner<T, D, P, R> {
handler: D,
param: P,
t: PhantomData<T>,
rndr: PhantomData<R>,
}
impl<T, D, P, R> Clone for DirectiveInner<T, D, P, R>
where
P: Clone + 'static,
D: Clone,
{
fn clone(&self) -> Self {
Self {
handler: self.handler.clone(),
param: self.param.clone(),
t: PhantomData,
rndr: PhantomData,
}
}
}
impl<T, P, D, R> Attribute<R> for Directive<T, D, P, R>
where
D: IntoDirective<T, P, R>,
P: Clone + 'static, // TODO this is just here to make them cloneable
T: 'static,
R: Renderer,
{
const MIN_LENGTH: usize = 0;
type State = R::Element;
type Cloneable = Directive<T, D::Cloneable, P, R>;
type CloneableOwned = Directive<T, D::Cloneable, P, R>;
fn html_len(&self) -> usize {
0
}
fn to_html(
self,
_buf: &mut String,
_class: &mut String,
_style: &mut String,
_inner_html: &mut String,
) {
}
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
let inner = self.0.take();
inner.handler.run(el.clone(), inner.param);
el.clone()
}
fn build(self, el: &R::Element) -> Self::State {
let inner = self.0.take();
inner.handler.run(el.clone(), inner.param);
el.clone()
}
fn rebuild(self, state: &mut Self::State) {
let inner = self.0.take();
inner.handler.run(state.clone(), inner.param);
}
fn into_cloneable(self) -> Self::Cloneable {
self.into_cloneable_owned()
}
fn into_cloneable_owned(self) -> Self::CloneableOwned {
let DirectiveInner {
handler,
param,
t,
rndr,
} = self.0.take();
Directive(SendWrapper::new(DirectiveInner {
handler: handler.into_cloneable(),
param,
t,
rndr,
}))
}
}
impl<T, D, P, R> NextAttribute<R> for Directive<T, D, P, R>
where
D: IntoDirective<T, P, R>,
P: Clone + 'static,
T: 'static,
R: Renderer,
{
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
fn add_any_attr<NewAttr: Attribute<R>>(
self,
new_attr: NewAttr,
) -> Self::Output<NewAttr> {
(self, new_attr)
}
}
impl<T, D, P, R> ToTemplate for Directive<T, D, P, R> {
const CLASS: &'static str = "";
fn to_template(
_buf: &mut String,
_class: &mut String,
_style: &mut String,
_inner_html: &mut String,
_position: &mut Position,
) {
}
}
/// 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: R::Element) {
/// // do sth
/// }
///
/// // This requires an attribute value
/// fn another_directive(el: R::Element, 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 IntoDirective<T: ?Sized, P, R: Renderer> {
type Cloneable: IntoDirective<T, P, R> + Clone + 'static;
/// Calls the handler function
fn run(&self, el: R::Element, param: P);
fn into_cloneable(self) -> Self::Cloneable;
}
impl<F, R> IntoDirective<(R::Element,), (), R> for F
where
F: Fn(R::Element) + 'static,
R: Renderer,
{
type Cloneable = Arc<dyn Fn(R::Element)>;
fn run(&self, el: R::Element, _: ()) {
self(el)
}
fn into_cloneable(self) -> Self::Cloneable {
Arc::new(self)
}
}
impl<R> IntoDirective<(R::Element,), (), R> for Arc<dyn Fn(R::Element)>
where
R: Renderer,
{
type Cloneable = Arc<dyn Fn(R::Element)>;
fn run(&self, el: R::Element, _: ()) {
self(el)
}
fn into_cloneable(self) -> Self::Cloneable {
self
}
}
impl<F, P, R> IntoDirective<(R::Element, P), P, R> for F
where
F: Fn(R::Element, P) + 'static,
P: 'static,
R: Renderer,
{
type Cloneable = Arc<dyn Fn(R::Element, P)>;
fn run(&self, el: R::Element, param: P) {
self(el, param);
}
fn into_cloneable(self) -> Self::Cloneable {
Arc::new(self)
}
}
impl<P, R> IntoDirective<(R::Element, P), P, R> for Arc<dyn Fn(R::Element, P)>
where
R: Renderer,
P: 'static,
{
type Cloneable = Arc<dyn Fn(R::Element, P)>;
fn run(&self, el: R::Element, param: P) {
self(el, param)
}
fn into_cloneable(self) -> Self::Cloneable {
self
}
}

View file

@ -7,6 +7,7 @@ use std::marker::PhantomData;
pub mod attribute;
pub mod class;
pub mod directive;
pub mod element;
pub mod event;
pub mod islands;

View file

@ -14,6 +14,7 @@ pub mod prelude {
OnTargetAttribute, PropAttribute, StyleAttribute,
},
},
directive::DirectiveAttribute,
element::{ElementChild, InnerHtmlAttribute},
node_ref::NodeRefAttribute,
},