bigskysoftware / bigskysoftware/htmx

[4.0.0-beta2] Q: hx-alpine-compat.js & propagation of Alpine data/reactivity during morph swaps

Open
#3,791 2 comments 0 reactions 0 assignees View on GitHub
htmx 4
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.