首页 > 解决方案 > React:当状态与选项不匹配时,依赖的受控选择元素应更改其值

问题描述

我有以下情况:

两个受控选择元素。在第一个选择元素中选择一个选项会更改第二个选择元素的选项。这工作正常。

但是:当第二个选择元素的选项列表不包含该元素的当前状态值时,它的视图将变为“未选择”选项(空值)。但是没有更改事件会触发相应地设置状态。

所以:状态保留旧值,但这不再匹配列表中的可选选项。

是否可以检测到选择输入元素的状态是否在控件的选项列表中不可用?

编辑:选择组件的代码

const Select = ({identifier, className, to, dal, setTemplateOptions, model, value, message, valueChanged, touched, evalInScope, fieldTouched, fieldRef, hideExpression}) => {

    const [labelProp, setLabelProp] = useState(to.labelProp?to.labelProp:"label");
    const [valueProp] = useState(to.valueProp?to.valueProp:"value");

    const setNewValue = e => {
        console.log(`SELECT.${identifier} changed its value`);
        if(to.multiple) {
            let resultValues = [];
            for(let opt of fieldRef.current.selectedOptions) {
                resultValues.push(opt.value);
            }
            valueChanged(resultValues, true);
        } else {
            valueChanged(e.target.value, false);
        }
    }

    useEffect(() => {
        async function fetchData() {
            if(dal) {
                // console.log("DAL UPDATE on field " + identifier);
                let jsonResponse = await DAL.request(dal);
                // console.log("Result from DAL request");
                // console.dir(jsonResponse);
                setTemplateOptions(prevTemplateOptions => {
                    return {...prevTemplateOptions, options: jsonResponse}
                })

                // Check to see if values are available in the options, otherwise remove this/these value(s)
                if(typeof value === "string" && value !== "") {
                    let found = false;
                    for(let option of jsonResponse) {
                        if(option[valueProp] === value) {
                            found = true;
                            break;
                        }
                    }
                    if(!found) {
                        //valueChanged("", false);
                        // console.log("Value " + value + " wurde in den Options des Feldes " + identifier + " nicht gefunden!");
                    }
                }
            }

        }
        fetchData();
    }, [dal]);

    useEffect(() => {
        if(to.labelExpression) {
            // example $item.cn + ': ' + $item.value
            to.options.forEach($item => {
                let result = eval(to.labelExpression);
                $item.exprLabel = result;
             });
             setLabelProp("exprLabel");
        }
    }, [to.options])

    return (
        <>
         {!hideExpression || !evalInScope(hideExpression, {model}) ? <div className={className + " mb-4"}>
                <label><b>{to.label ? to.label : identifier} {!to.disabled && to.required?<span style={{color: "red"}}>*</span>: ""}</b></label>
                <select value={value}
                    className="form-control"
                    disabled={to.disabled}
                    onChange={setNewValue}
                    onBlur={fieldTouched}
                    ref={fieldRef} multiple={to.multiple}>
                        {to.multiple?null:<option value="">{to.noChoiceLabel}</option>}
                        {to.options.map((option, i) => (
                            <option key={i} value={option[valueProp]}>{option[labelProp]}</option>
                        ))}
                </select>
                {touched ? <span style={{color: "red"}}>{message}</span> : null}
            </div> : null }
        </>
    )
}

to.options 包含选择的选项。当在“元素”组件上运行 setTemplateOptions “一级”时,这可能会发生变化,该组件将公共代码保存在一个地方。But when the options change to a list not containing the current "value", the select element will show the empty option, but the value state is not updated due to a missing change event.

EDIT2:我创建了一个 stackblitz ( https://stackblitz.com/edit/react-v8glq9 ) 来解决这个问题。请注意,选择元素的模型指向“building3”,这在选择选项中不可用。但是模型仍然显示 building3,即使选择没有这个选项

标签: reactjsreact-hooks

解决方案


如果选择的当前值不是一个选项,则没有简单的默认 html 方式来触发事件。

您可能可以通过表单验证和检查元素是否有效来做一些事情,但这不是我在这里要采用的方法。

您可以在这里使用两种方法:

您可以在存在无效值时清除它们,因此您的选择元素中只有有效值。

作为验证周期的一部分,您可以检查无效值。然后,虽然 select 仍然具有无效值,但它被认为是这种方式,您可以忽略它。

我将暂时专注于清除无效值:

// First, map through the value option and remove anything that doesn't exist within the options array.
  /**
   * values that remain after you remove all values that are invalid in the current options
   */
  const validValue = useMemo(() => {
    if (!value || (Array.isArray(value) && !value.length)) {
      // Nothing to do here
      return value;
    }
    const valueArr = Array.isArray(value) ? value : [value];
    const optionValues = Object.fromEntries(
      to.options.map(option => [option[valueProp], true])
    );

    const newValues = valueArr.filter(value => optionValues[value]);
    return Array.isArray(value) ? newValues : newValues[0] ?? '';
  }, [to.options, value, valueProp]);

  // update the state when there's things to fix.
  useEffect(() => {
    /*
     * call valueChanged whenever valid value differs from value.
     */
    if (
      Array.isArray(value)
        ? value.length !== validValue.length
        : value !== validValue
    ) {
      valueChanged(validValue, Array.isArray(value), true);
    }
  }, [value, validValue]);

这就是故事的结尾,尽管也有一些更新,Element.js以帮助确保正确设置此效果的有效性。

  const isMounting = useRef(true);
  // Only re-run setFieldState here when it's not the first mount.
  // The value is already correct since it's being set correctly as initial State
  useEffect(() => {
    if (isMounting.current) {
      return;
    }
    if (templateOptions.disabled) setFieldState({ valid: true, message: '' });
    else setFieldState(validateField());
  }, [templateOptions.disabled]);

在返回之前的 Element 组件的末尾:

  // Toggle isMounting to false
  useEffect(() => {
    isMounting.current = false;
  },[]);

这是运行代码:https ://stackblitz.com/edit/react-vmfsg6?file=src%2FinputElements%2FSelect.js

对于仅确保将值计为无效的另一种方法,只需更改validateField函数即可:

  // moved this up so it is defined when validateField is called
  const [templateOptions, setTemplateOptions] = useState(
    field.templateOptions || {}
  );

  const validateField = () => {
    let value = model[field.key];
    let isArray = Array.isArray(value);
    if (templateOptions?.options) {
      if (isArray ? value?.length : value) {
        // value to check should be based on the existing options
        const valueArr = Array.isArray(value) ? value : [value];
        const optionValues = Object.fromEntries(
          templateOptions.options.map(option => [
            option[templateOptions.valueProp || 'value'],
            true
          ])
        );

        const newValues = valueArr.filter(value => optionValues[value]);
        value = Array.isArray(value) ? newValues : newValues[0] ?? '';
      }
    }

    return !(
      field.templateOptions &&
      !field.templateOptions.disabled &&
      field.templateOptions.required &&
      (isArray ? !value.length : !value)
    )
      ? { valid: true, message: '' }
      : { valid: false, message: 'This field is required' };
  };

每当模板选项更改时,运行重新验证效果也是一个好主意。

  useEffect(() => {
    if (templateOptions.disabled) set$FieldState({ valid: true, message: '' });
    else set$FieldState(validateField());
  }, [templateOptions.disabled, templateOptions.options]);

此示例的运行代码:https ://stackblitz.com/edit/react-ohhram?file=src%2FElement.js


推荐阅读