From 2cddb7a6e56b171fb65c7e427e33893241fcb9cb Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 22 Sep 2022 16:38:39 +0800 Subject: [PATCH] Match-id-c129f1002ec86566362106c475c16f5a771e5763 --- .eslintrc.js | 1 + libs/horizon/src/dom/DOMOperator.ts | 2 - libs/horizon/src/dom/utils/Interface.ts | 2 +- .../src/dom/valueHandler/InputValueHandler.ts | 51 +++--------- .../dom/valueHandler/OptionValueHandler.ts | 8 +- .../dom/valueHandler/SelectValueHandler.ts | 6 +- .../dom/valueHandler/TextareaValueHandler.ts | 14 ++-- .../dom/valueHandler/ValueChangeHandler.ts | 3 +- libs/horizon/src/dom/valueHandler/index.ts | 8 +- libs/horizon/src/event/FormValueController.ts | 79 +++++++++++++++++++ libs/horizon/src/event/HorizonEventMain.ts | 26 +++--- 11 files changed, 125 insertions(+), 75 deletions(-) create mode 100644 libs/horizon/src/event/FormValueController.ts diff --git a/.eslintrc.js b/.eslintrc.js index 3f20b668..43a84822 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,6 +27,7 @@ module.exports = { }, rules: { '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-empty-function': 'off', semi: ['warn', 'always'], diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 4e6b9c2a..dfa668de 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -9,8 +9,6 @@ import { getSelectionInfo, resetSelectionRange, SelectionData } from './Selectio import { shouldAutoFocus } from './utils/Common'; import { NSS } from './utils/DomCreator'; import { adjustStyleValue } from './DOMPropertiesHandler/StyleHandler'; - -import { listenDelegatedEvents } from '../event/EventBinding'; import type { VNode } from '../renderer/Types'; import { setInitValue, diff --git a/libs/horizon/src/dom/utils/Interface.ts b/libs/horizon/src/dom/utils/Interface.ts index 2306d4b3..b6a901e3 100644 --- a/libs/horizon/src/dom/utils/Interface.ts +++ b/libs/horizon/src/dom/utils/Interface.ts @@ -1,4 +1,4 @@ -export interface IProperty { +export interface Props { [propName: string]: any; } diff --git a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts index 2ebe01a7..d9e9f8ef 100644 --- a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts @@ -1,11 +1,8 @@ import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp'; -import { IProperty } from '../utils/Interface'; -import { isInputElement } from '../utils/Common'; -import { getVNodeProps } from '../DOMInternalKeys'; -import { updateInputValueIfChanged } from './ValueChangeHandler'; +import { Props } from '../utils/Interface'; -function getInitValue(dom: HTMLInputElement, properties: IProperty) { - const { value, defaultValue, checked, defaultChecked } = properties; +function getInitValue(dom: HTMLInputElement, props: Props) { + const { value, defaultValue, checked, defaultChecked } = props; const defaultValueStr = defaultValue != null ? defaultValue : ''; const initValue = value != null ? value : defaultValueStr; @@ -14,15 +11,15 @@ function getInitValue(dom: HTMLInputElement, properties: IProperty) { return { initValue, initChecked }; } -export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) { +export function getInputPropsWithoutValue(dom: HTMLInputElement, props: Props) { // checked属于必填属性,无法置 - let {checked} = properties; + let {checked} = props; if (checked == null) { - checked = getInitValue(dom, properties).initChecked; + checked = getInitValue(dom, props).initChecked; } return { - ...properties, + ...props, value: undefined, defaultValue: undefined, defaultChecked: undefined, @@ -30,8 +27,8 @@ export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IPr }; } -export function updateInputValue(dom: HTMLInputElement, properties: IProperty) { - const {value, checked} = properties; +export function updateInputValue(dom: HTMLInputElement, props: Props) { + const {value, checked} = props; if (value != null) { // 处理 dom.value 逻辑 if (dom.value !== String(value)) { @@ -43,9 +40,9 @@ export function updateInputValue(dom: HTMLInputElement, properties: IProperty) { } // 设置input的初始值 -export function setInitInputValue(dom: HTMLInputElement, properties: IProperty) { - const {value, defaultValue} = properties; - const {initValue, initChecked} = getInitValue(dom, properties); +export function setInitInputValue(dom: HTMLInputElement, props: Props) { + const {value, defaultValue} = props; + const {initValue, initChecked} = getInitValue(dom, props); if (value != null || defaultValue != null) { // value 的使用优先级 value 属性 > defaultValue 属性 > 空字符串 @@ -59,27 +56,3 @@ export function setInitInputValue(dom: HTMLInputElement, properties: IProperty) // checked 的使用优先级 checked 属性 > defaultChecked 属性 > false dom.defaultChecked = Boolean(initChecked); } - -// 找出同一form内,name相同的Radio,更新它们Handler的Value -export function syncRadiosHandler(targetRadio: Element) { - if (isInputElement(targetRadio)) { - const props = getVNodeProps(targetRadio); - if (props) { - const { name, type } = props; - if (type === 'radio' && name != null) { - const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); - for (let i = 0; i < radioList.length; i++) { - const radio = radioList[i]; - if (radio === targetRadio) { - continue; - } - if (radio.form != null && targetRadio.form != null && radio.form !== targetRadio.form) { - continue; - } - - updateInputValueIfChanged(radio); - } - } - } - } -} diff --git a/libs/horizon/src/dom/valueHandler/OptionValueHandler.ts b/libs/horizon/src/dom/valueHandler/OptionValueHandler.ts index 12cc6e9a..3f867293 100644 --- a/libs/horizon/src/dom/valueHandler/OptionValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/OptionValueHandler.ts @@ -1,5 +1,5 @@ import { Children } from '../../external/ChildrenUtil'; -import { IProperty } from '../utils/Interface'; +import { Props } from '../utils/Interface'; // 把 const a = 'a'; 转成 giraffe function concatChildren(children) { @@ -11,11 +11,11 @@ function concatChildren(children) { return content; } -export function getOptionPropsWithoutValue(dom: Element, properties: IProperty) { - const content = concatChildren(properties.children); +export function getOptionPropsWithoutValue(dom: Element, props: Props) { + const content = concatChildren(props.children); return { - ...properties, + ...props, children: content || undefined, // 覆盖children }; } diff --git a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts index d7c5852f..c651432b 100644 --- a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts @@ -1,4 +1,4 @@ -import {HorizonSelect, IProperty} from '../utils/Interface'; +import {HorizonSelect, Props} from '../utils/Interface'; function updateMultipleValue(options, newValues) { const newValueSet = new Set(); @@ -46,8 +46,8 @@ export function getSelectPropsWithoutValue(dom: HorizonSelect, properties: Objec }; } -export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) { - const {value, defaultValue, multiple} = properties; +export function updateSelectValue(dom: HorizonSelect, props: Props, isInit: boolean = false) { + const {value, defaultValue, multiple} = props; const oldMultiple = dom._multiple !== undefined ? dom._multiple : dom.multiple; const newMultiple = Boolean(multiple); diff --git a/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts b/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts index d10e43d5..dd87c6ae 100644 --- a/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts @@ -1,12 +1,12 @@ -import {IProperty} from '../utils/Interface'; +import {Props} from '../utils/Interface'; // 值的优先级 value > children > defaultValue -function getInitValue(properties: IProperty) { - const {value} = properties; +function getInitValue(props: Props) { + const {value} = props; if (value == null) { - const {defaultValue, children} = properties; + const {defaultValue, children} = props; let initValue = defaultValue; // children content存在时,会覆盖defaultValue @@ -30,15 +30,15 @@ export function getTextareaPropsWithoutValue(dom: HTMLTextAreaElement, propertie }; } -export function updateTextareaValue(dom: HTMLTextAreaElement, properties: IProperty, isInit: boolean = false) { +export function updateTextareaValue(dom: HTMLTextAreaElement, props: Props, isInit: boolean = false) { if (isInit) { - const initValue = getInitValue(properties); + const initValue = getInitValue(props); if (initValue !== '') { dom.value = initValue; } } else { // 获取当前节点的 value 值 - let value = properties.value; + let value = props.value; if (value != null) { value = String(value); // 当且仅当值实际发生变化时才去设置节点的value值 diff --git a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts index 207e8c2e..134e980e 100644 --- a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts +++ b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts @@ -54,7 +54,8 @@ export function watchValueChange(dom) { } } -export function updateInputValueIfChanged(dom) { +// 更新input dom的handler 状态,返回是否更新 +export function updateInputHandlerIfChanged(dom) { const handler = dom[HANDLER_KEY]; if (!handler) { return true; diff --git a/libs/horizon/src/dom/valueHandler/index.ts b/libs/horizon/src/dom/valueHandler/index.ts index 5948a937..b716a27f 100644 --- a/libs/horizon/src/dom/valueHandler/index.ts +++ b/libs/horizon/src/dom/valueHandler/index.ts @@ -3,7 +3,7 @@ * 处理组件被代理和不被代理情况下的不同逻辑 */ -import {HorizonDom, HorizonSelect, IProperty} from '../utils/Interface'; +import {HorizonDom, HorizonSelect, Props} from '../utils/Interface'; import { getInputPropsWithoutValue, setInitInputValue, @@ -22,7 +22,7 @@ import { } from './TextareaValueHandler'; // 获取元素除了被代理的值以外的属性 -function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) { +function getPropsWithoutValue(type: string, dom: HorizonDom, properties: Props) { switch (type) { case 'input': return getInputPropsWithoutValue(dom, properties); @@ -38,7 +38,7 @@ function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProper } // 其它属性挂载完成后处理被代理值相关的属性 -function setInitValue(type: string, dom: HorizonDom, properties: IProperty) { +function setInitValue(type: string, dom: HorizonDom, properties: Props) { switch (type) { case 'input': setInitInputValue(dom, properties); @@ -55,7 +55,7 @@ function setInitValue(type: string, dom: HorizonDom, properties: IProperty) { } // 更新需要适配的属性 -function updateValue(type: string, dom: HorizonDom, properties: IProperty) { +function updateValue(type: string, dom: HorizonDom, properties: Props) { switch (type) { case 'input': updateInputValue(dom, properties); diff --git a/libs/horizon/src/event/FormValueController.ts b/libs/horizon/src/event/FormValueController.ts new file mode 100644 index 00000000..7b522ff5 --- /dev/null +++ b/libs/horizon/src/event/FormValueController.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import { getVNodeProps } from '../dom/DOMInternalKeys'; +import { getDomTag } from '../dom/utils/Common'; +import { Props } from '../dom/utils/Interface'; +import { updateTextareaValue } from '../dom/valueHandler/TextareaValueHandler'; +import { updateInputHandlerIfChanged } from '../dom/valueHandler/ValueChangeHandler'; +import { updateInputValue } from '../dom/valueHandler/InputValueHandler'; + +// 记录表单控件 input/textarea/select的onChange事件的targets +let changeEventTargets: Array | null = null; + +// 存储队列中缓存组件 +export function recordChangeEventTargets(target: EventTarget): void { + if (changeEventTargets) { + changeEventTargets.push(target); + } else { + changeEventTargets = [target]; + } +} + +// 判断是否需要控制value与props保持一致 +export function shouldControlValue(): boolean { + return changeEventTargets !== null && changeEventTargets.length > 0; +} + +// 从缓存队列中对受控组件进行赋值 +export function tryControlValue() { + if (!changeEventTargets) { + return; + } + changeEventTargets.forEach(target => { + controlValue(target); + }); + changeEventTargets = null; +} + +// 受控组件值重新赋值 +function controlValue(target: Element) { + const props = getVNodeProps(target); + if (props) { + const type = getDomTag(target); + switch (type) { + case 'input': + controlInputValue(target, props); + break; + case 'textarea': + updateTextareaValue(target, props); + break; + default: + break; + } + } +} + +function controlInputValue(inputDom: HTMLInputElement, props: Props) { + const { name, type } = props; + + // 如果是 radio,先更新相同 name 的 radio + if (type === 'radio' && name != null) { + const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); + for (let i = 0; i < radioList.length; i++) { + const radio = radioList[i]; + if (radio === inputDom) { + continue; + } + if (radio.form != null && inputDom.form != null && radio.form !== inputDom.form) { + continue; + } + + updateInputHandlerIfChanged(radio); + } + } else { + updateInputValue(inputDom, props); + } +} + diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index ebaadeb8..80dd9ddb 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -5,7 +5,6 @@ import { decorateNativeEvent } from './EventWrapper'; import { getListenersFromTree } from './ListenerGetter'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer'; import { findRoot } from '../renderer/vnode/VNodeUtils'; -import { syncRadiosHandler } from '../dom/valueHandler/InputValueHandler'; import { EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, @@ -14,8 +13,9 @@ import { transformToHorizonEvent, } from './EventHub'; import { getDomTag } from '../dom/utils/Common'; -import { updateInputValueIfChanged } from '../dom/valueHandler/ValueChangeHandler'; +import { updateInputHandlerIfChanged } from '../dom/valueHandler/ValueChangeHandler'; import { getDom } from '../dom/DOMInternalKeys'; +import { recordChangeEventTargets, shouldControlValue, tryControlValue } from './FormValueController'; // web规范,鼠标右键key值 const RIGHT_MOUSE_BUTTON = 2; @@ -34,11 +34,11 @@ function shouldTriggerChangeEvent(targetDom, evtName) { return evtName === 'change'; } else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) { if (evtName === 'click') { - return updateInputValueIfChanged(targetDom); + return updateInputHandlerIfChanged(targetDom); } } else if (isInputElement(targetDom)) { if (evtName === 'input' || evtName === 'change') { - return updateInputValueIfChanged(targetDom); + return updateInputHandlerIfChanged(targetDom); } } return false; @@ -52,6 +52,7 @@ function getChangeListeners( nativeEvtName: string, nativeEvt: AnyNativeEvent, vNode: null | VNode, + target: EventTarget ): ListenerUnitList { if (!vNode) { return []; @@ -60,6 +61,8 @@ function getChangeListeners( // 判断是否需要触发change事件 if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) { + recordChangeEventTargets(target); + const event = decorateNativeEvent( 'onChange', 'change', @@ -129,8 +132,7 @@ function triggerHorizonEvents( nativeEvent: AnyNativeEvent, vNode: VNode | null, ) { - const target = nativeEvent.target || nativeEvent.srcElement; - let hasTriggeredChangeEvent = false; + const target = nativeEvent.target || nativeEvent.srcElement!; // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( @@ -147,17 +149,15 @@ function triggerHorizonEvents( nativeEvtName, nativeEvent, vNode, + target ); if (changeListeners.length) { - hasTriggeredChangeEvent = true; listenerList = listenerList.concat(changeListeners); } } // 处理触发的事件队列 processListeners(listenerList); - - return hasTriggeredChangeEvent; } @@ -188,15 +188,13 @@ export function handleEventMain( // 没有事件在执行,经过调度再执行事件 isInEventsExecution = true; - let hasTriggeredChangeEvent = false; try { - hasTriggeredChangeEvent = asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); + asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); } finally { isInEventsExecution = false; - if (hasTriggeredChangeEvent) { + if (shouldControlValue()) { runDiscreteUpdates(); - // 若是Radio,同步同组其他Radio的Handler Value - syncRadiosHandler(nativeEvent.target as Element); + tryControlValue(); } } }