feat: dynamic lists (push/remove/pop) + TodoMVC demo
- MethodCall AST node: obj.method(args) parsing - Array push: items.push(x) → immutable spread+append - Array remove: items.remove(idx) → filter by index - Array pop: items.pop() → slice(0, -1) - Fix: loop vars (todo, _idx) emitted without .value via is_local_var() - Fix: _idx added to each loop scope for index-based event handlers - New: examples/todomvc.ds — add, remove, clear all, fully reactive
This commit is contained in:
parent
a7af39e900
commit
cbd6dfc7a6
5 changed files with 97 additions and 30 deletions
|
|
@ -844,7 +844,7 @@ impl JsEmitter {
|
||||||
));
|
));
|
||||||
self.emit_line(&format!("__list.forEach(({item_name}, _idx) => {{"));
|
self.emit_line(&format!("__list.forEach(({item_name}, _idx) => {{"));
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
self.push_scope(&[item_name.as_str()]);
|
self.push_scope(&[item_name.as_str(), "_idx"]);
|
||||||
let child_var = self.emit_view_expr(body, graph);
|
let child_var = self.emit_view_expr(body, graph);
|
||||||
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
|
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
|
||||||
self.pop_scope();
|
self.pop_scope();
|
||||||
|
|
@ -1019,8 +1019,13 @@ impl JsEmitter {
|
||||||
format!("{name}")
|
format!("{name}")
|
||||||
}
|
}
|
||||||
Expr::Ident(name) => {
|
Expr::Ident(name) => {
|
||||||
|
if self.is_local_var(name) {
|
||||||
|
// Local variables (loop vars, function params) — no .value
|
||||||
|
format!("{name}")
|
||||||
|
} else {
|
||||||
format!("{name}.value")
|
format!("{name}.value")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Expr::DotAccess(base, field) => {
|
Expr::DotAccess(base, field) => {
|
||||||
let base_js = self.emit_expr(base);
|
let base_js = self.emit_expr(base);
|
||||||
format!("{base_js}.{field}")
|
format!("{base_js}.{field}")
|
||||||
|
|
@ -1223,6 +1228,11 @@ impl JsEmitter {
|
||||||
.collect();
|
.collect();
|
||||||
format!("DS_{}({{ {} }})", name, props_js.join(", "))
|
format!("DS_{}({{ {} }})", name, props_js.join(", "))
|
||||||
}
|
}
|
||||||
|
Expr::MethodCall(obj, method, args) => {
|
||||||
|
let obj_js = self.emit_expr(obj);
|
||||||
|
let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();
|
||||||
|
format!("{}.{}({})", obj_js, method, args_js.join(", "))
|
||||||
|
}
|
||||||
_ => "null".to_string(),
|
_ => "null".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1292,6 +1302,44 @@ impl JsEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Method calls on arrays: items.push(x), items.remove(idx), items.filter(fn)
|
||||||
|
Expr::MethodCall(obj, method, args) => {
|
||||||
|
let obj_js = self.emit_expr(obj);
|
||||||
|
let signal_name = match obj.as_ref() {
|
||||||
|
Expr::Ident(name) => name.clone(),
|
||||||
|
_ => obj_js.clone(),
|
||||||
|
};
|
||||||
|
let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();
|
||||||
|
match method.as_str() {
|
||||||
|
"push" => {
|
||||||
|
// items.push(x) → items.value = [...items.value, x]
|
||||||
|
let val = args_js.first().map(|s| s.as_str()).unwrap_or("undefined");
|
||||||
|
format!(
|
||||||
|
"{sig}.value = [...{sig}.value, {val}]; DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||||
|
sig = signal_name, val = val
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"remove" => {
|
||||||
|
// items.remove(idx) → items.value = items.value.filter((_, i) => i !== idx)
|
||||||
|
let idx = args_js.first().map(|s| s.as_str()).unwrap_or("0");
|
||||||
|
format!(
|
||||||
|
"{sig}.value = {sig}.value.filter((_, _i) => _i !== {idx}); DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||||
|
sig = signal_name, idx = idx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"pop" => {
|
||||||
|
// items.pop() → items.value = items.value.slice(0, -1)
|
||||||
|
format!(
|
||||||
|
"{sig}.value = {sig}.value.slice(0, -1); DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||||
|
sig = signal_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Generic method call: obj.method(args)
|
||||||
|
format!("{}.{}({})", obj_js, method, args_js.join(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Expr::Block(exprs) => {
|
Expr::Block(exprs) => {
|
||||||
let stmts: Vec<String> = exprs.iter().map(|e| self.emit_event_handler_expr(e)).collect();
|
let stmts: Vec<String> = exprs.iter().map(|e| self.emit_event_handler_expr(e)).collect();
|
||||||
stmts.join("; ")
|
stmts.join("; ")
|
||||||
|
|
|
||||||
|
|
@ -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>),
|
||||||
|
/// Method call: `items.push(x)`, `items.filter(fn)`
|
||||||
|
MethodCall(Box<Expr>, String, Vec<Expr>),
|
||||||
/// Slot: renders children passed to a component
|
/// Slot: renders children passed to a component
|
||||||
Slot,
|
Slot,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -802,8 +802,14 @@ impl Parser {
|
||||||
TokenKind::Dot => {
|
TokenKind::Dot => {
|
||||||
self.advance();
|
self.advance();
|
||||||
let field = self.expect_ident()?;
|
let field = self.expect_ident()?;
|
||||||
|
// Check if this is a method call: obj.method(args)
|
||||||
|
if self.check(&TokenKind::LParen) {
|
||||||
|
let args = self.parse_call_args()?;
|
||||||
|
expr = Expr::MethodCall(Box::new(expr), field, args);
|
||||||
|
} else {
|
||||||
expr = Expr::DotAccess(Box::new(expr), field);
|
expr = Expr::DotAccess(Box::new(expr), field);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
TokenKind::LBracket => {
|
TokenKind::LBracket => {
|
||||||
self.advance(); // consume [
|
self.advance(); // consume [
|
||||||
let index = self.parse_expr()?;
|
let index = self.parse_expr()?;
|
||||||
|
|
|
||||||
|
|
@ -699,6 +699,14 @@ impl TypeChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::Slot => Type::View,
|
Expr::Slot => Type::View,
|
||||||
|
|
||||||
|
Expr::MethodCall(obj, _, args) => {
|
||||||
|
self.infer_expr(obj);
|
||||||
|
for arg in args {
|
||||||
|
self.infer_expr(arg);
|
||||||
|
}
|
||||||
|
self.fresh_tv()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,38 @@
|
||||||
-- DreamStack TodoMVC
|
-- DreamStack TodoMVC
|
||||||
-- Full todo app demonstrating lists, input bindings, filtering,
|
-- Dynamic list with add, remove, and reactive count
|
||||||
-- and dynamic DOM manipulation — all with compile-time reactivity.
|
|
||||||
|
|
||||||
let todos = []
|
import { Card } from "../registry/components/card"
|
||||||
let filter = "all"
|
import { Badge } from "../registry/components/badge"
|
||||||
let next_id = 0
|
|
||||||
let input_text = ""
|
|
||||||
|
|
||||||
-- Derived: filtered list
|
let todos = ["Learn DreamStack", "Build something amazing", "Ship it"]
|
||||||
let visible_todos = todos
|
let newTodo = ""
|
||||||
let active_count = 0
|
let completedCount = 0
|
||||||
let completed_count = 0
|
|
||||||
|
|
||||||
view app =
|
view main = column [
|
||||||
column [
|
text "📝 DreamStack Todos" { variant: "title" }
|
||||||
text "todos" { class: "title" }
|
text "Add, remove, and manage your tasks" { variant: "subtitle" }
|
||||||
|
|
||||||
|
-- Add new todo
|
||||||
|
Card { title: "New Todo", subtitle: "type and press Add" } [
|
||||||
row [
|
row [
|
||||||
input "" { placeholder: "What needs to be done?", value: input_text, class: "new-todo", keydown: input_text += "" }
|
input { bind: newTodo, placeholder: "What needs to be done?" }
|
||||||
|
button "Add" { click: todos.push(newTodo), variant: "primary" }
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
column [
|
-- Todo list
|
||||||
text visible_todos { class: "todo-list" }
|
Card { title: "Todo List", subtitle: "click × to remove" } [
|
||||||
|
each todo in todos ->
|
||||||
|
row [
|
||||||
|
text "•"
|
||||||
|
text todo
|
||||||
|
button "×" { click: todos.remove(_idx), variant: "ghost" }
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
-- Footer
|
||||||
row [
|
row [
|
||||||
text active_count { class: "count" }
|
Badge { label: "ITEMS", variant: "info" }
|
||||||
row [
|
button "Clear All" { click: todos = [], variant: "ghost" }
|
||||||
button "All" { click: filter = "all", class: "filter-btn" }
|
|
||||||
button "Active" { click: filter = "active", class: "filter-btn" }
|
|
||||||
button "Done" { click: filter = "completed", class: "filter-btn" }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue