dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Iterable Children

Open itsezc opened this issue 2 years ago • 5 comments

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.

itsezc avatar Jul 10 '23 07:07 itsezc

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

ealmloff avatar Jul 10 '23 14:07 ealmloff

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!()
    }
}

ealmloff avatar Jul 10 '23 14:07 ealmloff

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()}
            }
        }
    }
}

tv42 avatar Dec 01 '25 02:12 tv42

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

ealmloff avatar Dec 01 '25 14:12 ealmloff

Oh I see, my bad! I'm hiding my comments.

tv42 avatar Dec 02 '25 03:12 tv42