fix: prevent increment decrement from pruning change handler binding (#2913)

* fix: prevent increment decrement from pruning change handler binding
This commit is contained in:
Ryan Turnquist 2025-10-24 18:41:40 -07:00 committed by GitHub
parent 917c88ed23
commit 280f4d3141
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 465 additions and 3 deletions

View File

@ -0,0 +1,5 @@
---
"@marko/runtime-tags": patch
---
Ensure increment and decrement assignments don't prune change handler bindings

View File

@ -0,0 +1,16 @@
{
"vars": {
"props": {
"$_": "o",
"$init": "t",
"$$pattern2": "n",
"$$bar": "r",
"$$foo2__script": "m",
"$$foo2": "e",
"$$foo": "a",
"$$foo__OR__fooChange__script": "c",
"$$foo__OR__fooChange": "_",
"$$fooChange2": "f"
}
}
}

View File

@ -0,0 +1,39 @@
# Render
```html
<button>
0:0
</button>
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
1:1
</button>
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
2:2
</button>
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
3:3
</button>
```

View File

@ -0,0 +1,59 @@
# Render
```html
<button>
0:0
</button>
```
# Mutations
```
INSERT button
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
1:1
</button>
```
# Mutations
```
UPDATE button/#text2 "0" => "1"
UPDATE button/#text0 "0" => "1"
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
2:2
</button>
```
# Mutations
```
UPDATE button/#text2 "1" => "2"
UPDATE button/#text0 "1" => "2"
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
3:3
</button>
```
# Mutations
```
UPDATE button/#text2 "2" => "3"
UPDATE button/#text0 "2" => "3"
```

View File

@ -0,0 +1,26 @@
// size: 330 (min) 206 (brotli)
const $pattern2 = _._const(4, ($scope, $pattern) => {
($foo2($scope, $pattern.foo), $fooChange2($scope, $pattern.fooChange));
}),
$bar = _._let(3, ($scope, bar) => {
(_._text($scope[2], bar),
$pattern2($scope, { foo: bar, fooChange: $foo($scope) }));
}),
$foo__OR__fooChange__script = _._script(
"a1",
($scope, { 5: foo, 6: $fooChange }) =>
_._on($scope[0], "click", function () {
$fooChange(++foo);
}),
),
$foo__OR__fooChange = _._or(7, $foo__OR__fooChange__script),
$foo2 = _._const(5, ($scope, foo) => {
(_._text($scope[1], foo), $foo__OR__fooChange($scope));
}),
$fooChange2 = _._const(6, $foo__OR__fooChange);
function $foo($scope) {
return function (v) {
$bar($scope, v);
};
}
(_._resume("a0", $foo), init());

View File

@ -0,0 +1,36 @@
export const $template = "<button><!>:<!></button>";
export const $walks = /* get, next(1), replace, over(2), replace, out(1) */" D%c%l";
import * as _ from "@marko/runtime-tags/debug/dom";
const $pattern2 = /* @__PURE__ */_._const("$pattern", ($scope, $pattern) => {
$foo2($scope, $pattern.foo);
$fooChange2($scope, $pattern.fooChange);
});
const $bar = /* @__PURE__ */_._let("bar/3", ($scope, bar) => {
_._text($scope["#text/2"], bar);
$pattern2($scope, {
foo: bar,
fooChange: $foo($scope)
});
});
export function $setup($scope) {
$bar($scope, 0);
}
const $foo__OR__fooChange__script = _._script("__tests__/template.marko_0_foo_$fooChange", ($scope, {
foo,
$fooChange
}) => _._on($scope["#button/0"], "click", function () {
$fooChange(++foo);
}));
const $foo__OR__fooChange = /* @__PURE__ */_._or(7, $foo__OR__fooChange__script);
const $foo2 = /* @__PURE__ */_._const("foo", ($scope, foo) => {
_._text($scope["#text/1"], foo);
$foo__OR__fooChange($scope);
});
const $fooChange2 = /* @__PURE__ */_._const("$fooChange", $foo__OR__fooChange);
function $foo($scope) {
return function (v) {
$bar($scope, v);
};
}
_._resume("__tests__/template.marko_0/foo", $foo);
export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup);

View File

@ -0,0 +1,24 @@
import * as _ from "@marko/runtime-tags/debug/html";
export default _._template("__tests__/template.marko", input => {
const $scope0_id = _._scope_id();
let bar = 0;
const {
foo,
fooChange: $fooChange
} = {
foo: bar,
fooChange: _._resume(function (v) {
bar = v;
}, "__tests__/template.marko_0/foo", $scope0_id)
};
_._html(`<button>${_._escape(foo)}${_._el_resume($scope0_id, "#text/1")}:<!>${_._escape(bar)}${_._el_resume($scope0_id, "#text/2")}</button>${_._el_resume($scope0_id, "#button/0")}`);
_._script($scope0_id, "__tests__/template.marko_0_foo_$fooChange");
_._scope($scope0_id, {
foo,
$fooChange
}, "__tests__/template.marko", 0, {
foo: "2:9",
$fooChange: "9:20"
});
_._resume_branch($scope0_id);
});

View File

@ -0,0 +1,39 @@
# Render
```html
<button>
0:0
</button>
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
1:1
</button>
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
2:2
</button>
```
# Render
```js
container.querySelector("button").click();
```
```html
<button>
3:3
</button>
```

View File

