Iterable Children
Specific Demand
Iterating through the Children of a component is very complex currently, for instance:
#![allow(non_snake_case)]
use dioxus::prelude::*;
#[derive(Props)]
pub struct BreadcrumbsProps<'a> {
divider: Option<&'a str>,
children: Vec<Element<'a>>
}
pub fn Breadcrumbs<'a>(cx: Scope<'a, BreadcrumbsProps<'a>>) -> Element {
cx.render(rsx!(
nav {
role: "navigation",
for child in &cx.props.children {
span {
p { cx.props.divider.unwrap_or("->") }
child
}
}
}
))
}
#[derive(PartialEq, Props)]
pub struct BreadcrumbProps<'a> {
name: &'a str,
link: &'a str
}
pub fn Breadcrumb<'a>(cx: Scope<'a, BreadcrumbProps<'a>>) -> Element {
cx.render(rsx!(
a {
href: cx.props.link,
cx.props.name
}
))
}
So this should be consumable like so:
Breadcrumbs {
divider: "/",
Breadcrumb {
name: "Level 1",
link: "/"
}
Breadcrumb {
name: "Level 2",
link: "/level2"
}
}
Additional context: https://discord.com/channels/899851952891002890/1127735881076330548
Implement Suggestion
The syntax for such implementation is not very straight forward, making the DX very poor implementing components like this. Either this should be made easier, or a better approach should be documented on how to best tackle this issue with code examples.
In the short term, using a Vec of Elements as children can work, it just makes using the component less clean:
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app)
}
fn app(cx: Scope) -> Element {
render! {
Breadcrumbs {
divider: "/",
child_routes: vec![
render!{
Breadcrumb {
name: "Level 1",
link: "/"
}
},
render!{
Breadcrumb {
name: "Level 2",
link: "/level2"
}
}
]
}
}
}
#[derive(Props)]
pub struct BreadcrumbsProps<'a> {
divider: Option<&'a str>,
child_routes: Option<Vec<Element<'a>>>,
}
pub fn Breadcrumbs<'a>(cx: Scope<'a, BreadcrumbsProps<'a>>) -> Element {
cx.render(rsx!(
nav {
role: "navigation",
for child in cx.props.child_routes.iter().flatten() {
span {
p { cx.props.divider.unwrap_or("->") }
child
}
}
}
))
}
#[derive(PartialEq, Props)]
pub struct BreadcrumbProps<'a> {
name: &'a str,
link: &'a str,
}
pub fn Breadcrumb<'a>(cx: Scope<'a, BreadcrumbProps<'a>>) -> Element {
cx.render(rsx!(
a {
href: cx.props.link,
cx.props.name
}
))
}
In the future, for the specific case of a sitemap, this should become easier with #1020. The enum based router exposes a sitemap which should make it possible to implement a link tree that is generic over the router instead of hardcoded with components
In the longer term, a way to traverse through a single template could be beneficial. A single Element node can contain many different nodes, so it will never have a simple children attribute
For example this is one Element struct even though it includes several elements in the rsx:
div {
img {
src: "",
}
div {
p { "Hello world" }
}
}
We could implement a cursor to help users traverse through the element structure. It could look something like this:
use dioxus::{
core::{DynamicNode, IntoDynNode},
prelude::{SvgAttributes, *},
};
#[derive(Clone)]
struct NodeCursor<'a> {
// The position in the inner node, represented as a list of child offsets
// more info https://dioxuslabs.com/docs/nightly/guide/en/contributing/walkthrough_readme.html#the-rsx-macro
position: Vec<u8>,
// The inner node we are moving in. This is a block of static nodes with references to any dynamic children
inner: VNode<'a>,
}
impl<'a> NodeCursor<'a> {
fn current_node(&self) -> TemplateNode {
let mut child_index_iter = self.position.iter().copied();
let mut current =
self.inner.template.get().roots[child_index_iter.next().unwrap() as usize];
for child_index in child_index_iter {
match current {
TemplateNode::Element {
tag,
namespace,
attrs,
children,
} => {
current = children[child_index as usize];
}
_ => unreachable!(),
}
}
current
}
// Node cursor would have a set of getters to get data about the underlying node
fn current_node_text(&self) -> Option<&str> {
match self.current_node() {
TemplateNode::Element {
tag,
namespace,
attrs,
children,
} => None,
TemplateNode::Text { text } => Some(text),
TemplateNode::Dynamic { id } => None,
TemplateNode::DynamicText { id } => {
let node = &self.inner.dynamic_nodes[id];
match node {
dioxus::core::DynamicNode::Text(text) => Some(text.value),
_ => None,
}
}
}
}
fn first_child(&self) -> Option<NodeCursor<'a>> {
match self.current_node() {
TemplateNode::Element {
tag,
namespace,
attrs,
children,
} => Some(NodeCursor {
inner: self.inner.clone(),
position: {
let mut new_pos = self.position.clone();
new_pos.push(0);
new_pos
},
}),
TemplateNode::Dynamic { id } => {
let dyn_node = &self.inner.dynamic_nodes[id];
match dyn_node {
// We cannot easily traverse into Components
dioxus::core::DynamicNode::Component(_) => None,
dioxus::core::DynamicNode::Fragment(children) => {
children.get(0).map(|child| NodeCursor {
inner: child.clone(),
position: vec![0],
})
}
// Children and Placeholders do not have children
dioxus::core::DynamicNode::Placeholder(_) => None,
dioxus::core::DynamicNode::Text(_) => None,
}
}
// Text does not have children
TemplateNode::Text { text } => None,
TemplateNode::DynamicText { id } => None,
}
}
fn next_sibling(&self) -> NodeCursor<'a> {
todo!()
}
}
// This allows NodeCursor to be used in rsx
impl<'a> IntoDynNode<'a> for NodeCursor<'a> {
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> {
todo!()
}
}
This was crazy hard to discover, so FYI here's what I used to feed my nav tree:
impl Route {
pub(crate) fn ancestry(&self) -> Vec<Route> {
let mut ancestry: Vec<_> =
std::iter::successors(Some(self.clone()), Routable::parent).collect();
ancestry.reverse();
ancestry
}
pub(crate) fn siblings(&self) -> Vec<Route> {
let mut siblings: Vec<_> = Route::static_routes()
.into_iter()
.filter(|r| r.parent() == self.parent())
.collect();
if !siblings.contains(self) {
siblings.push(self.clone());
}
siblings
}
pub(crate) fn children(&self) -> impl Iterator<Item = Route> {
Route::static_routes()
.into_iter()
.filter(|r| r.parent().is_some_and(|p| p == *self))
}
pub(crate) fn title(&self) -> Element {
match self {
Route::FrontPage => rsx! {
Icon {
name: "home",
}
},
// ...and so on
Route::PageNotFound { segments: _ } => rsx! { "Page not found" },
}
}
pub(crate) fn display_segment(&self) -> Element {
rsx! {
Link {
to: self.clone(),
{self.title()}
}
}
}
}
impl Route {...}
This issue is tracking iterating over the VNodes directly which is difficult because of the template structure. Iterating over routes is significantly easier. We could add those methods to the routable trait alongside the existing parent method
Oh I see, my bad! I'm hiding my comments.