Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/plugins/fragment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ multiple sibling elements for each iteration without wrapping them:
```
🚀 [Live demo | Alpine.js x-fragment: Multiple root elements with x-for](https://jsfiddle.net/rameel/201rmntc/)

> [!NOTE]
> Alpine's `x-for` does not keep in order the content rendered by templates nested in its items,
> such as `x-for` directly in `x-for` or a template directly in `x-if`
> ([alpinejs/alpine#4911](https://github.com/alpinejs/alpine/issues/4911)).
> Inside `x-for`, the same applies to `x-if`, `x-for` or `x-fragment` placed directly in `x-fragment`,
> and to `x-fragment` placed directly in `x-if`. Put the nested templates inside an element,
> and use `x-when` instead of `x-if` with `x-fragment`:
> ```html
> <template x-for="group in groups" :key="group.id">
> <template x-fragment>
> <h2 x-text="group.name"></h2>
> <ul>
> <template x-for="item in group.items" :key="item.id">
> <li x-text="item.name"></li>
> </template>
> </ul>
> </template>
> </template>
> ```

## Source code
You can find the source code for this plugin on GitHub:

Expand Down
8 changes: 8 additions & 0 deletions src/plugins/match/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ Here's an example demonstrating the use of `<template>` with `x-case` to render
In this example, the `x-match` directive with `<template>` tags allows rendering multiple root elements
(e.g., `<h2>`, `<p>`, `<a>`, and `<button>`) without needing an extra wrapper like a `<div>`.

> [!NOTE]
> Alpine's `x-for` does not keep in order the content rendered by templates nested in its items,
> such as `x-for` directly in `x-for` or a template directly in `x-if`
> ([alpinejs/alpine#4911](https://github.com/alpinejs/alpine/issues/4911)).
> Inside `x-for`, the same applies to `x-if`, `x-for` or `x-match` placed directly in an `x-case`
> or `x-default` template, and to `x-match` placed directly in `x-if`. Put the nested templates
> inside an element instead.

## Source code
You can find the source code for this plugin on GitHub:

Expand Down
7 changes: 7 additions & 0 deletions src/plugins/when/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ The `x-when` directive can also be used with the directive `x-for` to conditiona
```
🚀 [Live demo | Alpine.js x-when: Multiple root elements with x-for](https://jsfiddle.net/rameel/cuoh3297/)

> [!NOTE]
> Alpine's `x-for` does not keep in order the content rendered by templates nested in its items,
> such as `x-for` directly in `x-for` or a template directly in `x-if`
> ([alpinejs/alpine#4911](https://github.com/alpinejs/alpine/issues/4911)).
> Inside `x-for`, the same applies to `x-if`, `x-for` or `x-when` placed directly in `x-when`,
> and to `x-when` placed directly in `x-if`. Put the nested templates inside an element instead.

## Source code
You can find the source code for this plugin on GitHub:

Expand Down
102 changes: 84 additions & 18 deletions src/utilities/anchor_block.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,112 @@ export function anchor_block(el, template, { addScopeToNode, cleanup, destroyTre
? [...template.content.cloneNode(true).childNodes]
: [template.cloneNode(true)];

mutateDom(() => {
for (let node of nodes) {
is_element(node) && addScopeToNode(node, scope, el);
el.parentElement.insertBefore(node, el);
is_element(node) && initTree(node);
}
// Like x-if, the content is rendered after the template,
// and the template exposes the last rendered node
const end = () => nodes.findLast(node => node.parentNode);

Object.defineProperties(el, {
// x-for before alpinejs/alpine#4912 moves an x-if item together with
// its single rendered element and continues after it. Pretending to be x-if
// makes it move the last node of the block, and the observer brings
// the rest back. Not needed once the minimum supported Alpine includes the fix.
_x_currentIfEl: { configurable: true, get: end },

// x-for with alpinejs/alpine#4912 follows it to move the whole block,
// morph to skip the content
_x_lastRenderedEl: { configurable: true, get: end }
});

el._r_block = {
template,
update() {
mutateDom(() => {
for (let node of nodes ?? []) {
el.parentElement.insertBefore(node, el);
}
})
// Do not bring back nodes that were removed rather than moved
const attached = nodes.filter(node => node.parentNode);

// While the nodes keep their order, the block is left as is, along with
// the content rendered between them by a nested x-if, x-for or block
if (el.parentNode && !in_order(el, attached)) {
mutateDom(() => {
let prev = el;
for (let node of attached) {
prev.nextSibling === node || prev.after(node);
prev = node;
}
});
}
},
delete() {
// Not in mutateDom: Alpine before 3.14.2 removes nested x-if content
// and x-for items without destroying them, leaving that to its observer
el._r_block = null;
mutateDom(() => {
for (let node of nodes ?? []) {
is_element(node) && destroyTree(node);
node.remove();
}
});
delete el._x_currentIfEl;
delete el._x_lastRenderedEl;

for (let node of nodes) {
node._r_anchor = null;
is_element(node) && destroyTree(node);
node.remove();
}

nodes = null;
}
}

mutateDom(() => {
for (let node of nodes) {
node._r_anchor = el;
is_element(node) && addScopeToNode(node, scope, el);
}

el.after(...nodes);

for (let node of nodes) {
is_element(node) && initTree(node);
}
});

cleanup(() => el._r_block?.delete());
}

// Checks that the nodes follow the anchor in the same parent, in their order
function in_order(anchor, nodes) {
let prev = anchor;

for (let node of nodes) {
if (node.parentNode !== anchor.parentNode
|| !(prev.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING)) {
return false;
}

prev = node;
}

return true;
}

function initialize() {
document.body._r_block ??= (() => {
const observer = new MutationObserver(mutations => {
const anchors = new Set();

// x-for moves the template and the last node of a block, and may swap
// its other nodes with the ones of other items. Only the nodes of the block
// itself are restored: like Alpine with nested templates in x-for items
// (alpinejs/alpine#4911), the content rendered for a nested x-if, x-for
// or block at the top level of the block is not kept in order.
for (let mutation of mutations) {
for (let node of mutation.addedNodes) {
node._r_block?.update();
anchors.add(node._r_block ? node : node._r_anchor);
}
}

for (let anchor of anchors) {
anchor?._r_block?.update();
}

// Restoring only puts the existing nodes back in place,
// so the records it produces need no further handling
observer.takeRecords();
});

observer.observe(document.body, { childList: true, subtree: true });
Expand Down
Loading
Loading