Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}