Skip to content

Commit

Permalink
feat: add directives with use: (#1821)
Browse files Browse the repository at this point in the history
  • Loading branch information
maccesch authored Oct 19, 2023
1 parent 9a70898 commit c87328f
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 14 deletions.
2 changes: 2 additions & 0 deletions examples/directives/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
rustflags = ["--cfg=web_sys_unstable_apis"]
17 changes: 17 additions & 0 deletions examples/directives/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions examples/directives/Makefile.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]
7 changes: 7 additions & 0 deletions examples/directives/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions examples/directives/index.html
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions examples/directives/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

[toolchain]
channel = "nightly"
51 changes: 51 additions & 0 deletions examples/directives/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions examples/directives/src/main.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 58 additions & 0 deletions examples/directives/tests/web.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 83 additions & 0 deletions leptos_dom/src/directive.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
30 changes: 26 additions & 4 deletions leptos_dom/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -508,7 +509,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
use once_cell::unsync::OnceCell;
use std::{
cell::RefCell,
rc::Rc,
task::{Poll, Waker},
};

Expand Down Expand Up @@ -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))]
Expand Down
Loading

0 comments on commit c87328f

Please sign in to comment.