@ -0,0 +1,147 @@
# Render
```html
<html>
<head />
<body>
<button>
0
<!--M_*1 #text/1-->
:
<!---->
0
<!--M_*1 #text/2-->
</button>
<!--M_*1 #button/0-->
<script>
WALKER_RUNTIME("M")("_");
M._.r = [_ =&gt; (_.b = [0, _.a = {
foo: 0
}], _.a.$fooChange = _._[
"__tests__/template.marko_0/foo"
](_.a), _.b),
"__tests__/template.marko_0_foo_$fooChange",
1
];
M._.w()
</script>
</body>
</html>
```
# Render
```js
container.querySelector("button").click();
```
```html
<html>
<head />
<body>
<button>
1
<!--M_*1 #text/1-->
:
<!---->
1
<!--M_*1 #text/2-->
</button>
<!--M_*1 #button/0-->
<script>
WALKER_RUNTIME("M")("_");
M._.r = [_ =&gt; (_.b = [0, _.a = {
foo: 0
}], _.a.$fooChange = _._[
"__tests__/template.marko_0/foo"
](_.a), _.b),
"__tests__/template.marko_0_foo_$fooChange",
1
];
M._.w()
</script>
</body>
</html>
```
# Mutations
```
UPDATE html/body/button/#text2 "0" => "1"
UPDATE html/body/button/#text0 "0" => "1"
```
# Render
```js
container.querySelector("button").click();
```
```html
<html>
<head />
<body>
<button>
2
<!--M_*1 #text/1-->
:
<!---->
2
<!--M_*1 #text/2-->
</button>
<!--M_*1 #button/0-->
<script>
WALKER_RUNTIME("M")("_");
M._.r = [_ =&gt; (_.b = [0, _.a = {
foo: 0
}], _.a.$fooChange = _._[
"__tests__/template.marko_0/foo"
](_.a), _.b),
"__tests__/template.marko_0_foo_$fooChange",
1
];
M._.w()
</script>
</body>
</html>
```
# Mutations
```
UPDATE html/body/button/#text2 "1" => "2"
UPDATE html/body/button/#text0 "1" => "2"
```
# Render
```js
container.querySelector("button").click();
```
```html
<html>
<head />
<body>
<button>
3
<!--M_*1 #text/1-->
:
<!---->
3
<!--M_*1 #text/2-->
</button>
<!--M_*1 #button/0-->
<script>
WALKER_RUNTIME("M")("_");
M._.r = [_ =&gt; (_.b = [0, _.a = {
foo: 0
}], _.a.$fooChange = _._[
"__tests__/template.marko_0/foo"
](_.a), _.b),
"__tests__/template.marko_0_foo_$fooChange",
1
];
M._.w()
</script>
</body>
</html>
```
# Mutations
```
UPDATE html/body/button/#text2 "2" => "3"
UPDATE html/body/button/#text0 "2" => "3"
```

View File

@ -0,0 +1,6 @@
# Render End
```html
<button>
0:0
</button>
```

View File

@ -0,0 +1,51 @@
# Write
```html
<button>0<!--M_*1 #text/1-->:<!>0<!--M_*1 #text/2--></button><!--M_*1 #button/0--><script>WALKER_RUNTIME("M")("_");M._.r=[_=>(_.b=[0,_.a={foo:0}],_.a.$fooChange=_._["__tests__/template.marko_0/foo"](_.a),_.b),"__tests__/template.marko_0_foo_$fooChange",1];M._.w()</script>
```
# Render End
```html
<html>
<head />
<body>
<button>
0
<!--M_*1 #text/1-->
:
<!---->
0
<!--M_*1 #text/2-->
</button>
<!--M_*1 #button/0-->
<script>
WALKER_RUNTIME("M")("_");
M._.r = [_ =&gt; (_.b = [0, _.a = {
foo: 0
}], _.a.$fooChange = _._[
"__tests__/template.marko_0/foo"
](_.a), _.b),
"__tests__/template.marko_0_foo_$fooChange",
1
];
M._.w()
</script>
</body>
</html>
```
# Mutations
```
INSERT html
INSERT html/head
INSERT html/body
INSERT html/body/button
INSERT html/body/button/#text0
INSERT html/body/button/#comment0
INSERT html/body/button/#text1
INSERT html/body/button/#comment1
INSERT html/body/button/#text2
INSERT html/body/button/#comment2
INSERT html/body/#comment
INSERT html/body/script
INSERT html/body/script/#text
```

View File

@ -0,0 +1,10 @@
let/bar=0
const/{ foo }={
foo: bar,
fooChange(v) {
bar = v;
}
}
button onClick() { foo++ }
-- ${foo}:${bar}

View File

@ -0,0 +1,5 @@
export const steps = [{}, click, click, click];
function click(container: Element) {
container.querySelector("button")!.click();
}

View File

@ -1730,10 +1730,9 @@ function resolveReferencedBindings(
const rootBindings = getRootBindings(reads);
for (const read of reads) {
let { binding } = read;
if (read.node) {
const exprReference = ((read.node.extra ??= {}).read ??=
if (read.node && read.node.extra?.assignmentTo !== binding) {
({ binding } = (read.node.extra ??= {}).read ??=
resolveExpressionReference(rootBindings, binding));
({ binding } = (read.node.extra ??= {}).read = exprReference);
}
referencedBindings = bindingUtil.add(referencedBindings, binding);
}