diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index f3054261..0baa43c2 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -86,7 +86,7 @@ function removeRootEventLister(container: Container) { // 卸载入口 function destroy(container: Container): boolean { - if (container._treeRoot) { + if (container && container._treeRoot) { syncUpdates(() => { executeRender(null, container, () => { removeRootEventLister(container); diff --git a/libs/horizon/src/renderer/ContextSaver.ts b/libs/horizon/src/renderer/ContextSaver.ts index 2e72ce3c..e0ea8432 100644 --- a/libs/horizon/src/renderer/ContextSaver.ts +++ b/libs/horizon/src/renderer/ContextSaver.ts @@ -7,7 +7,6 @@ import type { VNode, ContextType } from './Types'; import type { Container } from '../dom/DOMOperator'; import { getNSCtx } from '../dom/DOMOperator'; -import { ContextProvider } from './vnode/VNodeTags'; // 保存的是“http://www.w3.org/1999/xhtml”或“http://www.w3.org/2000/svg”, // 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM @@ -44,32 +43,3 @@ export function resetContext(providerVNode: VNode) { context.value = providerVNode.context; } - -// 在局部更新时,从上到下恢复父节点的context -export function recoverParentContext(vNode: VNode) { - const contextProviders: VNode[] = []; - let parent = vNode.parent; - while (parent !== null) { - if (parent.tag === ContextProvider) { - contextProviders.unshift(parent); - } - parent = parent.parent; - } - contextProviders.forEach(node => { - setContext(node, node.props.value); - }); -} - -// 在局部更新时,从下到上重置父节点的context -export function resetParentContext(vNode: VNode) { - let parent = vNode.parent; - - while (parent !== null) { - if (parent.tag === ContextProvider) { - resetContext(parent); - } - parent = parent.parent; - } -} - - diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 2622bc4a..d9a10bd7 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -2,7 +2,7 @@ import type { VNode } from './Types'; import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue'; import { updateVNode } from './vnode/VNodeCreator'; -import { DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags'; +import { ContextProvider, DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags'; import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags'; import { captureVNode } from './render/BaseComponent'; import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit'; @@ -30,7 +30,12 @@ import { isExecuting, setExecuteMode, } from './ExecuteMode'; -import { recoverParentContext, resetNamespaceCtx, resetParentContext, setNamespaceCtx } from './ContextSaver'; +import { + resetContext, + resetNamespaceCtx, + setContext, + setNamespaceCtx, +} from './ContextSaver'; import { updateChildShouldUpdate, updateParentsChildShouldUpdate, @@ -244,7 +249,7 @@ function buildVNodeTree(treeRoot: VNode) { } // 恢复父节点的context - recoverParentContext(startVNode); + recoverTreeContext(startVNode); } // 重置环境变量,为重新进行深度遍历做准备 @@ -272,7 +277,7 @@ function buildVNodeTree(treeRoot: VNode) { } if (startVNode.tag !== TreeRoot) { // 不是根节点 // 恢复父节点的context - resetParentContext(startVNode); + resetTreeContext(startVNode); } setProcessingClassVNode(null); @@ -280,6 +285,39 @@ function buildVNodeTree(treeRoot: VNode) { setExecuteMode(preMode); } +// 在局部更新时,从上到下恢复父节点的context和PortalStack +function recoverTreeContext(vNode: VNode) { + const contextProviders: VNode[] = []; + let parent = vNode.parent; + while (parent !== null) { + if (parent.tag === ContextProvider) { + contextProviders.unshift(parent); + } + if(parent.tag === DomPortal){ + pushCurrentRoot(parent); + } + parent = parent.parent; + } + contextProviders.forEach(node => { + setContext(node, node.props.value); + }); +} + +// 在局部更新时,从下到上重置父节点的context +function resetTreeContext(vNode: VNode) { + let parent = vNode.parent; + + while (parent !== null) { + if (parent.tag === ContextProvider) { + resetContext(parent); + } + if(parent.tag === DomPortal){ + popCurrentRoot(); + } + parent = parent.parent; + } +} + // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index e027696c..34ba6b6c 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -7,9 +7,10 @@ import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator'; import componentRenders from './index'; import { setProcessingVNode } from '../GlobalVar'; import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; +import { pushCurrentRoot } from '../RootStack'; -// 复用vNode时,也需对stack进行处理 -function handlerContext(processing: VNode) { +// 复用vNode时,也需对树的上下文值处理,如context,portal, namespaceContext +function setTreeContextValue(processing: VNode) { switch (processing.tag) { case TreeRoot: setNamespaceCtx(processing, processing.realNode); @@ -19,6 +20,7 @@ function handlerContext(processing: VNode) { break; case DomPortal: setNamespaceCtx(processing, processing.realNode); + pushCurrentRoot(processing); break; case ContextProvider: { const newValue = processing.props.value; @@ -36,7 +38,7 @@ export function captureVNode(processing: VNode): VNode | null { // 该vNode没有变化,不用进入capture,直接复用。 if (!processing.isCreated && processing.oldProps === processing.props && !processing.shouldUpdate) { // 复用还需对stack进行处理 - handlerContext(processing); + setTreeContextValue(processing); return onlyUpdateChildVNodes(processing); } diff --git a/scripts/__tests__/ComponentTest/PortalComponent.test.js b/scripts/__tests__/ComponentTest/PortalComponent.test.js index 05908f18..2dcadc38 100755 --- a/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -1,5 +1,6 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; import { getLogUtils } from '../jest/testUtils'; +import dispatchChangeEvent from '../utils/dispatchChangeEvent'; describe('PortalComponent Test', () => { const LogUtils = getLogUtils(); @@ -14,12 +15,10 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); //
PortalApp
被渲染到了portalRoot而非container @@ -43,17 +42,12 @@ describe('PortalComponent Test', () => { render() { return [ - Horizon.createPortal( - this.props.child, - this.element, - ), - Horizon.createPortal( - this.props.child, - this.newElement, - ) + Horizon.createPortal(this.props.child, this.element), + Horizon.createPortal(this.props.child, this.newElement), ]; } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); //
PortalApp
被渲染到了portalRoot而非container @@ -82,21 +76,16 @@ describe('PortalComponent Test', () => { render() { return [
PortalApp1st
, - Horizon.createPortal([ -
PortalApp4
, - Horizon.createPortal( - this.props.child, - this.element3rd, - ), - ], this.element), -
PortalApp2nd
, Horizon.createPortal( - this.props.child, - this.newElement, - ) + [
PortalApp4
, Horizon.createPortal(this.props.child, this.element3rd)], + this.element + ), +
PortalApp2nd
, + Horizon.createPortal(this.props.child, this.newElement), ]; } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe('PortalApp1stPortalApp2nd'); //
PortalApp4
会挂载在this.element上 @@ -120,25 +109,23 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } - Horizon.render(PortalApp} />, container); + + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('PortalApp'); - Horizon.render(AppPortal} />, container); + Horizon.render(AppPortal} />, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('AppPortal'); - Horizon.render(, container); + Horizon.render(, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('portal'); - Horizon.render(, container); + Horizon.render(, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe(''); @@ -158,10 +145,7 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } @@ -173,7 +157,6 @@ describe('PortalComponent Test', () => { ); }; - const App = () => { const handleClick = () => { LogUtils.log('bubble click event'); @@ -185,9 +168,7 @@ describe('PortalComponent Test', () => { return (
- }> - - + }>
); }; @@ -199,24 +180,23 @@ describe('PortalComponent Test', () => { expect(LogUtils.getAndClear()).toEqual([ // 从外到内先捕获再冒泡 'capture click event', - 'bubble click event' + 'bubble click event', ]); }); it('Create portal at app root should not add event listener multiple times', () => { - const btnRef = Horizon.createRef(); + const btnRef = Horizon.createRef(); + class PortalApp extends Horizon.Component { constructor(props) { super(props); } render() { - return Horizon.createPortal( - this.props.child, - container, - ); + return Horizon.createPortal(this.props.child, container); } } + const onClick = jest.fn(); class App extends Horizon.Component { @@ -225,14 +205,70 @@ describe('PortalComponent Test', () => { } render() { - return
- - -
; + return ( +
+ + +
+ ); } } + Horizon.render(, container); btnRef.current.click(); expect(onClick).toHaveBeenCalledTimes(1); }); + + it('#76 Portal onChange should activate', () => { + class Dialog extends Horizon.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } + + render() { + return Horizon.createPortal(this.props.children, this.node); + } + } + + let showPortalInput; + const fn = jest.fn(); + const inputRef = Horizon.createRef(); + + function App() { + const Input = () => { + const [show, setShow] = Horizon.useState(false); + showPortalInput = setShow; + + Horizon.useEffect(() => { + setTimeout(() => { + setShow(true); + }, 0); + }, []); + + if (!show) { + return null; + } + + return ; + }; + + return ( +
+ + + +
+ ); + } + + Horizon.render(, container); + showPortalInput(true); + jest.advanceTimersToNextTimer(); + dispatchChangeEvent(inputRef.current, 'test'); + expect(fn).toHaveBeenCalledTimes(1); + }); }); diff --git a/scripts/__tests__/utils/dispatchChangeEvent.js b/scripts/__tests__/utils/dispatchChangeEvent.js new file mode 100644 index 00000000..1b8bbd04 --- /dev/null +++ b/scripts/__tests__/utils/dispatchChangeEvent.js @@ -0,0 +1,9 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ +export default function dispatchChangeEvent(inputEle, value) { + const nativeInputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + nativeInputSetter.call(inputEle, value); + + inputEle.dispatchEvent(new Event('input', { bubbles: true})); +}