feat: slot/children composition for components
- AST: added Expr::Slot variant
- Parser: 'slot' keyword renders children, [...] bracket children after ComponentUse
- Codegen: DS_{name}(props, __children) factory pattern
- Type checker: Slot => Type::View
- Updated Card component with slot for children
- Added examples/slot-demo.ds
This commit is contained in:
parent
bb65e10f5c
commit
76bb1bb3a2
6 changed files with 79 additions and 6 deletions
|
|
@ -464,7 +464,7 @@ impl JsEmitter {
|
|||
}
|
||||
|
||||
fn emit_component_decl(&mut self, comp: &ComponentDecl, graph: &SignalGraph) {
|
||||
self.emit_line(&format!("function DS_{}(props) {{", comp.name));
|
||||
self.emit_line(&format!("function DS_{}(props, __children) {{", comp.name));
|
||||
self.indent += 1;
|
||||
// Destructure props into local signal-compatible variables
|
||||
// Props may be raw values or signals — wrap in a signal-like accessor
|
||||
|
|
@ -891,13 +891,30 @@ impl JsEmitter {
|
|||
container_var
|
||||
}
|
||||
|
||||
// Component usage: `Button { label: "hello" }`
|
||||
Expr::ComponentUse { name, props, children: _ } => {
|
||||
// Component usage: `Button { label: "hello" }` or `Card { title: "x" } [ children ]`
|
||||
Expr::ComponentUse { name, props, children } => {
|
||||
let node_var = self.next_node_id();
|
||||
let props_js: Vec<String> = props.iter()
|
||||
.map(|(k, v)| format!("{}: {}", k, self.emit_expr(v)))
|
||||
.collect();
|
||||
self.emit_line(&format!("const {} = DS_{}({{ {} }});", node_var, name, props_js.join(", ")));
|
||||
if children.is_empty() {
|
||||
self.emit_line(&format!("const {} = DS_{}({{ {} }});", node_var, name, props_js.join(", ")));
|
||||
} else {
|
||||
// Build children factory function
|
||||
let children_fn = self.next_node_id();
|
||||
self.emit_line(&format!("function {}() {{", children_fn));
|
||||
self.indent += 1;
|
||||
let container = self.next_node_id();
|
||||
self.emit_line(&format!("const {} = document.createDocumentFragment();", container));
|
||||
for child in children {
|
||||
let child_var = self.emit_view_expr(child, graph);
|
||||
self.emit_line(&format!("{}.appendChild({});", container, child_var));
|
||||
}
|
||||
self.emit_line(&format!("return {};", container));
|
||||
self.indent -= 1;
|
||||
self.emit_line("}");
|
||||
self.emit_line(&format!("const {} = DS_{}({{ {} }}, {});", node_var, name, props_js.join(", "), children_fn));
|
||||
}
|
||||
node_var
|
||||
}
|
||||
|
||||
|
|
@ -929,6 +946,16 @@ impl JsEmitter {
|
|||
}
|
||||
|
||||
|
||||
// Slot: render children passed to this component
|
||||
Expr::Slot => {
|
||||
let node_var = self.next_node_id();
|
||||
self.emit_line(&format!(
|
||||
"const {} = __children ? __children() : document.createComment('empty-slot');",
|
||||
node_var
|
||||
));
|
||||
node_var
|
||||
}
|
||||
|
||||
// Fallback: just create a text node
|
||||
_ => {
|
||||
let node_var = self.next_node_id();
|
||||
|
|
|
|||
|
|
@ -304,6 +304,8 @@ pub enum Expr {
|
|||
},
|
||||
/// Index access: `grid[i]`, `pads[8 + i]`
|
||||
Index(Box<Expr>, Box<Expr>),
|
||||
/// Slot: renders children passed to a component
|
||||
Slot,
|
||||
}
|
||||
|
||||
/// String literal with interpolation segments.
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,11 @@ impl Parser {
|
|||
let name = name.clone();
|
||||
self.advance();
|
||||
|
||||
// Slot keyword: renders children inside a component
|
||||
if name == "slot" {
|
||||
return Ok(Expr::Slot);
|
||||
}
|
||||
|
||||
// Component use: `Button { label: "hello" }` — capitalized name + `{`
|
||||
if name.chars().next().map_or(false, |c| c.is_uppercase())
|
||||
&& self.check(&TokenKind::LBrace)
|
||||
|
|
@ -1197,6 +1202,18 @@ impl Parser {
|
|||
self.skip_newlines();
|
||||
}
|
||||
self.expect(&TokenKind::RBrace)?;
|
||||
// Check for [...] children block after props
|
||||
self.skip_newlines();
|
||||
if self.check(&TokenKind::LBracket) {
|
||||
self.advance(); // consume [
|
||||
self.skip_newlines();
|
||||
while !self.check(&TokenKind::RBracket) && !self.is_at_end() {
|
||||
self.skip_newlines();
|
||||
children.push(self.parse_expr()?);
|
||||
self.skip_newlines();
|
||||
}
|
||||
self.expect(&TokenKind::RBracket)?;
|
||||
}
|
||||
return Ok(Expr::ComponentUse { name, props, children });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -697,6 +697,8 @@ impl TypeChecker {
|
|||
_ => self.fresh_tv(),
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Slot => Type::View,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
24
examples/slot-demo.ds
Normal file
24
examples/slot-demo.ds
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
-- DreamStack Slot Composition Demo
|
||||
-- Demonstrates component children/slot rendering
|
||||
|
||||
import { Card } from "../registry/components/card"
|
||||
import { Button } from "../registry/components/button"
|
||||
import { Badge } from "../registry/components/badge"
|
||||
|
||||
let count = 0
|
||||
|
||||
view main = column [
|
||||
text "🧩 Slot Composition" { variant: "title" }
|
||||
text "Components can now accept children!" { variant: "subtitle" }
|
||||
|
||||
Card { title: "Counter Card", subtitle: "with interactive children" } [
|
||||
text "Count: {count}"
|
||||
button "+1" { click: count += 1, variant: "primary" }
|
||||
button "-1" { click: count -= 1, variant: "secondary" }
|
||||
]
|
||||
|
||||
Card { title: "Nested Components", subtitle: "badge inside card" } [
|
||||
Badge { label: "nested", variant: "success" }
|
||||
text "Badge rendered inside Card slot"
|
||||
]
|
||||
]
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
-- DreamStack Card Component
|
||||
-- Glassmorphism container with title and subtitle
|
||||
-- DreamStack Card Component with Slot
|
||||
-- Glassmorphism container with title, subtitle, and slot for children
|
||||
|
||||
export component Card(title, subtitle) =
|
||||
column [
|
||||
text title { variant: "title" }
|
||||
text subtitle { variant: "subtitle" }
|
||||
slot
|
||||
] { variant: "card" }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue