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) {
|
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;
|
self.indent += 1;
|
||||||
// Destructure props into local signal-compatible variables
|
// Destructure props into local signal-compatible variables
|
||||||
// Props may be raw values or signals — wrap in a signal-like accessor
|
// Props may be raw values or signals — wrap in a signal-like accessor
|
||||||
|
|
@ -891,13 +891,30 @@ impl JsEmitter {
|
||||||
container_var
|
container_var
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component usage: `Button { label: "hello" }`
|
// Component usage: `Button { label: "hello" }` or `Card { title: "x" } [ children ]`
|
||||||
Expr::ComponentUse { name, props, children: _ } => {
|
Expr::ComponentUse { name, props, children } => {
|
||||||
let node_var = self.next_node_id();
|
let node_var = self.next_node_id();
|
||||||
let props_js: Vec<String> = props.iter()
|
let props_js: Vec<String> = props.iter()
|
||||||
.map(|(k, v)| format!("{}: {}", k, self.emit_expr(v)))
|
.map(|(k, v)| format!("{}: {}", k, self.emit_expr(v)))
|
||||||
.collect();
|
.collect();
|
||||||
|
if children.is_empty() {
|
||||||
self.emit_line(&format!("const {} = DS_{}({{ {} }});", node_var, name, props_js.join(", ")));
|
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
|
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
|
// Fallback: just create a text node
|
||||||
_ => {
|
_ => {
|
||||||
let node_var = self.next_node_id();
|
let node_var = self.next_node_id();
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,8 @@ pub enum Expr {
|
||||||
},
|
},
|
||||||
/// Index access: `grid[i]`, `pads[8 + i]`
|
/// Index access: `grid[i]`, `pads[8 + i]`
|
||||||
Index(Box<Expr>, Box<Expr>),
|
Index(Box<Expr>, Box<Expr>),
|
||||||
|
/// Slot: renders children passed to a component
|
||||||
|
Slot,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// String literal with interpolation segments.
|
/// String literal with interpolation segments.
|
||||||
|
|
|
||||||
|
|
@ -1169,6 +1169,11 @@ impl Parser {
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
|
// Slot keyword: renders children inside a component
|
||||||
|
if name == "slot" {
|
||||||
|
return Ok(Expr::Slot);
|
||||||
|
}
|
||||||
|
|
||||||
// Component use: `Button { label: "hello" }` — capitalized name + `{`
|
// Component use: `Button { label: "hello" }` — capitalized name + `{`
|
||||||
if name.chars().next().map_or(false, |c| c.is_uppercase())
|
if name.chars().next().map_or(false, |c| c.is_uppercase())
|
||||||
&& self.check(&TokenKind::LBrace)
|
&& self.check(&TokenKind::LBrace)
|
||||||
|
|
@ -1197,6 +1202,18 @@ impl Parser {
|
||||||
self.skip_newlines();
|
self.skip_newlines();
|
||||||
}
|
}
|
||||||
self.expect(&TokenKind::RBrace)?;
|
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 });
|
return Ok(Expr::ComponentUse { name, props, children });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -697,6 +697,8 @@ impl TypeChecker {
|
||||||
_ => self.fresh_tv(),
|
_ => 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
|
-- DreamStack Card Component with Slot
|
||||||
-- Glassmorphism container with title and subtitle
|
-- Glassmorphism container with title, subtitle, and slot for children
|
||||||
|
|
||||||
export component Card(title, subtitle) =
|
export component Card(title, subtitle) =
|
||||||
column [
|
column [
|
||||||
text title { variant: "title" }
|
text title { variant: "title" }
|
||||||
text subtitle { variant: "subtitle" }
|
text subtitle { variant: "subtitle" }
|
||||||
|
slot
|
||||||
] { variant: "card" }
|
] { variant: "card" }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue