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.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);
|
||||
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
|
||||
self.pop_scope();
|
||||
|
|
@ -1019,7 +1019,12 @@ impl JsEmitter {
|
|||
format!("{name}")
|
||||
}
|
||||
Expr::Ident(name) => {
|
||||
format!("{name}.value")
|
||||
if self.is_local_var(name) {
|
||||
// Local variables (loop vars, function params) — no .value
|
||||
format!("{name}")
|
||||
} else {
|
||||
format!("{name}.value")
|
||||
}
|
||||
}
|
||||
Expr::DotAccess(base, field) => {
|
||||
let base_js = self.emit_expr(base);
|
||||
|
|
@ -1223,6 +1228,11 @@ impl JsEmitter {
|
|||
.collect();
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
let stmts: Vec<String> = exprs.iter().map(|e| self.emit_event_handler_expr(e)).collect();
|
||||
stmts.join("; ")
|
||||
|
|
|
|||
|
|
@ -304,6 +304,8 @@ pub enum Expr {
|
|||
},
|
||||
/// Index access: `grid[i]`, `pads[8 + i]`
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -802,7 +802,13 @@ impl Parser {
|
|||
TokenKind::Dot => {
|
||||
self.advance();
|
||||
let field = self.expect_ident()?;
|
||||
expr = Expr::DotAccess(Box::new(expr), field);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
TokenKind::LBracket => {
|
||||
self.advance(); // consume [
|
||||
|
|
|
|||
|
|
@ -699,6 +699,14 @@ impl TypeChecker {
|
|||
}
|
||||
|
||||
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
|
||||
-- Full todo app demonstrating lists, input bindings, filtering,
|
||||
-- and dynamic DOM manipulation — all with compile-time reactivity.
|
||||
-- Dynamic list with add, remove, and reactive count
|
||||
|
||||
let todos = []
|
||||
let filter = "all"
|
||||
let next_id = 0
|
||||
let input_text = ""
|
||||
import { Card } from "../registry/components/card"
|
||||
import { Badge } from "../registry/components/badge"
|
||||
|
||||
-- Derived: filtered list
|
||||
let visible_todos = todos
|
||||
let active_count = 0
|
||||
let completed_count = 0
|
||||
let todos = ["Learn DreamStack", "Build something amazing", "Ship it"]
|
||||
let newTodo = ""
|
||||
let completedCount = 0
|
||||
|
||||
view app =
|
||||
column [
|
||||
text "todos" { class: "title" }
|
||||
view main = column [
|
||||
text "📝 DreamStack Todos" { variant: "title" }
|
||||
text "Add, remove, and manage your tasks" { variant: "subtitle" }
|
||||
|
||||
-- Add new todo
|
||||
Card { title: "New Todo", subtitle: "type and press Add" } [
|
||||
row [
|
||||
input "" { placeholder: "What needs to be done?", value: input_text, class: "new-todo", keydown: input_text += "" }
|
||||
]
|
||||
|
||||
column [
|
||||
text visible_todos { class: "todo-list" }
|
||||
]
|
||||
|
||||
row [
|
||||
text active_count { class: "count" }
|
||||
row [
|
||||
button "All" { click: filter = "all", class: "filter-btn" }
|
||||
button "Active" { click: filter = "active", class: "filter-btn" }
|
||||
button "Done" { click: filter = "completed", class: "filter-btn" }
|
||||
]
|
||||
input { bind: newTodo, placeholder: "What needs to be done?" }
|
||||
button "Add" { click: todos.push(newTodo), variant: "primary" }
|
||||
]
|
||||
]
|
||||
|
||||
-- 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 [
|
||||
Badge { label: "ITEMS", variant: "info" }
|
||||
button "Clear All" { click: todos = [], variant: "ghost" }
|
||||
]
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue