feat: two-way binding, form props, and async resources
Phase 8 features:
- bind prop: two-way signal <-> input sync
- placeholder, value, style, disabled props
- DS.resource() for reactive async data fetching
- DS.fetchJSON() convenience wrapper
- Props-only element parsing: input { bind: x }
- examples/form.ds: contact form with validation
Fixed: double .value.value bug in bind codegen.
This commit is contained in:
parent
33ed843abb
commit
a35d44bd59
3 changed files with 93 additions and 0 deletions
|
|
@ -334,10 +334,50 @@ impl JsEmitter {
|
|||
node_var, dom_event, handler_js
|
||||
));
|
||||
}
|
||||
"bind" => {
|
||||
// Two-way binding: signal <-> input
|
||||
// Use raw signal name (not emit_expr which adds .value)
|
||||
let signal_name = match val {
|
||||
Expr::Ident(name) => name.clone(),
|
||||
_ => self.emit_expr(val),
|
||||
};
|
||||
// Signal -> input
|
||||
self.emit_line(&format!(
|
||||
"DS.effect(() => {{ {}.value = {}.value; }});",
|
||||
node_var, signal_name
|
||||
));
|
||||
// Input -> signal
|
||||
self.emit_line(&format!(
|
||||
"{}.addEventListener('input', (e) => {{ {}.value = e.target.value; DS.flush(); }});",
|
||||
node_var, signal_name
|
||||
));
|
||||
}
|
||||
"class" => {
|
||||
let js = self.emit_expr(val);
|
||||
self.emit_line(&format!("{}.className += ' ' + {};", node_var, js));
|
||||
}
|
||||
"placeholder" => {
|
||||
let js = self.emit_expr(val);
|
||||
self.emit_line(&format!("{}.placeholder = {};", node_var, js));
|
||||
}
|
||||
"value" => {
|
||||
let js = self.emit_expr(val);
|
||||
self.emit_line(&format!(
|
||||
"DS.effect(() => {{ {}.value = {}.value; }});",
|
||||
node_var, js
|
||||
));
|
||||
}
|
||||
"style" => {
|
||||
let js = self.emit_expr(val);
|
||||
self.emit_line(&format!("{}.style.cssText = {};", node_var, js));
|
||||
}
|
||||
"disabled" => {
|
||||
let js = self.emit_expr(val);
|
||||
self.emit_line(&format!(
|
||||
"DS.effect(() => {{ {}.disabled = {}.value; }});",
|
||||
node_var, js
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
let js = self.emit_expr(val);
|
||||
self.emit_line(&format!("{}.setAttribute('{}', {});", node_var, key, js));
|
||||
|
|
@ -943,8 +983,25 @@ const DS = (() => {
|
|||
return params;
|
||||
}
|
||||
|
||||
// ── Async Resources ──
|
||||
function resource(fetcher) {
|
||||
const state = new Signal({ status: 'loading', data: null, error: null });
|
||||
const eff = effect(() => {
|
||||
state.value = { status: 'loading', data: state._value.data, error: null };
|
||||
Promise.resolve(fetcher())
|
||||
.then(data => { state.value = { status: 'ok', data, error: null }; })
|
||||
.catch(err => { state.value = { status: 'error', data: null, error: err.message || String(err) }; });
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
function fetchJSON(url) {
|
||||
return resource(() => fetch(url).then(r => r.json()));
|
||||
}
|
||||
|
||||
return { signal, derived, effect, batch, flush, onEvent, emit,
|
||||
keyedList, route: _route, navigate, matchRoute,
|
||||
resource, fetchJSON,
|
||||
Signal, Derived, Effect };
|
||||
})();
|
||||
"#;
|
||||
|
|
|
|||
|
|
@ -719,6 +719,14 @@ impl Parser {
|
|||
None => Ok(Expr::Ident(fallback)),
|
||||
}
|
||||
}
|
||||
// Element with props only: `input { bind: name }`
|
||||
else if is_ui_element(&name) && matches!(self.peek(), TokenKind::LBrace) {
|
||||
let fallback = name.clone();
|
||||
match self.parse_element(name)? {
|
||||
Some(el) => Ok(el),
|
||||
None => Ok(Expr::Ident(fallback)),
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(Expr::Ident(name))
|
||||
}
|
||||
|
|
|
|||
28
examples/form.ds
Normal file
28
examples/form.ds
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- DreamStack Form Example
|
||||
-- Two-way binding, validation, and reactive form state
|
||||
|
||||
let name = ""
|
||||
let email = ""
|
||||
let message = ""
|
||||
let submitted = false
|
||||
let char_count = 0
|
||||
|
||||
view main = column [
|
||||
text "Contact Form"
|
||||
|
||||
text "Name"
|
||||
input { bind: name, placeholder: "Your name" }
|
||||
|
||||
text "Email"
|
||||
input { bind: email, placeholder: "you@example.com" }
|
||||
|
||||
text "Message"
|
||||
input { bind: message, placeholder: "Type your message..." }
|
||||
|
||||
text char_count
|
||||
|
||||
button "Submit" { click: submitted = true }
|
||||
|
||||
when submitted ->
|
||||
text "Thanks for your message!"
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue