Teact: Fix empty fragment mount position (#5918)

This commit is contained in:
zubiden 2025-05-14 19:02:17 +03:00 committed by Alexander Zinchuk
parent b67b8ba533
commit f21ca52337
4 changed files with 60 additions and 9 deletions

View File

@ -69,7 +69,7 @@ const AccountMenuItems = ({
}, [accounts, currentCount, totalLimit]);
return (
<div>
<>
{Object.entries(accounts || {})
.sort(([, account]) => (account.userId === currentUser.id ? -1 : 1))
.map(([slot, account], index, arr) => {
@ -116,7 +116,7 @@ const AccountMenuItems = ({
{lang('MenuAddAccount')}
</MenuItem>
)}
</div>
</>
);
};

View File

@ -11,12 +11,7 @@ export function App() {
}}
>
<h2>Click to update</h2>
{true && (
<>
{trigger && <span>fragment</span>}
{trigger && <span>content</span>}
</>
)}
<FragmentContainer items={trigger ? [1, 2, 3] : []} />
<div>
This should always go last.
</div>
@ -24,4 +19,12 @@ export function App() {
);
}
function FragmentContainer({ items }: { items: number[] }) {
return (
<>
{items.map((n) => <div>{n}</div>)}
</>
);
}
export default App;

View File

@ -303,6 +303,15 @@ function mountChildren(
},
) {
const { children } = $element;
// Add a placeholder comment node for empty fragments to maintain position
if ($element.type === VirtualType.Fragment && children.length === 0) {
const fragmentEl = $element as VirtualElementFragment;
fragmentEl.placeholderTarget = document.createComment('empty-fragment');
insertBefore(options.fragment || parentEl, fragmentEl.placeholderTarget, options.nextSibling);
return;
}
for (let i = 0, l = children.length; i < l; i++) {
const $child = children[i];
const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $element, currentContext, i, options);
@ -390,7 +399,14 @@ function remount(
function unmountRealTree($element: VirtualElement) {
if ($element.type === VirtualType.Component) {
unmountComponent($element.componentInstance);
} else if ($element.type !== VirtualType.Fragment) {
} else if ($element.type === VirtualType.Fragment) {
// Remove placeholder for empty fragments
const fragment = $element as VirtualElementFragment;
if (fragment.placeholderTarget && fragment.children.length === 0) {
fragment.placeholderTarget.parentNode?.removeChild(fragment.placeholderTarget);
fragment.placeholderTarget = undefined;
}
} else {
if ($element.type === VirtualType.Tag) {
extraClasses.delete($element.target!);
setElementRef($element, undefined);
@ -419,6 +435,15 @@ function insertBefore(parentEl: DOMElement | DocumentFragment, node: Node, nextS
function getNextSibling($current: VirtualElement): ChildNode | undefined {
if ($current.type === VirtualType.Component || $current.type === VirtualType.Fragment) {
if ($current.children.length === 0) {
// For empty fragments, use the placeholder node to track position
const fragment = $current as VirtualElementFragment;
if (fragment.placeholderTarget) {
return fragment.placeholderTarget.nextSibling || undefined;
}
return undefined;
}
const lastChild = $current.children[$current.children.length - 1];
return getNextSibling(lastChild);
}
@ -444,6 +469,28 @@ function renderChildren(
return;
}
// Handle transitions between empty and non-empty fragments
if ($current.type === VirtualType.Fragment && $new.type === VirtualType.Fragment) {
const currentFragment = $current as VirtualElementFragment;
const newFragment = $new as VirtualElementFragment;
// If transitioning from empty to non-empty, use the placeholder's position
if (currentFragment.children.length === 0 && newFragment.children.length > 0 && currentFragment.placeholderTarget) {
nextSibling = currentFragment.placeholderTarget.nextSibling || undefined;
// Remove the placeholder as we're adding real content
currentFragment.placeholderTarget.parentNode?.removeChild(currentFragment.placeholderTarget);
currentFragment.placeholderTarget = undefined;
}
// If transitioning from non-empty to empty, add a placeholder
if (currentFragment.children.length > 0 && newFragment.children.length === 0) {
const lastCurrentChild = currentFragment.children[currentFragment.children.length - 1];
const siblingAfterFragment = getNextSibling(lastCurrentChild);
newFragment.placeholderTarget = document.createComment('empty-fragment');
insertBefore(currentEl, newFragment.placeholderTarget, siblingAfterFragment);
}
}
const currentChildren = $current.children;
const newChildren = $new.children;

View File

@ -56,6 +56,7 @@ export interface VirtualElementComponent {
export interface VirtualElementFragment {
type: VirtualType.Fragment;
children: VirtualElementChildren;
placeholderTarget?: Comment;
}
export type StateHookSetter<T> = (newValue: ((current: T) => T) | T) => void;