@tarojs/components
Version:
180 lines (177 loc) • 8.23 kB
JavaScript
import { flushSync, unstable_batchedUpdates } from 'react-dom';
import { camelToDashCase } from './case.js';
/**
* Modify from https://github.com/ionic-team/stencil-ds-output-targets/blob/main/packages/react-output-target/react-component-lib/utils/attachProps.ts
* MIT License https://github.com/ionic-team/stencil-ds-output-targets/blob/main/LICENSE
*/
const arrayToMap = (arr) => {
const map = new Map();
arr.forEach((s) => map.set(s, s));
return map;
};
const getClassName = (classList, newProps, oldProps) => {
const newClassProp = newProps.className || newProps.class;
const oldClassProp = oldProps.className || oldProps.class;
// map the classes to Maps for performance
const currentClasses = arrayToMap(classList);
const incomingPropClasses = arrayToMap(newClassProp ? newClassProp.split(' ') : []);
const oldPropClasses = arrayToMap(oldClassProp ? oldClassProp.split(' ') : []);
const finalClassNames = [];
// loop through each of the current classes on the component
// to see if it should be a part of the classNames added
currentClasses.forEach((currentClass) => {
if (incomingPropClasses.has(currentClass)) {
// add it as its already included in classnames coming in from newProps
finalClassNames.push(currentClass);
incomingPropClasses.delete(currentClass);
}
else if (!oldPropClasses.has(currentClass)) {
// add it as it has NOT been removed by user
finalClassNames.push(currentClass);
}
});
incomingPropClasses.forEach((s) => finalClassNames.push(s));
return finalClassNames.join(' ');
};
// Note(taro): 禁用 react 合成事件抛出 (避免自定义事件属性调用问题, 例如: event.detail.value 等)
const isCoveredByReact = (__eventNameSuffix) => false;
function getComponentName(node) {
return node.tagName.replace(/^TARO-/, '').replace(/-CORE$/, '');
}
function getControlledValue(node) {
const componentName = getComponentName(node);
if (['INPUT', 'TEXTAREA', 'SLIDER', 'PICKER'].includes(componentName)) {
return 'value';
}
else if (componentName === 'SWITCH') {
// Radio、Checkbox 受 RadioGroup、CheckboxGroup 控制,不支持受控
return 'checked';
}
else {
return null;
}
}
function getPropsAfterReactUpdate(node) {
const key = Object.keys(node).find(key => key.includes('__reactProps'));
if (key) {
return node[key];
}
else {
return null;
}
}
function finishedEventHandler(node) {
const controlledValue = getControlledValue(node);
// 不是可以受控的表单组件,直接返回
if (!controlledValue)
return;
// 立即执行事件回调中用户可能触发了的 React 更新
flushSync(() => { });
// 组件在 React 更新后的 React props
const newProps = getPropsAfterReactUpdate(node);
if ((newProps === null || newProps === void 0 ? void 0 : newProps.hasOwnProperty(controlledValue)) && newProps[controlledValue] !== node[controlledValue]) {
// 如果 React Props 的 value 和 DOM 上的 value 不一致,以 React Props 为准(受控)
node[controlledValue] = newProps[controlledValue];
node.setAttribute(controlledValue, newProps[controlledValue]);
}
}
const syncEvent = (node, eventName, newEventHandler) => {
const eventStore = node.__events || (node.__events = {});
const oldEventHandler = eventStore[eventName];
if (!newEventHandler) {
oldEventHandler && node.removeEventListener(eventName, oldEventHandler);
}
else {
if (oldEventHandler) {
if (oldEventHandler.fn === newEventHandler) {
return;
}
else {
// 删除旧的,绑定新的
node.removeEventListener(eventName, oldEventHandler);
}
}
const listener = eventStore[eventName] = function (e) {
// React batch event updates
unstable_batchedUpdates(() => newEventHandler.call(this, e));
// 控制是否更新受控组件的 value 值
finishedEventHandler(node);
};
listener.fn = newEventHandler;
node.addEventListener(eventName, listener);
}
};
// TODO(performace): ReactComponent 已更新了一次,这里手动更新可能存在重复设置属性的问题
const attachProps = (node, newProps, oldProps = {}) => {
// some test frameworks don't render DOM elements, so we test here to make sure we are dealing with DOM first
if (node instanceof Element) {
Object.keys(oldProps).forEach((name) => {
if (['style', 'children', 'ref', 'class', 'className', 'forwardedRef'].includes(name)) {
return;
}
// Note: 移除节点上冗余事件、属性
if (!newProps.hasOwnProperty(name)) {
if (/^on([A-Z].*)/.test(name)) {
const eventName = name.substring(2);
const eventNameLc = eventName.toLowerCase();
if (!isCoveredByReact(eventNameLc)) {
syncEvent(node, eventNameLc);
}
}
else {
node[name] = null;
node.removeAttribute(camelToDashCase(name));
}
}
});
// add any classes in className to the class list
node.className = getClassName(node.classList, newProps, oldProps);
Object.keys(newProps).forEach((name) => {
/** Note(taro): 优化 style 属性的处理
* 1. 考虑到兼容旧版本项目,支持使用字符串配置 style 属性,但这并非推荐写法,且不考虑优化在 style 移除时同步删除属性
* 2. style 属性应当交与前端 UI 框架自行处理,不考虑实现类似于 reactify-wc 的更新策略
*/
if ((name === 'style' && typeof newProps[name] !== 'string') || ['children', 'ref', 'class', 'className', 'forwardedRef'].includes(name)) {
return;
}
if (/^on([A-Z].*)/.test(name)) {
const eventName = name.substring(2);
const eventNameLc = eventName.toLowerCase();
if (!isCoveredByReact(eventNameLc)) {
syncEvent(node, eventNameLc, newProps[name]);
}
}
else {
node[name] = newProps[name];
const propType = typeof newProps[name];
if (propType === 'string') {
node.setAttribute(camelToDashCase(name), newProps[name]);
}
}
});
// 保证受控组件会被默认绑定一个空事件,用于触发 finishedEventHandler 中的受控逻辑
const controlledValue = getControlledValue(node);
if (controlledValue &&
newProps.hasOwnProperty(controlledValue)) {
const handleChangeEvent = ['INPUT', 'TEXTAREA'].includes(getComponentName(node)) ? 'input' : 'change';
node.__events || (node.__events = {});
if (!node.__events.hasOwnProperty(handleChangeEvent)) {
syncEvent(node, handleChangeEvent, function () { });
}
}
}
};
function applyUnControlledDefaultValue(node, props) {
const controlledValue = getControlledValue(node);
// 不是可以受控的表单组件,直接返回
if (!controlledValue)
return;
const defaultValueName = 'default' + controlledValue.charAt(0).toUpperCase() + controlledValue.slice(1);
if (!props.hasOwnProperty(controlledValue) && props.hasOwnProperty(defaultValueName)) {
// 如果是可以受控的表单组件,当没有传入 value/checked 而是传入 defaultValue/defaultChecked 时,把表单值初始化为 defaultValue/defaultChecked
node[controlledValue] = props[defaultValueName];
node.setAttribute(controlledValue, props[defaultValueName]);
}
}
export { applyUnControlledDefaultValue, attachProps, getClassName, isCoveredByReact, syncEvent };
//# sourceMappingURL=attachProps.js.map