DioxusLabs / dioxus

Fullstack app framework for web, desktop, mobile, and more.
https://dioxuslabs.com
Apache License 2.0
20.14k stars 771 forks source link

Iterable Children #1177

Open itsezc opened 1 year ago

itsezc commented 1 year ago

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.

ealmloff commented 1 year ago

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 commented 1 year ago

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