From e69c13eabffded618787df93e678d7724ff56bc8 Mon Sep 17 00:00:00 2001 From: dpiercey Date: Thu, 6 Feb 2025 14:53:08 -0700 Subject: [PATCH] fix: share bound attr change handler --- .changeset/serious-dryers-relate.md | 5 + .../__snapshots__/.name-cache.json | 4 +- .../__snapshots__/.name-cache.json | 4 +- .../dom.expected/template.hydrate.js | 72 ++++----- .../__snapshots__/dom.expected/template.js | 30 ++-- .../__snapshots__/html.expected/template.js | 12 +- .../__snapshots__/resume.expected.md | 8 +- .../__snapshots__/ssr.expected.md | 4 +- .../controllable-checked-value/template.marko | 1 + .../__snapshots__/.name-cache.json | 4 +- .../dom.expected/template.hydrate.js | 46 +++--- .../__snapshots__/dom.expected/template.js | 32 ++-- .../__snapshots__/html.expected/template.js | 16 +- .../__snapshots__/resume.expected.md | 8 +- .../__snapshots__/ssr.expected.md | 4 +- .../__snapshots__/.name-cache.json | 4 +- .../dom.expected/template.hydrate.js | 72 ++++----- .../__snapshots__/dom.expected/template.js | 30 ++-- .../__snapshots__/html.expected/template.js | 12 +- .../__snapshots__/resume.expected.md | 8 +- .../__snapshots__/ssr.expected.md | 4 +- .../__snapshots__/.name-cache.json | 4 +- .../__snapshots__/.name-cache.json | 4 +- .../__snapshots__/.name-cache.json | 4 +- .../__snapshots__/.name-cache.json | 3 +- .../__snapshots__/.name-cache.json | 4 +- .../__snapshots__/.name-cache.json | 6 +- .../dom.expected/template.error.txt | 4 +- .../html.expected/template.error.txt | 4 +- .../__snapshots__/.name-cache.json | 4 +- .../__snapshots__/.name-cache.json | 3 +- .../dom.expected/template.hydrate.js | 4 +- .../__snapshots__/dom.expected/template.js | 4 +- .../__snapshots__/html.expected/template.js | 4 +- .../src/translator/visitors/tag/index.ts | 137 +++++++++++++----- 35 files changed, 310 insertions(+), 259 deletions(-) create mode 100644 .changeset/serious-dryers-relate.md diff --git a/.changeset/serious-dryers-relate.md b/.changeset/serious-dryers-relate.md new file mode 100644 index 000000000..8bdf2d7f8 --- /dev/null +++ b/.changeset/serious-dryers-relate.md @@ -0,0 +1,5 @@ +--- +"@marko/runtime-tags": patch +--- + +When using the bound attribute syntax, prefer to create a simple shared function that assigns to the bound variable. diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-spread/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-spread/__snapshots__/.name-cache.json index bfbaa8f44..7d67a8817 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-spread/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-spread/__snapshots__/.name-cache.json @@ -6,7 +6,9 @@ "$_input__effect": "a", "$_input_": "r", "$_checkedChange": "o", - "$_checked": "m" + "$_checked": "m", + "$_expr_checked__checkedChange": "n", + "$_checked2": "c" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/.name-cache.json index 9432ff80b..d1d8fe281 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/.name-cache.json @@ -6,7 +6,9 @@ "$_checkedValueChange": "c", "$_checkedValueChange2": "a", "$_checkedValueChange3": "l", - "$_checkedValue": "o" + "$_checkedValue": "o", + "$_expr_checkedValue__checkedValueChange": "n", + "$_checkedValue2": "r" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.hydrate.js index 8e3a55ac4..6ef0b6159 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.hydrate.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.hydrate.js @@ -1,49 +1,41 @@ -// size: 515 (min) 165 (brotli) -const _checkedValue = _$.state(4, (_scope, checkedValue) => { - _$.controllable_input_checkedValue( - _scope, - 0, - checkedValue, - _checkedValueChange3(_scope), - "a", - ), +// size: 451 (min) 178 (brotli) +const _expr_checkedValue__checkedValueChange = _$.intersection(2, (_scope) => { + const { 4: checkedValue, 5: _checkedValueChange } = _scope; _$.controllable_input_checkedValue( _scope, - 1, + 0, checkedValue, - _checkedValueChange2(_scope), - "b", + _checkedValueChange, + "a", ), - _$.controllable_input_checkedValue( - _scope, - 2, - checkedValue, - _checkedValueChange(_scope), - "c", - ), - _$.data(_scope[3], checkedValue); -}); -function _checkedValueChange(_scope) { - return (_new_checkedValue3) => { - _checkedValue(_scope, _new_checkedValue3); - }; -} -function _checkedValueChange2(_scope) { - return (_new_checkedValue2) => { - _checkedValue(_scope, _new_checkedValue2); - }; -} -function _checkedValueChange3(_scope) { - return (_new_checkedValue) => { - _checkedValue(_scope, _new_checkedValue); - }; -} -_$.effect("a3", (_scope) => { + _$.controllable_input_checkedValue( + _scope, + 1, + checkedValue, + _checkedValueChange, + "b", + ), + _$.controllable_input_checkedValue( + _scope, + 2, + checkedValue, + _checkedValueChange, + "c", + ); + }), + _checkedValue = _$.state( + 4, + (_scope, checkedValue) => _$.data(_scope[3], checkedValue), + () => _expr_checkedValue__checkedValueChange, + ); +_$.effect("a1", (_scope) => { _$.controllable_input_checkedValue_effect(_scope, 0), _$.controllable_input_checkedValue_effect(_scope, 1), _$.controllable_input_checkedValue_effect(_scope, 2); }), - _$.register("a2", _checkedValueChange), - _$.register("a1", _checkedValueChange2), - _$.register("a0", _checkedValueChange3), + _$.register("a0", function (_scope) { + return (_new_checkedValue) => { + _checkedValue(_scope, _new_checkedValue); + }; + }), init(); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.js index 0c37ce515..7fafb5ea1 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/dom.expected/template.js @@ -1,12 +1,17 @@ export const _template_ = " "; export const _walks_ = /* get, over(1), get, over(1), get, over(1), next(1), get, out(1) */" b b bD l"; import * as _$ from "@marko/runtime-tags/debug/dom"; -const _checkedValue = /* @__PURE__ */_$.state("checkedValue", (_scope, checkedValue) => { - _$.controllable_input_checkedValue(_scope, "#input/0", checkedValue, _checkedValueChange3(_scope), "a"); - _$.controllable_input_checkedValue(_scope, "#input/1", checkedValue, _checkedValueChange2(_scope), "b"); - _$.controllable_input_checkedValue(_scope, "#input/2", checkedValue, _checkedValueChange(_scope), "c"); - _$.data(_scope["#text/3"], checkedValue); +const _expr_checkedValue__checkedValueChange = /* @__PURE__ */_$.intersection(2, _scope => { + const { + checkedValue, + _checkedValueChange + } = _scope; + _$.controllable_input_checkedValue(_scope, "#input/0", checkedValue, _checkedValueChange, "a"); + _$.controllable_input_checkedValue(_scope, "#input/1", checkedValue, _checkedValueChange, "b"); + _$.controllable_input_checkedValue(_scope, "#input/2", checkedValue, _checkedValueChange, "c"); }); +const _checkedValueChange2 = /* @__PURE__ */_$.value("_checkedValueChange", 0, () => _expr_checkedValue__checkedValueChange); +const _checkedValue = /* @__PURE__ */_$.state("checkedValue", (_scope, checkedValue) => _$.data(_scope["#text/3"], checkedValue), () => _expr_checkedValue__checkedValueChange); const _setup__effect = _$.effect("__tests__/template.marko_0", _scope => { _$.controllable_input_checkedValue_effect(_scope, "#input/0"); _$.controllable_input_checkedValue_effect(_scope, "#input/1"); @@ -15,23 +20,12 @@ const _setup__effect = _$.effect("__tests__/template.marko_0", _scope => { export function _setup_(_scope) { _setup__effect(_scope); _checkedValue(_scope, "a"); -} -function _checkedValueChange(_scope) { - return _new_checkedValue3 => { - _checkedValue(_scope, _new_checkedValue3); - }; -} -function _checkedValueChange2(_scope) { - return _new_checkedValue2 => { - _checkedValue(_scope, _new_checkedValue2); - }; + _checkedValueChange2(_scope, _checkedValueChange3(_scope)); } function _checkedValueChange3(_scope) { return _new_checkedValue => { _checkedValue(_scope, _new_checkedValue); }; } -_$.register("__tests__/template.marko_0/checkedValueChange_1", _checkedValueChange); -_$.register("__tests__/template.marko_0/checkedValueChange_0", _checkedValueChange2); -_$.register("__tests__/template.marko_0/checkedValueChange", _checkedValueChange3); +_$.register("__tests__/template.marko_0/_checkedValueChange", _checkedValueChange3); export default /* @__PURE__ */_$.createTemplate("__tests__/template.marko", _template_, _walks_, _setup_); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/html.expected/template.js index 5c60a83f7..d85d11ba3 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/html.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/html.expected/template.js @@ -2,16 +2,14 @@ import * as _$ from "@marko/runtime-tags/debug/html"; const _renderer = /* @__PURE__ */_$.createRenderer(input => { const _scope0_id = _$.nextScopeId(); const checkedValue = "a"; - _$.write(` { + const _checkedValueChange = _$.register(_new_checkedValue => { checkedValue = _new_checkedValue; - }, "__tests__/template.marko_0/checkedValueChange", _scope0_id), "a")} type=radio>${_$.markResumeNode(_scope0_id, "#input/0")} { - checkedValue = _new_checkedValue2; - }, "__tests__/template.marko_0/checkedValueChange_0", _scope0_id), "b")} type=radio>${_$.markResumeNode(_scope0_id, "#input/1")} { - checkedValue = _new_checkedValue3; - }, "__tests__/template.marko_0/checkedValueChange_1", _scope0_id), "c")} type=radio>${_$.markResumeNode(_scope0_id, "#input/2")}${_$.escapeXML(checkedValue)}${_$.markResumeNode(_scope0_id, "#text/3")}`); + }, "__tests__/template.marko_0/_checkedValueChange", _scope0_id); + _$.write(`${_$.markResumeNode(_scope0_id, "#input/0")}${_$.markResumeNode(_scope0_id, "#input/1")}${_$.markResumeNode(_scope0_id, "#input/2")}${_$.escapeXML(checkedValue)}${_$.markResumeNode(_scope0_id, "#text/3")}`); _$.writeEffect(_scope0_id, "__tests__/template.marko_0"); _$.debug(_$.writeScope(_scope0_id, { - "checkedValue": checkedValue + "checkedValue": checkedValue, + "_checkedValueChange": _checkedValueChange }), "__tests__/template.marko", 0, { "checkedValue": "1:6" }); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/resume.expected.md index 78ca695a4..ffa259e27 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/resume.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/resume.expected.md @@ -24,7 +24,7 @@ @@ -60,7 +60,7 @@ container.querySelectorAll(`input`)[1].click(); @@ -100,7 +100,7 @@ container.querySelectorAll(`input`)[2].click(); @@ -140,7 +140,7 @@ container.querySelectorAll(`input`)[0].click(); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/ssr.expected.md index 6184843ea..630473cb6 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/ssr.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/__snapshots__/ssr.expected.md @@ -1,6 +1,6 @@ # Write ```html - a + a ``` # Render End @@ -29,7 +29,7 @@ diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/template.marko b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/template.marko index 32f12e73d..9a2a00ac5 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/template.marko +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-value/template.marko @@ -1,4 +1,5 @@ + diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/.name-cache.json index 9b87b411f..63c86cc05 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/.name-cache.json @@ -8,7 +8,9 @@ "$_checkedValueChange": "i", "$_checkedValueChange2": "n", "$_checkedValueChange3": "r", - "$_checkedValue": "u" + "$_checkedValue": "u", + "$_expr_checkedValue__checkedValueChange": "o", + "$_checkedValue2": "h" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.hydrate.js index cdee5d283..3ec330169 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.hydrate.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.hydrate.js @@ -1,25 +1,25 @@ -// size: 558 (min) 240 (brotli) +// size: 494 (min) 238 (brotli) const _input__effect = _$.effect("a0", (_scope) => _$.attrsEvents(_scope, 0)), _input_ = _$.value(2, (_scope, input) => { _$.attrs(_scope, 0, { type: "checkbox", ...input }), _input__effect(_scope); }), - _checkedValue = _$.state( - 4, - (_scope, checkedValue) => { - _$.data(_scope[3], checkedValue), - _input_(_scope[0], { - checkedValue: checkedValue, - checkedValueChange: _checkedValueChange(_scope), - value: "a", - }), + _expr_checkedValue__checkedValueChange = _$.intersection( + 2, + (_scope) => { + const { 4: checkedValue, 5: _checkedValueChange } = _scope; + _input_(_scope[0], { + checkedValue: checkedValue, + checkedValueChange: _checkedValueChange, + value: "a", + }), _input_(_scope[1], { checkedValue: checkedValue, - checkedValueChange: _checkedValueChange2(_scope), + checkedValueChange: _checkedValueChange, value: "b", }), _input_(_scope[2], { checkedValue: checkedValue, - checkedValueChange: _checkedValueChange3(_scope), + checkedValueChange: _checkedValueChange, value: "c", }); }, @@ -29,23 +29,15 @@ const _input__effect = _$.effect("a0", (_scope) => _$.attrsEvents(_scope, 0)), _$.inChild(1, _input_), _$.inChild(2, _input_), ]), + ), + _checkedValue = _$.state( + 4, + (_scope, checkedValue) => _$.data(_scope[3], checkedValue), + () => _expr_checkedValue__checkedValueChange, ); -function _checkedValueChange(_scope) { +_$.register("b0", function (_scope) { return (_new_checkedValue) => { _checkedValue(_scope, _new_checkedValue); }; -} -function _checkedValueChange2(_scope) { - return (_new_checkedValue2) => { - _checkedValue(_scope, _new_checkedValue2); - }; -} -function _checkedValueChange3(_scope) { - return (_new_checkedValue3) => { - _checkedValue(_scope, _new_checkedValue3); - }; -} -_$.register("b0", _checkedValueChange), - _$.register("b1", _checkedValueChange2), - _$.register("b2", _checkedValueChange3), +}), init(); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.js index a9cc9f2d8..69fc6f39f 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/dom.expected/template.js @@ -2,46 +2,40 @@ export const _template_ = `${_checkbox_template}${_checkbox_template}${_checkbox export const _walks_ = /* beginChild, _checkbox_walks, endChild, beginChild, _checkbox_walks, endChild, beginChild, _checkbox_walks, endChild, next(1), get, out(1) */`/${_checkbox_walks}&/${_checkbox_walks}&/${_checkbox_walks}&D l`; import { _setup_ as _checkbox, _input_ as _checkbox_input, _template_ as _checkbox_template, _walks_ as _checkbox_walks } from "./tags/checkbox.marko"; import * as _$ from "@marko/runtime-tags/debug/dom"; -const _checkedValue = /* @__PURE__ */_$.state("checkedValue", (_scope, checkedValue) => { - _$.data(_scope["#text/3"], checkedValue); +const _expr_checkedValue__checkedValueChange = /* @__PURE__ */_$.intersection(2, _scope => { + const { + checkedValue, + _checkedValueChange + } = _scope; _checkbox_input(_scope["#childScope/0"], { checkedValue: checkedValue, - checkedValueChange: _checkedValueChange(_scope), + checkedValueChange: _checkedValueChange, value: "a" }); _checkbox_input(_scope["#childScope/1"], { checkedValue: checkedValue, - checkedValueChange: _checkedValueChange2(_scope), + checkedValueChange: _checkedValueChange, value: "b" }); _checkbox_input(_scope["#childScope/2"], { checkedValue: checkedValue, - checkedValueChange: _checkedValueChange3(_scope), + checkedValueChange: _checkedValueChange, value: "c" }); }, () => _$.intersections([/* @__PURE__ */_$.inChild("#childScope/0", _checkbox_input), /* @__PURE__ */_$.inChild("#childScope/1", _checkbox_input), /* @__PURE__ */_$.inChild("#childScope/2", _checkbox_input)])); +const _checkedValueChange2 = /* @__PURE__ */_$.value("_checkedValueChange", 0, () => _expr_checkedValue__checkedValueChange); +const _checkedValue = /* @__PURE__ */_$.state("checkedValue", (_scope, checkedValue) => _$.data(_scope["#text/3"], checkedValue), () => _expr_checkedValue__checkedValueChange); export function _setup_(_scope) { _checkbox(_scope["#childScope/0"]); _checkbox(_scope["#childScope/1"]); _checkbox(_scope["#childScope/2"]); _checkedValue(_scope, ["a", "b"]); + _checkedValueChange2(_scope, _checkedValueChange3(_scope)); } -function _checkedValueChange(_scope) { +function _checkedValueChange3(_scope) { return _new_checkedValue => { _checkedValue(_scope, _new_checkedValue); }; } -function _checkedValueChange2(_scope) { - return _new_checkedValue2 => { - _checkedValue(_scope, _new_checkedValue2); - }; -} -function _checkedValueChange3(_scope) { - return _new_checkedValue3 => { - _checkedValue(_scope, _new_checkedValue3); - }; -} -_$.register("__tests__/template.marko_0/checkedValueChange", _checkedValueChange); -_$.register("__tests__/template.marko_0/checkedValueChange_0", _checkedValueChange2); -_$.register("__tests__/template.marko_0/checkedValueChange_1", _checkedValueChange3); +_$.register("__tests__/template.marko_0/_checkedValueChange", _checkedValueChange3); export default /* @__PURE__ */_$.createTemplate("__tests__/template.marko", _template_, _walks_, _setup_); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/html.expected/template.js index 01737ab43..369c5d9e5 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/html.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/html.expected/template.js @@ -3,32 +3,30 @@ import * as _$ from "@marko/runtime-tags/debug/html"; const _renderer = /* @__PURE__ */_$.createRenderer(input => { const _scope0_id = _$.nextScopeId(); const checkedValue = ["a", "b"]; + const _checkedValueChange = _$.register(_new_checkedValue => { + checkedValue = _new_checkedValue; + }, "__tests__/template.marko_0/_checkedValueChange", _scope0_id); const _childScope = _$.peekNextScope(); _checkbox({ checkedValue: checkedValue, - checkedValueChange: _$.register(_new_checkedValue => { - checkedValue = _new_checkedValue; - }, "__tests__/template.marko_0/checkedValueChange", _scope0_id), + checkedValueChange: _checkedValueChange, value: "a" }); const _childScope2 = _$.peekNextScope(); _checkbox({ checkedValue: checkedValue, - checkedValueChange: _$.register(_new_checkedValue2 => { - checkedValue = _new_checkedValue2; - }, "__tests__/template.marko_0/checkedValueChange_0", _scope0_id), + checkedValueChange: _checkedValueChange, value: "b" }); const _childScope3 = _$.peekNextScope(); _checkbox({ checkedValue: checkedValue, - checkedValueChange: _$.register(_new_checkedValue3 => { - checkedValue = _new_checkedValue3; - }, "__tests__/template.marko_0/checkedValueChange_1", _scope0_id), + checkedValueChange: _checkedValueChange, value: "c" }); _$.write(`${_$.escapeXML(checkedValue)}${_$.markResumeNode(_scope0_id, "#text/3")}`); _$.debug(_$.writeScope(_scope0_id, { + "_checkedValueChange": _checkedValueChange, "#childScope/0": _$.writeExistingScope(_childScope), "#childScope/1": _$.writeExistingScope(_childScope2), "#childScope/2": _$.writeExistingScope(_childScope3) diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/resume.expected.md index e8a33b091..6334dc721 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/resume.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/resume.expected.md @@ -25,7 +25,7 @@ @@ -61,7 +61,7 @@ container.querySelector("input").click(); @@ -102,7 +102,7 @@ container.querySelector("input").click(); @@ -142,7 +142,7 @@ container.querySelector("input").click(); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/ssr.expected.md index 62d06e23d..0cfcecc43 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/ssr.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values-spread/__snapshots__/ssr.expected.md @@ -1,6 +1,6 @@ # Write ```html - a,b + a,b ``` # Render End @@ -30,7 +30,7 @@ diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/.name-cache.json index 9432ff80b..d1d8fe281 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/.name-cache.json @@ -6,7 +6,9 @@ "$_checkedValueChange": "c", "$_checkedValueChange2": "a", "$_checkedValueChange3": "l", - "$_checkedValue": "o" + "$_checkedValue": "o", + "$_expr_checkedValue__checkedValueChange": "n", + "$_checkedValue2": "r" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.hydrate.js index 8e3a55ac4..6ef0b6159 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.hydrate.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.hydrate.js @@ -1,49 +1,41 @@ -// size: 515 (min) 165 (brotli) -const _checkedValue = _$.state(4, (_scope, checkedValue) => { - _$.controllable_input_checkedValue( - _scope, - 0, - checkedValue, - _checkedValueChange3(_scope), - "a", - ), +// size: 451 (min) 178 (brotli) +const _expr_checkedValue__checkedValueChange = _$.intersection(2, (_scope) => { + const { 4: checkedValue, 5: _checkedValueChange } = _scope; _$.controllable_input_checkedValue( _scope, - 1, + 0, checkedValue, - _checkedValueChange2(_scope), - "b", + _checkedValueChange, + "a", ), - _$.controllable_input_checkedValue( - _scope, - 2, - checkedValue, - _checkedValueChange(_scope), - "c", - ), - _$.data(_scope[3], checkedValue); -}); -function _checkedValueChange(_scope) { - return (_new_checkedValue3) => { - _checkedValue(_scope, _new_checkedValue3); - }; -} -function _checkedValueChange2(_scope) { - return (_new_checkedValue2) => { - _checkedValue(_scope, _new_checkedValue2); - }; -} -function _checkedValueChange3(_scope) { - return (_new_checkedValue) => { - _checkedValue(_scope, _new_checkedValue); - }; -} -_$.effect("a3", (_scope) => { + _$.controllable_input_checkedValue( + _scope, + 1, + checkedValue, + _checkedValueChange, + "b", + ), + _$.controllable_input_checkedValue( + _scope, + 2, + checkedValue, + _checkedValueChange, + "c", + ); + }), + _checkedValue = _$.state( + 4, + (_scope, checkedValue) => _$.data(_scope[3], checkedValue), + () => _expr_checkedValue__checkedValueChange, + ); +_$.effect("a1", (_scope) => { _$.controllable_input_checkedValue_effect(_scope, 0), _$.controllable_input_checkedValue_effect(_scope, 1), _$.controllable_input_checkedValue_effect(_scope, 2); }), - _$.register("a2", _checkedValueChange), - _$.register("a1", _checkedValueChange2), - _$.register("a0", _checkedValueChange3), + _$.register("a0", function (_scope) { + return (_new_checkedValue) => { + _checkedValue(_scope, _new_checkedValue); + }; + }), init(); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.js index 4e1a91996..d7271bd73 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/dom.expected/template.js @@ -1,12 +1,17 @@ export const _template_ = " "; export const _walks_ = /* get, over(1), get, over(1), get, over(1), next(1), get, out(1) */" b b bD l"; import * as _$ from "@marko/runtime-tags/debug/dom"; -const _checkedValue = /* @__PURE__ */_$.state("checkedValue", (_scope, checkedValue) => { - _$.controllable_input_checkedValue(_scope, "#input/0", checkedValue, _checkedValueChange3(_scope), "a"); - _$.controllable_input_checkedValue(_scope, "#input/1", checkedValue, _checkedValueChange2(_scope), "b"); - _$.controllable_input_checkedValue(_scope, "#input/2", checkedValue, _checkedValueChange(_scope), "c"); - _$.data(_scope["#text/3"], checkedValue); +const _expr_checkedValue__checkedValueChange = /* @__PURE__ */_$.intersection(2, _scope => { + const { + checkedValue, + _checkedValueChange + } = _scope; + _$.controllable_input_checkedValue(_scope, "#input/0", checkedValue, _checkedValueChange, "a"); + _$.controllable_input_checkedValue(_scope, "#input/1", checkedValue, _checkedValueChange, "b"); + _$.controllable_input_checkedValue(_scope, "#input/2", checkedValue, _checkedValueChange, "c"); }); +const _checkedValueChange2 = /* @__PURE__ */_$.value("_checkedValueChange", 0, () => _expr_checkedValue__checkedValueChange); +const _checkedValue = /* @__PURE__ */_$.state("checkedValue", (_scope, checkedValue) => _$.data(_scope["#text/3"], checkedValue), () => _expr_checkedValue__checkedValueChange); const _setup__effect = _$.effect("__tests__/template.marko_0", _scope => { _$.controllable_input_checkedValue_effect(_scope, "#input/0"); _$.controllable_input_checkedValue_effect(_scope, "#input/1"); @@ -15,23 +20,12 @@ const _setup__effect = _$.effect("__tests__/template.marko_0", _scope => { export function _setup_(_scope) { _setup__effect(_scope); _checkedValue(_scope, ["a", "b"]); -} -function _checkedValueChange(_scope) { - return _new_checkedValue3 => { - _checkedValue(_scope, _new_checkedValue3); - }; -} -function _checkedValueChange2(_scope) { - return _new_checkedValue2 => { - _checkedValue(_scope, _new_checkedValue2); - }; + _checkedValueChange2(_scope, _checkedValueChange3(_scope)); } function _checkedValueChange3(_scope) { return _new_checkedValue => { _checkedValue(_scope, _new_checkedValue); }; } -_$.register("__tests__/template.marko_0/checkedValueChange_1", _checkedValueChange); -_$.register("__tests__/template.marko_0/checkedValueChange_0", _checkedValueChange2); -_$.register("__tests__/template.marko_0/checkedValueChange", _checkedValueChange3); +_$.register("__tests__/template.marko_0/_checkedValueChange", _checkedValueChange3); export default /* @__PURE__ */_$.createTemplate("__tests__/template.marko", _template_, _walks_, _setup_); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/html.expected/template.js index dc041e550..b5b80d1ea 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/html.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/html.expected/template.js @@ -2,16 +2,14 @@ import * as _$ from "@marko/runtime-tags/debug/html"; const _renderer = /* @__PURE__ */_$.createRenderer(input => { const _scope0_id = _$.nextScopeId(); const checkedValue = ["a", "b"]; - _$.write(` { + const _checkedValueChange = _$.register(_new_checkedValue => { checkedValue = _new_checkedValue; - }, "__tests__/template.marko_0/checkedValueChange", _scope0_id), "a")} type=checkbox>${_$.markResumeNode(_scope0_id, "#input/0")} { - checkedValue = _new_checkedValue2; - }, "__tests__/template.marko_0/checkedValueChange_0", _scope0_id), "b")} type=checkbox>${_$.markResumeNode(_scope0_id, "#input/1")} { - checkedValue = _new_checkedValue3; - }, "__tests__/template.marko_0/checkedValueChange_1", _scope0_id), "c")} type=checkbox>${_$.markResumeNode(_scope0_id, "#input/2")}${_$.escapeXML(checkedValue)}${_$.markResumeNode(_scope0_id, "#text/3")}`); + }, "__tests__/template.marko_0/_checkedValueChange", _scope0_id); + _$.write(`${_$.markResumeNode(_scope0_id, "#input/0")}${_$.markResumeNode(_scope0_id, "#input/1")}${_$.markResumeNode(_scope0_id, "#input/2")}${_$.escapeXML(checkedValue)}${_$.markResumeNode(_scope0_id, "#text/3")}`); _$.writeEffect(_scope0_id, "__tests__/template.marko_0"); _$.debug(_$.writeScope(_scope0_id, { - "checkedValue": checkedValue + "checkedValue": checkedValue, + "_checkedValueChange": _checkedValueChange }), "__tests__/template.marko", 0, { "checkedValue": "1:6" }); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/resume.expected.md index 38addc0c2..f7f5831f7 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/resume.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/resume.expected.md @@ -25,7 +25,7 @@ @@ -61,7 +61,7 @@ container.querySelectorAll(`input`)[1].click(); @@ -102,7 +102,7 @@ container.querySelectorAll(`input`)[2].click(); @@ -142,7 +142,7 @@ container.querySelectorAll(`input`)[0].click(); diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/ssr.expected.md index c01e2eb7f..619d30c17 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/ssr.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked-values/__snapshots__/ssr.expected.md @@ -1,6 +1,6 @@ # Write ```html - a,b + a,b ``` # Render End @@ -30,7 +30,7 @@ diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked/__snapshots__/.name-cache.json index 7cb63aed7..ade3bedae 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-checked/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-checked/__snapshots__/.name-cache.json @@ -4,7 +4,9 @@ "$_$": "t", "$init": "e", "$_checkedChange": "o", - "$_checked": "r" + "$_checked": "r", + "$_expr_checked__checkedChange": "n", + "$_checked2": "a" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-input-value/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/controllable-input-value/__snapshots__/.name-cache.json index 85d528207..f7b5bc3f1 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-input-value/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-input-value/__snapshots__/.name-cache.json @@ -4,7 +4,9 @@ "$_$": "t", "$init": "a", "$_valueChange": "o", - "$_value": "e" + "$_value": "e", + "$_expr_value__valueChange": "r", + "$_value2": "n" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/controllable-textarea-value/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/controllable-textarea-value/__snapshots__/.name-cache.json index 370fe935c..b482ec58f 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/controllable-textarea-value/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/controllable-textarea-value/__snapshots__/.name-cache.json @@ -4,7 +4,9 @@ "$_$": "t", "$init": "a", "$_valueChange": "e", - "$_value": "o" + "$_value": "o", + "$_expr_value__valueChange": "r", + "$_value2": "n" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/cross-tag-closure/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/cross-tag-closure/__snapshots__/.name-cache.json index e3caefb48..24e6fbdb8 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/cross-tag-closure/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/cross-tag-closure/__snapshots__/.name-cache.json @@ -6,7 +6,8 @@ "$_value": "a", "$_count$myTag_content_effect": "e", "$_count$myTag_content": "n", - "$_setup$myTag_content": "i" + "$_setup$myTag_content": "i", + "$_value2": "o" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/custom-tag-var-assignment/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/custom-tag-var-assignment/__snapshots__/.name-cache.json index eaadce092..37cc8e202 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/custom-tag-var-assignment/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/custom-tag-var-assignment/__snapshots__/.name-cache.json @@ -5,7 +5,9 @@ "$init": "a", "$_x_effect": "n", "$_x": "r", - "$_count_effect": "i" + "$_count_effect": "i", + "$_x2_effect": "o", + "$_x2": "e" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/dynamic-tag-var-assignment/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/dynamic-tag-var-assignment/__snapshots__/.name-cache.json index 1b81cfb65..bff0cea97 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/dynamic-tag-var-assignment/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/dynamic-tag-var-assignment/__snapshots__/.name-cache.json @@ -7,7 +7,11 @@ "$_x_effect": "r", "$_x": "e", "$_setup_": "i", - "$Counter": "o" + "$Counter": "o", + "$_xChange2": "c", + "$_x2_effect": "g", + "$_x2": "u", + "$_xChange3": "f" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/dom.expected/template.error.txt b/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/dom.expected/template.error.txt index 5f63e43d3..16ac3f580 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/dom.expected/template.error.txt +++ b/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/dom.expected/template.error.txt @@ -1,5 +1,5 @@ - at packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/template.marko:1:11 + at packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/template.marko:1:13 > 1 | - | ^^^ Attributes may only be bound to identifiers or member expressions + | ^ Attributes may only be bound to identifiers or member expressions 2 | \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/html.expected/template.error.txt b/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/html.expected/template.error.txt index 5f63e43d3..16ac3f580 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/html.expected/template.error.txt +++ b/packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/__snapshots__/html.expected/template.error.txt @@ -1,5 +1,5 @@ - at packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/template.marko:1:11 + at packages/runtime-tags/src/__tests__/fixtures/error-let-invalid-binding/template.marko:1:13 > 1 | - | ^^^ Attributes may only be bound to identifiers or member expressions + | ^ Attributes may only be bound to identifiers or member expressions 2 | \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/let-tag-controllable-child/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/let-tag-controllable-child/__snapshots__/.name-cache.json index 9370d2ef7..7b194d6b5 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/let-tag-controllable-child/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/let-tag-controllable-child/__snapshots__/.name-cache.json @@ -18,7 +18,9 @@ "$_source": "d", "$_expr_input_value_input_valueChange": "g", "$_otherState_effect": "h", - "$_state_effect": "k" + "$_state_effect": "k", + "$_expr_source__sourceChange": "C", + "$_source2": "p" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/.name-cache.json index fe850c344..7692e9628 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/.name-cache.json +++ b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/.name-cache.json @@ -14,7 +14,8 @@ "$_count2$1": "f", "$_input_count2_": "u", "$_expr_input_count2Change_input_count": "d", - "$_expr_input_count1Change_input_count": "l" + "$_expr_input_count1Change_input_count": "l", + "$_count3": "g" } } } diff --git a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.hydrate.js index a611ec7c8..fcb2b0d3c 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.hydrate.js +++ b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.hydrate.js @@ -40,8 +40,8 @@ const _expr_input_count2_input_count2Change = _$.intersection(2, (_scope) => { () => _$.inChild(0, _input_count1_), ); _$.register("b0", function (_scope) { - return (_new_count) => { - _count(_scope, _new_count); + return (_new_count1) => { + _count(_scope, _new_count1); }; }), _$.register("b1", function (_scope) { diff --git a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.js index 9041555ca..43ecc8d9f 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/dom.expected/template.js @@ -18,8 +18,8 @@ export function _setup_(_scope) { _counters_input_count2Change(_scope["#childScope/0"], _count2Change(_scope)); } function _count1Change(_scope) { - return _new_count => { - _count(_scope, _new_count); + return _new_count1 => { + _count(_scope, _new_count1); }; } function _count2Change(_scope) { diff --git a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/html.expected/template.js index d2f229d5a..974ced00c 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/html.expected/template.js +++ b/packages/runtime-tags/src/__tests__/fixtures/multiple-bound-values/__snapshots__/html.expected/template.js @@ -7,8 +7,8 @@ const _renderer = /* @__PURE__ */_$.createRenderer(input => { const _childScope = _$.peekNextScope(); _counters({ count1: count1, - count1Change: _$.register(_new_count => { - count1 = _new_count; + count1Change: _$.register(_new_count1 => { + count1 = _new_count1; }, "__tests__/template.marko_0/count1Change", _scope0_id), count2: count2, count2Change: _$.register(_new_count2 => { diff --git a/packages/runtime-tags/src/translator/visitors/tag/index.ts b/packages/runtime-tags/src/translator/visitors/tag/index.ts index 51a89d78e..8c7e72784 100644 --- a/packages/runtime-tags/src/translator/visitors/tag/index.ts +++ b/packages/runtime-tags/src/translator/visitors/tag/index.ts @@ -5,6 +5,7 @@ import { type Plugin, } from "@marko/compiler/babel-utils"; +import { getMarkoRoot, isMarko } from "../../util/get-root"; import { isOutputHTML } from "../../util/marko-config"; import * as hooks from "../../util/plugin-hooks"; import analyzeTagNameType, { TagNameType } from "../../util/tag-name-type"; @@ -16,6 +17,10 @@ import DynamicTag from "./dynamic-tag"; import NativeTag from "./native-tag"; const TAG_NAME_IDENTIFIER_REG = /^[A-Z][a-zA-Z0-9_$]*$/; +const BINDING_CHANGE_HANDLER = new WeakMap< + t.Identifier, + t.MarkoAttribute | t.Identifier +>(); export default { transform: { @@ -39,19 +44,7 @@ export default { const attr = attributes[i]; if (t.isMarkoAttribute(attr) && attr.bound) { attr.bound = false; - const changeValue = getChangeHandler(tag, attr); - if (changeValue === null) { - throw tag.hub.buildError( - attr, - "Attributes may only be bound to identifiers or member expressions", - ); - } - - attributes.splice( - ++i, - 0, - t.markoAttribute(attr.name + "Change", changeValue), - ); + attributes.splice(++i, 0, getChangeHandler(tag, attr)); crawl = true; } @@ -208,33 +201,109 @@ export default { function getChangeHandler( tag: t.NodePath, - attr: t.MarkoAttribute | t.MarkoSpreadAttribute, -) { + attr: t.MarkoAttribute, +): t.MarkoAttribute { + const attrName = attr.name; + const changeAttrName = attrName + "Change"; + if (t.isIdentifier(attr.value)) { - const valueId = tag.scope.generateUidIdentifier("new_" + attr.value.name); - return t.arrowFunctionExpression( - [valueId], - t.blockStatement([ - t.expressionStatement( - t.assignmentExpression("=", t.cloneNode(attr.value), valueId), + const binding = tag.scope.getBinding(attr.value.name); + if (!binding) + return t.markoAttribute( + changeAttrName, + buildChangeHandlerFunction(attr.value), + ); + + const existingChangedAttr = BINDING_CHANGE_HANDLER.get(binding.identifier); + + if (!existingChangedAttr) { + const changeHandlerAttr = t.markoAttribute( + changeAttrName, + buildChangeHandlerFunction(attr.value), + ); + BINDING_CHANGE_HANDLER.set(binding.identifier, changeHandlerAttr); + return changeHandlerAttr; + } + + if (existingChangedAttr.type === "Identifier") { + return t.markoAttribute( + changeAttrName, + withPreviousLocation( + t.identifier(existingChangedAttr.name), + attr.value, ), - ]), + ); + } + + const markoRoot = isMarko(binding.path) + ? binding.path + : getMarkoRoot(binding.path); + + if (!(markoRoot?.isMarkoTag() || markoRoot?.isMarkoTagBody())) { + throw tag.hub.buildError(attr.value, "Unable to bind to value."); + } + + const changeHandlerId = markoRoot.scope.generateUid(changeAttrName); + const changeHandlerConst = t.markoTag( + t.stringLiteral("const"), + [t.markoAttribute("value", existingChangedAttr.value, null, null, true)], + t.markoTagBody([]), + null, + t.identifier(changeHandlerId), + ); + BINDING_CHANGE_HANDLER.set( + binding.identifier, + (existingChangedAttr.value = t.identifier(changeHandlerId)), + ); + + if (markoRoot.isMarkoTag()) { + markoRoot.insertAfter(changeHandlerConst); + } else { + markoRoot.unshiftContainer("body", changeHandlerConst); + } + + return t.markoAttribute( + changeAttrName, + withPreviousLocation(t.identifier(changeHandlerId), attr.value), ); } else if (t.isMemberExpression(attr.value)) { const prop = attr.value.property; - if (t.isPrivateName(prop)) return null; - if (t.isIdentifier(prop)) { - return t.memberExpression( - t.cloneNode(attr.value.object), - t.identifier(prop.name + "Change"), - ); - } else { - return t.memberExpression( - t.cloneNode(attr.value.object), - t.binaryExpression("+", t.cloneNode(prop), t.stringLiteral("Change")), - true, + if (!t.isPrivateName(attr.value.property)) { + return t.markoAttribute( + changeAttrName, + t.memberExpression( + t.cloneNode(attr.value.object), + prop.type === "Identifier" + ? withPreviousLocation(t.identifier(prop.name + "Change"), prop) + : t.binaryExpression( + "+", + t.cloneNode(prop), + t.stringLiteral("Change"), + ), + prop.type !== "Identifier", + ), ); } } - return null; + + throw tag.hub.buildError( + attr.value, + "Attributes may only be bound to identifiers or member expressions", + ); +} + +function buildChangeHandlerFunction(id: t.Identifier) { + const newId = "_new_" + id.name; + return t.arrowFunctionExpression( + [withPreviousLocation(t.identifier(newId), id)], + t.blockStatement([ + t.expressionStatement( + t.assignmentExpression( + "=", + withPreviousLocation(t.identifier(id.name), id), + withPreviousLocation(t.identifier(newId), id), + ), + ), + ]), + ); }