@invisibleloop/pulse 0.20.59 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/agent/guide-spec.md +1 -0
- package/src/runtime/index.js +22 -3
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.21.0
|
package/src/agent/guide-spec.md
CHANGED
|
@@ -120,6 +120,7 @@ export default {
|
|
|
120
120
|
- data-event="change:mutationName" to fire on input change
|
|
121
121
|
- data-action="actionName" on <form> elements only — submits pass FormData to the action
|
|
122
122
|
- Do NOT use data-event on text inputs to mirror value into state — innerHTML re-renders destroy focus. Capture values from FormData in onStart or run instead.
|
|
123
|
+
- **`data-preserve`** on a `<textarea>`, `<input>` or checkbox — a re-render that renders the same value as before leaves what the user typed or ticked alone (without it, any re-render resets a non-focused control to the rendered value). Use it on long-form editors and any field a re-render elsewhere on the page must not wipe; the control still follows the view when the rendered value changes. Not needed on forms that should clear after submit. Children are matched by position, so the control must keep its place: an element that appears above it (an error banner) rebuilds the field and loses the text — render an empty placeholder in that slot, or wrap the field in a `data-key` container. A focused control is never touched, and a marked field won't reset if the view re-renders the same value on purpose.
|
|
123
124
|
- **`data-debounce="300"`** alongside `data-event="input:search"` — fires the mutation 300ms after the last keystroke. Use for live search, filtering. No per-spec timer code needed.
|
|
124
125
|
- **`data-throttle="100"`** alongside `data-event` — fires at most once per 100ms. Use for scroll or resize-like inputs. Both attributes apply to `input` and `change` events. Value is milliseconds.
|
|
125
126
|
- onSuccess AND onError are both required in every action (missing either will cause a runtime error)
|
package/src/runtime/index.js
CHANGED
|
@@ -598,6 +598,9 @@ function morphKeyed(cur, nxtEls) {
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
function morphAttrs(cur, nxt) {
|
|
601
|
+
// What the previous render said this form control's value/checkedness was — read before the
|
|
602
|
+
// attributes below overwrite it. data-preserve compares against it (see syncFormProperty).
|
|
603
|
+
const prevRendered = renderedFormState(cur)
|
|
601
604
|
for (const { name, value } of Array.from(nxt.attributes)) {
|
|
602
605
|
if (cur.getAttribute(name) !== value) {
|
|
603
606
|
// Trusted Types: script[src] is a TrustedScriptURL sink
|
|
@@ -612,7 +615,7 @@ function morphAttrs(cur, nxt) {
|
|
|
612
615
|
if (cur.tagName === 'DIALOG' && name === 'open') continue
|
|
613
616
|
if (!nxt.hasAttribute(name)) cur.removeAttribute(name)
|
|
614
617
|
}
|
|
615
|
-
syncFormProperty(cur, nxt)
|
|
618
|
+
syncFormProperty(cur, nxt, prevRendered)
|
|
616
619
|
}
|
|
617
620
|
|
|
618
621
|
/**
|
|
@@ -631,15 +634,31 @@ function morphAttrs(cur, nxt) {
|
|
|
631
634
|
* has its own guidance (use FormData, not a controlled value) but this
|
|
632
635
|
* covers every other legitimate re-render source.
|
|
633
636
|
*/
|
|
634
|
-
function
|
|
637
|
+
function renderedFormState(el) {
|
|
638
|
+
const tag = el.tagName
|
|
639
|
+
if (tag === 'TEXTAREA') return el.textContent
|
|
640
|
+
if (tag === 'INPUT') return (el.type === 'checkbox' || el.type === 'radio') ? el.hasAttribute('checked') : (el.getAttribute('value') ?? '')
|
|
641
|
+
return null
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function syncFormProperty(cur, nxt, prevRendered = null) {
|
|
635
645
|
const tag = cur.tagName
|
|
646
|
+
// data-preserve: the view only changes this control when the value it *renders* changes. A re-render
|
|
647
|
+
// that renders the same value as last time (a button elsewhere on the page, a counter) leaves what
|
|
648
|
+
// the user typed or ticked alone. Without it every re-render resets a non-focused control to the
|
|
649
|
+
// rendered value — fine for forms that clear on submit, destructive for a long-form editor.
|
|
650
|
+
const preserve = nxt.hasAttribute('data-preserve')
|
|
636
651
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
637
652
|
const type = cur.type
|
|
638
653
|
if (type === 'checkbox' || type === 'radio') {
|
|
639
654
|
const next = nxt.hasAttribute('checked')
|
|
655
|
+
if (preserve && next === prevRendered) return
|
|
640
656
|
if (cur.checked !== next) cur.checked = next
|
|
641
657
|
} else if (document.activeElement !== cur) {
|
|
642
|
-
|
|
658
|
+
// A textarea has no value attribute: its default value is its child text, which
|
|
659
|
+
// nxt.value reads (and which the HTML parser has already normalised).
|
|
660
|
+
const next = tag === 'TEXTAREA' ? nxt.value : (nxt.getAttribute('value') ?? '')
|
|
661
|
+
if (preserve && next === prevRendered) return
|
|
643
662
|
if (cur.value !== next) cur.value = next
|
|
644
663
|
}
|
|
645
664
|
} else if (tag === 'SELECT' && document.activeElement !== cur) {
|