Skip to content
Prev Previous commit
Next Next commit
Update Primitive to Leptos 0.7
  • Loading branch information
geoffreygarrett committed Jan 8, 2025
commit e896ace2c06b0f85a58bd61957d1d352f6c9695b
14 changes: 14 additions & 0 deletions packages/primitives/leptos/primitive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "radix-leptos-primitive"
description = "Leptos port of Radix Primitive."

authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true

[dependencies]
leptos.workspace = true
leptos-node-ref.workspace = true
leptos-typed-fallback-show.workspace = true
98 changes: 98 additions & 0 deletions packages/primitives/leptos/primitive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

<p align="center">
<a href="../../../../logo.svg">
<img src="../../../../logo.svg" width="300" height="200" alt="Rust Radix Logo">
</a>
</p>

<h1 align="center">radix-leptos-primitive</h1>

This is an internal utility, not intended for public usage.

[Rust Radix](https://github.com/RustForWeb/radix) is a Rust port of [Radix](https://www.radix-ui.com/primitives).

## Overview

```rust
use leptos::*;
use leptos_node_ref::AnyNodeRef;
use leptos_typed_fallback_show::TypedFallbackShow;

/// A generic Primitive component. Renders `element()` by default, or its
/// children directly if `as_child` is `true`. We rely on `TypedChildrenFn`
/// so that attributes can pass through at runtime—critical in Leptos v0.7
/// because `Children`-based types block such passthrough.
#[component]
#[allow(non_snake_case)]
pub fn Primitive<E, C>(
element: fn() -> HtmlElement<E, (), ()>,
children: TypedChildrenFn<C>,
#[prop(optional, into)] as_child: MaybeProp<bool>,
#[prop(optional, into)] node_ref: AnyNodeRef,
) -> impl IntoView
where
E: ElementType + 'static,
C: IntoView + 'static,
{
let children = StoredValue::new(children.into_inner());
view! {
<TypedFallbackShow
when=move || as_child.get().unwrap_or_default()
fallback=move || {
element()
.child(children.with_value(|c| c()))
.add_any_attr(leptos_node_ref::any_node_ref(node_ref))
}
>
{children.with_value(|c| c())
.add_any_attr(leptos_node_ref::any_node_ref(node_ref))}
</TypedFallbackShow>
}
}

/// Same idea, but for elements that do not take children (e.g. `img`, `input`).
#[component]
#[allow(non_snake_case)]
pub fn VoidPrimitive<E, C>(
element: fn() -> HtmlElement<E, (), ()>,
children: TypedChildrenFn<C>,
#[prop(optional, into)] as_child: MaybeProp<bool>,
#[prop(optional, into)] node_ref: AnyNodeRef,
) -> impl IntoView
where
E: ElementType + 'static,
{
let children = StoredValue::new(children.into_inner());
view! {
<TypedFallbackShow
when=move || as_child.get().unwrap_or_default()
fallback=move || {
element().add_any_attr(leptos_node_ref::any_node_ref(node_ref))
}
>
{children.with_value(|c| c())
.add_any_attr(leptos_node_ref::any_node_ref(node_ref))}
</TypedFallbackShow>
}
}

// (Compose callbacks is an internal piece from Radix Core; omitted for brevity.)
```

## Notes

- **Why `TypedChildrenFn`?**: Leptos attribute passthrough only works if a component doesn't rely on `AnyView` or `Children`. Using typed children ensures classes, events, etc. from the parent can flow to the rendered DOM node.
- **`as_child`**: Mimics `asChild` in Radix’s React version, but we skip an explicit `<Slot>`: Leptos’s approach to typed fallback rendering covers “slot-like” logic.
- **Class Handling**: Static classes from a parent can overwrite child-defined classes. No built-in merging exists.
- **Attribute System Limitations**: Leptos limits you to 26 dynamic attributes. Past that, nest components or try a custom approach.
- **Parity with React**: In React, `...props` merges everything automatically. In Leptos, we rely on typed props/attributes and can intercept unknown ones with `AttributeInterceptor`.

## Documentation

See [the Rust Radix book](https://radix.rustforweb.org/) for documentation.

## Rust For Web

The Rust Radix project is part of the [Rust For Web](https://github.com/RustForWeb).

[Rust For Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source.
9 changes: 9 additions & 0 deletions packages/primitives/leptos/primitive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Leptos port of [Radix Primitive](https://www.radix-ui.com/primitives).
//!
//! This is an internal utility, not intended for public usage.

//! See [`@radix-ui/react-primitive`](https://www.npmjs.com/package/@radix-ui/react-primitive) for the original package.

mod primitive;

pub use primitive::*;
109 changes: 109 additions & 0 deletions packages/primitives/leptos/primitive/src/primitive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use leptos::{
attr::Attribute,
ev::Event,
html::{ElementType, HtmlElement},
prelude::*,
wasm_bindgen::JsCast,
tachys::html::{class::IntoClass, node_ref::NodeRefContainer, style::IntoStyle},
};
use leptos_node_ref::{any_node_ref, AnyNodeRef};
use leptos_typed_fallback_show::TypedFallbackShow;

/* -------------------------------------------------------------------------------------------------
* Primitive
* -----------------------------------------------------------------------------------------------*/

#[component]
#[allow(non_snake_case)]
pub fn Primitive<E, C>(
element: fn() -> HtmlElement<E, (), ()>,
children: TypedChildrenFn<C>,
#[prop(optional, into)] as_child: MaybeProp<bool>,
#[prop(optional, into)] node_ref: AnyNodeRef,
) -> impl IntoView
where
E: ElementType + 'static,
C: IntoView + 'static,
View<C>: RenderHtml,
HtmlElement<E, (), ()>: ElementChild<View<C>>,
<HtmlElement<E, (), ()> as ElementChild<View<C>>>::Output: IntoView,
<E as ElementType>::Output: JsCast,
AnyNodeRef: NodeRefContainer<E>,
{
let children = StoredValue::new(children.into_inner());

view! {
<TypedFallbackShow
when=move || as_child.get().unwrap_or_default()
fallback=move || {
element().child(children.with_value(|children| children())).add_any_attr(any_node_ref(node_ref))
}
>
{children.with_value(|children| children()).add_any_attr(any_node_ref(node_ref))}
</TypedFallbackShow>
}
}

#[component]
#[allow(non_snake_case)]
pub fn VoidPrimitive<E, C>(
element: fn() -> HtmlElement<E, (), ()>,
children: TypedChildrenFn<C>,
#[prop(into, optional)] as_child: MaybeProp<bool>,
#[prop(into, optional)] node_ref: AnyNodeRef,
) -> impl IntoView
where
E: ElementType + 'static,
C: IntoView + 'static,
View<C>: RenderHtml,
<E as ElementType>::Output: JsCast,
AnyNodeRef: NodeRefContainer<E>,
{
let children = StoredValue::new(children.into_inner());
view! {
<TypedFallbackShow
when=move || as_child.get().unwrap_or_default()
fallback=move || { element().add_any_attr(any_node_ref(node_ref)) }
>
{children.with_value(|children| children()).add_any_attr(any_node_ref(node_ref))}
</TypedFallbackShow>
}
}

/* -------------------------------------------------------------------------------------------------
* Utils
* -----------------------------------------------------------------------------------------------*/

pub fn compose_callbacks<E>(
original_handler: Option<Callback<E>>,
our_handler: Option<Callback<E>>,
check_default_prevented: Option<bool>,
) -> impl Fn(E)
where
E: Clone + Into<Event> + 'static,
{
let check_default_prevented = check_default_prevented.unwrap_or(true);

move |event: E| {
// Run original handler first, matching TypeScript behavior
if let Some(original) = &original_handler {
original.run(event.clone());
}

// Only run our handler if default wasn't prevented (when checking is enabled)
if !check_default_prevented || !event.clone().into().default_prevented() {
if let Some(our) = &our_handler {
our.run(event);
}
}
}
}

/* -------------------------------------------------------------------------------------------------
* Primitive re-exports
* -----------------------------------------------------------------------------------------------*/

pub mod primitive {
pub use super::*;
pub use Primitive as Root;
}