use beet_core::prelude::*;
use beet_dom::prelude::*;
pub fn apply_slots(
mut commands: Commands,
roots: Populated<Entity, Added<BeetRoot>>,
) -> Result {
for root in roots.iter() {
commands.run_system_cached_with(apply_slots_recursive, root);
}
Ok(())
}
pub fn apply_slots_recursive(
In(instance_root): In<Entity>,
mut commands: Commands,
children: Query<&Children>,
template_roots: Query<(Entity, &TemplateRoot, Option<&NodeTag>)>,
slot_targets: Query<(Entity, &SlotTarget)>,
slot_children: Query<(Entity, &SlotChild)>,
) -> Result {
for (node_entity, root, node_tag) in
children
.iter_descendants_inclusive(instance_root)
.filter_map(|entity| template_roots.get(entity).ok())
{
let node_tag = node_tag.map(|tag| tag.as_str()).unwrap_or("Unnamed");
let (named_slots, default_slots) =
collect_slot_children(node_entity, &children, &slot_children);
let slot_targets = children
.iter_descendants(**root)
.filter_map(|c| slot_targets.get(c).ok())
.collect::<Vec<_>>();
let default_slot_target = slot_targets
.iter()
.find(|(_, target)| **target == SlotTarget::Default);
let mut used_targets = HashSet::<Entity>::default();
for (named_slot_ent, named_slot) in named_slots.iter() {
let Some((target, _)) = slot_targets
.iter()
.find(|(_, target)| target.name() == Some(named_slot.as_str()))
else {
bevybail!(
"Attempted to add a child to {node_tag} which has no '{named_slot}' slot target,
consider adding a <slot name=\"{named_slot}\"/> to {node_tag}.
available named targets: {}",
slot_targets.iter().filter_map(|(_,target)|target.name().clone())
.collect::<Vec<_>>().join(", ")
);
};
used_targets.insert(*target);
commands.entity(*named_slot_ent).insert(ChildOf(*target));
}
if !default_slots.is_empty() {
let Some((target, _)) = default_slot_target else {
bevybail!(
"Attempted to add a child to {node_tag} which has no default slot target,
consider adding a <slot/> to {node_tag}."
);
};
used_targets.insert(*target);
for slot in default_slots {
commands.entity(slot).insert(ChildOf(*target));
}
}
used_targets
.into_iter()
.filter_map(|e| children.get(e).ok())
.for_each(|children| {
for child in children.iter() {
commands.entity(child).despawn();
}
});
commands
.entity(**root)
.remove::<TemplateOf>()
.insert(ChildOf(node_entity));
commands.run_system_cached_with(apply_slots_recursive, **root);
}
Ok(())
}
fn collect_slot_children(
node_ent: Entity,
children: &Query<&Children>,
slot_children: &Query<(Entity, &SlotChild)>,
) -> (Vec<(Entity, String)>, Vec<Entity>) {
let named_slots = children
.iter_direct_descendants(node_ent)
.filter_map(|c| slot_children.get(c).ok())
.collect::<Vec<_>>();
let mut default_slots = children
.get(node_ent)
.map(|children| {
children
.iter()
.filter(|e| !named_slots.iter().any(|s| s.0 == *e))
.collect::<Vec<_>>()
})
.unwrap_or_default();
let named_slots = named_slots
.into_iter()
.filter_map(|(entity, slot)| {
if let SlotChild::Named(name) = slot {
Some((entity, name.to_string()))
} else {
default_slots.push(entity);
None
}
})
.collect::<Vec<_>>();
(named_slots, default_slots)
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
use beet_dom::prelude::*;
#[template]
fn Span() -> impl Bundle {
rsx! {
<span>
<slot />
</span>
}
}
#[template]
fn MyComponent() -> impl Bundle {
rsx! {
<html>
<slot name="header">Fallback Title</slot>
<br />
<slot />
</html>
}
}
#[test]
fn works() {
rsx! {
<MyComponent>
<div>Default</div>
<div slot="header">Title</div>
</MyComponent>
}
.xmap(HtmlFragment::parse_bundle)
.xpect_eq("<html><div>Title</div><br/><div>Default</div></html>");
}
#[test]
fn component_slots() {
rsx! {
<MyComponent>
<div>Default</div>
<Span slot="header">Title</Span>
</MyComponent>
}
.xmap(HtmlFragment::parse_bundle)
.xpect_eq("<html><span>Title</span><br/><div>Default</div></html>");
}
#[test]
fn fallback() {
rsx! { <MyComponent /> }
.xmap(HtmlFragment::parse_bundle)
.xpect_eq("<html>Fallback Title<br/></html>");
}
#[test]
fn recursive() {
rsx! {
<Span>
<MyComponent>
<div>Default</div>
<div slot="header">Title</div>
</MyComponent>
</Span>
}
.xmap(HtmlFragment::parse_bundle)
.xpect_eq(
"<span><html><div>Title</div><br/><div>Default</div></html></span>",
);
}
#[test]
fn transfer_simple() {
#[template]
fn Layout() -> impl Bundle {
rsx! {
<Header>
<slot name="header" slot="default" />
</Header>
}
}
#[template]
fn Header() -> impl Bundle {
rsx! {
<header>
<slot />
</header>
}
}
rsx! {
<Layout>
<h1 slot="header">"Title"</h1>
</Layout>
}
.xmap(HtmlFragment::parse_bundle)
.xpect_eq("<header><h1>Title</h1></header>");
}
#[test]
fn transfer_complex() {
#[template]
fn Layout() -> impl Bundle {
rsx! {
<body>
<Middle>
<slot name="header" slot="header" />
</Middle>
<main>
<slot />
</main>
</body>
}
}
#[template]
fn Middle() -> impl Bundle {
rsx! {
<Header>
<slot name="header" slot="default" />
</Header>
}
}
#[template]
fn Header() -> impl Bundle {
rsx! {
<header>
<slot />
</header>
}
}
rsx! {
<Layout>
<div>"Content"</div>
<h1 slot="header">"Title"</h1>
</Layout>
}
.xmap(HtmlFragment::parse_bundle)
.xpect_eq("<body><header><h1>Title</h1></header><main><div>Content</div></main></body>");
}
}