bigskysoftware / bigskysoftware/htmx
[4.0.0-beta2] Q: hx-alpine-compat.js & propagation of Alpine data/reactivity during morph swaps
- Dominant language
- JavaScript
- Stars
- 49.4k
- Forks
- 1.7k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 30
Description
Hello, this a question & idea about enhancing preservation & inheritance of Alpine state during morph operations, specifically about propagating morphed/updated state. Fully aware that this could/should be posted to the Alpine github instead.
## Situation
Working with Alpine and morph swaps, I've noticed that changes to state (`_x_dataStack`) on the target elt work correctly, but that the changes aren't reflected in each `_x_dataStack` or bindings for descendant nodes.
E.g., For sake of simplicity, we focus on one`disabled` x-data variable (there could be more variables that need such inheritance):
```
```
If `#node` is swapped with `x-data="{ disabled: true, ...}"`, the `_x_dataStack` and any directives referencing it on `#node` react & evaluate correctly based on the updated `true` value, but not for descendants`#node-child-1`, `#node-child-2`, `#node-grandchild-1`, etc.
Inspecting datastacks, we see for `#node`:
```
_x_dataStack: Array(n)
0: Proxy(Object) { disabled: true, ... }
...
n: Proxy(Object) { ... }
```
but for `#node-child-2` we see `disabled` still `false` on inherited scope:
```
_x_dataStack: Array(n+1)
0: Proxy(Object) { ... } <-- local scope
1: Proxy(Object) { disabled: false, ... } <-- inherited scope from #node
...
n+1: Proxy(Object) { ... }
```
and similarly for `#node-grandchild-1`:
```
_x_dataStack: Array(n+2)
0: Proxy(Object) { ... } <-- local scope
0: Proxy(Object) { ... } <-- #node-child-2 scope
1: Proxy(Object) { disabled: false, ... } <-- inherited scope from #node
...
n+2: Proxy(Object) { ... }
```
## Question
When Alpine morphs a parent node and updates its `x-data`, descendants appear to retain stale inherited references in their `_x_dataStack`.
Is this expected?
If so, should morphing also propagate updated inherited scope references through descendant `_x_dataStacks` so bindings re-evaluate against the new parent state?
## Idea
Post-morph, recursively walk descendant nodes with `_x_dataStack` and, for each `x-data` property updated on the morphed parent, locate the first inherited occurrence in descendant data stacks and replace it with the updated value so descendant bindings re-evaluate against the new parent state.
Below is a quick & crude implementation on `hx-alpine-compat.js`. Does not cover teleported nodes.
To preface, I needed to add brief timeout in order for Alpine to evaluate and produce updated values. Hence, could open issue there.
```js
(() => {
let api;
let deferCount = 0;
function propagateStack(detail) {
let target = detail.ctx.target;
let stack = target?._x_dataStack;
if (!stack || !stack.length) return;
function propagate(elt) {
for (let child of elt.children) {
if (child._x_dataStack) {
const stackDiff = child._x_dataStack.length - stack.length;
const parentProxy = child._x_dataStack[stackDiff];
if (parentProxy) {
for (const key of Object.keys(stack[0])) {
if (Object.prototype.hasOwnProperty.call(parentProxy, key)) {
parentProxy[key] = stack[0][key];
}
}
}
}
propagate(child);
}
}
propagate(target)
}
function maybeFlush(detail) {
if (deferCount > 0) deferCount--;
if (deferCount === 0 && window.Alpine?.flushAndStopDeferringMutations) {
window.Alpine.flushAndStopDeferringMutations();
setTimeout(() => propagateStack(detail), 1);
}
}
htmx.registerExtension('alpine-compat', {
init: (internalAPI) => {
...
}
...
htmx_after_swap: (elt, detail) => {
detail.ctx._alpineFlushed = true;
maybeFlush(detail);
},
htmx_finally_request: (elt, detail) => {
if (!detail.ctx._alpineFlushed) maybeFlush(detail);
}
});
})();
```
Thoughts? Thank you!!!
Contributor guide
Assessment
This issue has not been assessed yet.