From 19ac35b676b8af667257beda93553eb92bd43777 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 16 Sep 2022 18:28:44 +0200 Subject: [PATCH] Teact: Fix fragment breaking other elements order --- src/components/test/TestFragment.tsx | 35 +++++++++++++ src/lib/teact/teact-dom.ts | 77 +++++++++++++++++++--------- src/lib/teact/teact.ts | 38 ++++++++++---- 3 files changed, 115 insertions(+), 35 deletions(-) create mode 100644 src/components/test/TestFragment.tsx diff --git a/src/components/test/TestFragment.tsx b/src/components/test/TestFragment.tsx new file mode 100644 index 00000000..9c73dad0 --- /dev/null +++ b/src/components/test/TestFragment.tsx @@ -0,0 +1,35 @@ +import React, { useRef, useState } from '../../lib/teact/teact'; + +export function App() { + const [trigger, setTrigger] = useState(false); + + return ( +
{ + setTrigger((current) => !current); + }} + > +

Click to update

+ {trigger ? ( + <> + fragment + content + + ) : undefined} + +
+ ); +} + +function Child() { + const idRef = useRef(String(Math.random()).slice(-4)); + + return ( +
+ This number should never change: {idRef.current} +
+ ); +} + +export default App; diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index df6c1f6c..205852a3 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -5,6 +5,7 @@ import type { VirtualElementParent, VirtualElementChildren, VirtualElementReal, + VirtualElementFragment, } from './teact'; import { hasElementChanged, @@ -16,6 +17,7 @@ import { mountComponent, renderComponent, unmountComponent, + isFragmentElement, } from './teact'; import generateIdFor from '../../util/generateIdFor'; import { DEBUG } from '../../config'; @@ -80,6 +82,9 @@ function renderWithVirtual( const isNewComponent = $new && isComponentElement($new); const $newAsReal = $new as VirtualElementReal; + const isCurrentFragment = $current && !isCurrentComponent && isFragmentElement($current); + const isNewFragment = $new && !isNewComponent && isFragmentElement($new); + if ( !skipComponentUpdate && isCurrentComponent && isNewComponent @@ -105,9 +110,12 @@ function renderWithVirtual( } if (!$current && $new) { - if (isNewComponent) { - $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new; - mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment }); + if (isNewComponent || isNewFragment) { + if (isNewComponent) { + $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new; + } + + mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); } else { const node = createNode($newAsReal); $newAsReal.target = node; @@ -121,10 +129,13 @@ function renderWithVirtual( nextSibling = getNextSibling($current); } - if (isNewComponent) { - $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new; + if (isNewComponent || isNewFragment) { + if (isNewComponent) { + $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new; + } + remount(parentEl, $current, undefined); - mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment }); + mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); } else { const node = createNode($newAsReal); $newAsReal.target = node; @@ -132,10 +143,12 @@ function renderWithVirtual( } } else { const isComponent = isCurrentComponent && isNewComponent; - if (isComponent) { - ($new as VirtualElementComponent).children = renderChildren( + const isFragment = isCurrentFragment && isNewFragment; + + if (isComponent || isFragment) { + ($new as VirtualElementComponent | VirtualElementFragment).children = renderChildren( $current, - $new as VirtualElementComponent, + $new as VirtualElementComponent | VirtualElementFragment, parentEl, nextSibling, ); @@ -220,16 +233,20 @@ function setupComponentUpdateListener( }; } -function mountComponentChildren(parentEl: HTMLElement, $element: VirtualElementComponent, options: { - nextSibling?: ChildNode; - fragment?: DocumentFragment; -}) { +function mountChildren( + parentEl: HTMLElement, + $element: VirtualElementComponent | VirtualElementFragment, + options: { + nextSibling?: ChildNode; + fragment?: DocumentFragment; + }, +) { $element.children = $element.children.map(($child, i) => { return renderWithVirtual(parentEl, undefined, $child, $element, i, options); }); } -function unmountComponentChildren(parentEl: HTMLElement, $element: VirtualElementComponent) { +function unmountChildren(parentEl: HTMLElement, $element: VirtualElementComponent | VirtualElementFragment) { $element.children.forEach(($child) => { renderWithVirtual(parentEl, $child, undefined, $element, -1); }); @@ -274,9 +291,15 @@ function remount( node: Node | undefined, componentNextSibling?: ChildNode, ) { - if (isComponentElement($current)) { - unmountComponent($current.componentInstance); - unmountComponentChildren(parentEl, $current); + const isComponent = isComponentElement($current); + const isFragment = !isComponent && isFragmentElement($current); + + if (isComponent || isFragment) { + if (isComponent) { + unmountComponent($current.componentInstance); + } + + unmountChildren(parentEl, $current); if (node) { insertBefore(parentEl, node, componentNextSibling); @@ -327,7 +350,7 @@ function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, next } function getNextSibling($current: VirtualElement): ChildNode | undefined { - if (isComponentElement($current)) { + if (isComponentElement($current) || isFragmentElement($current)) { const lastChild = $current.children[$current.children.length - 1]; return getNextSibling(lastChild); } @@ -344,7 +367,7 @@ function renderChildren( DEBUG_checkKeyUniqueness($new.children); } - if ($new.props.teactFastList) { + if (('props' in $new) && $new.props.teactFastList) { return renderFastListChildren($current, $new, currentEl); } @@ -388,10 +411,16 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle $new.children.map(($newChild) => { const key = 'props' in $newChild && $newChild.props.key; - // eslint-disable-next-line no-null/no-null - if (DEBUG && isParentElement($newChild) && (key === undefined || key === null)) { - // eslint-disable-next-line no-console - console.warn('Missing `key` in `teactFastList`'); + if (DEBUG && isParentElement($newChild)) { + // eslint-disable-next-line no-null/no-null + if (key === undefined || key === null) { + // eslint-disable-next-line no-console + console.warn('Missing `key` in `teactFastList`'); + } + + if (isFragmentElement($newChild)) { + throw new Error('[Teact] Fragment can not be child of container with `teactFastList`'); + } } return key; @@ -555,7 +584,7 @@ function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) { } } -function updateAttributes($current: VirtualElementParent, $new: VirtualElementParent, element: HTMLElement) { +function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: HTMLElement) { processControlled(element.tagName, $new.props); const currentEntries = Object.entries($current.props); diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 60a60e33..be26e750 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -19,6 +19,7 @@ export enum VirtualElementTypesEnum { Text, Tag, Component, + Fragment, } interface VirtualElementEmpty { @@ -47,6 +48,12 @@ export interface VirtualElementComponent { children: VirtualElementChildren; } +export interface VirtualElementFragment { + type: VirtualElementTypesEnum.Fragment; + target?: Node; + children: VirtualElementChildren; +} + export type StateHookSetter = (newValue: ((current: T) => T) | T) => void; interface ComponentInstance { @@ -96,12 +103,14 @@ export type VirtualElement = VirtualElementEmpty | VirtualElementText | VirtualElementTag - | VirtualElementComponent; + | VirtualElementComponent + | VirtualElementFragment; export type VirtualElementParent = VirtualElementTag - | VirtualElementComponent; + | VirtualElementComponent + | VirtualElementFragment; export type VirtualElementChildren = VirtualElement[]; -export type VirtualElementReal = Exclude; +export type VirtualElementReal = Exclude; // Compatibility with JSX types export type TeactNode = @@ -135,8 +144,12 @@ export function isComponentElement($element: VirtualElement): $element is Virtua return $element.type === VirtualElementTypesEnum.Component; } +export function isFragmentElement($element: VirtualElement): $element is VirtualElementFragment { + return $element.type === VirtualElementTypesEnum.Fragment; +} + export function isParentElement($element: VirtualElement): $element is VirtualElementParent { - return isTagElement($element) || isComponentElement($element); + return isTagElement($element) || isComponentElement($element) || isFragmentElement($element); } function createElement( @@ -144,21 +157,24 @@ function createElement( props: Props, ...children: any[] ): VirtualElementParent | VirtualElementChildren { - if (!props) { - props = {}; - } - children = children.flat(); if (source === Fragment) { - return children; + return buildFragmentElement(children); } else if (typeof source === 'function') { - return createComponentInstance(source, props, children); + return createComponentInstance(source, props || {}, children); } else { - return buildTagElement(source, props, children); + return buildTagElement(source, props || {}, children); } } +function buildFragmentElement(children: any[]): VirtualElementFragment { + return { + type: VirtualElementTypesEnum.Fragment, + children: dropEmptyTail(children).map(buildChildElement), + }; +} + function createComponentInstance(Component: FC, props: Props, children: any[]): VirtualElementComponent { let parsedChildren: any | any[] | undefined; if (children.length === 0) {