0

我有一个通过 REST API 与数据库交互的表单,并提供了几个受控的 Fluent UI 组件。对于多选字段,我构建了一个组件,该组件显示一个带有任意数量的受控 Checkbox 组件的 Stack。下面是组件定义。

class MultiChoiceField extends React.Component
{
  static contextType = FormContext;
  static displayName = "MultiChoiceField";

  #handlers = { change: {} };

  /**
   * Initializes the component using the information provided in the {@link Item} provided by the {@link FormContext}.
   * @constructor
   * @param {Object} props The properties provided for this component.
   */
  constructor(props)
  {
    super(props);
    this.state = { value: {} };
  }

  /**
   * Set up the component once it is added to the DOM. Context isn't available in the constructor, so we set up the
   * value here.
   * @function
   * @param {Object} nextProps The value that will be assigned to `this.props`.
   * @param {Object} nextContext The {@link FormContext} that will be assigned to `this.context`.
   * @public
   * @returns {void}
   */
  componentDidMount(nextProps, nextContext)
  {
    const choices = nextProps?.Field?.Choices?.results || [];
    let value = nextContext?.Item?.[nextProps.FieldName] || {};
    value = Array.isArray(value) ? value : (value.results || []);
    this.setState({
      value: choices.reduce((result, choice) => ({ ...result, [choice]: value.indexOf(choice) >= 0 }), {})
    });
  }

  /**
   * Update the component when it receives new props or context information.
   * @function
   * @param {Object} nextProps The value that will be assigned to `this.props`.
   * @param {Object} nextContext The {@link FormContext} that will be assigned to `this.context`.
   * @public
   * @returns {void}
   */
  componentWillReceiveProps(nextProps, nextContext)
  {
    const choices = nextProps?.Field?.Choices?.results;
    let value = nextContext.Item?.[nextProps.FieldName] || {};
    value = Array.isArray(value) ? value : (value.results || []);
    this.setState({
      value: choices.reduce((result, choice) => ({ ...result, [choice]: value.indexOf(choice) >= 0 }), {})
    });
  }

  /**
   * Get an event handler for the specified choice.
   * @function
   * @param {string} name The choice with which this event handler is associated.
   * @public
   * @returns {function} An event handler for the specified choice.
   */
  handleChange = (name) =>
  {
    const bubbleOnChange = (event, value) =>
      (this.props.onChange?.(event, Object.keys(value).filter((choice) => (value[choice]))));
    if (!this.#handlers.change[name])
    {
      this.#handlers.change[name] = (event) =>
      {
        const value = { ...this.state.value, [name]: !this.state.value[name] };
        this.setState({ value }, () => (void bubbleOnChange(event, value)));
      };
    }
    return this.#handlers.change[name];
  }

  /**
   * Render the user interface for this component as a
   * [Stack]{@link https://developer.microsoft.com/en-us/fluentui#/controls/web/stack} containing
   * [Checkbox]{@link https://developer.microsoft.com/en-us/fluentui#/controls/web/checkbox} components.
   * @function
   * @public
   * @returns {JSX} The user interface for this component.
   */
  render()
  {
    const choices = this.props.Field.Choices.results;
    return (<>
      <Fabric.Stack {...this.props.stackTokens}>
        {choices.map((choice) => (
          <Fabric.Checkbox label={choice} checked={this.state.value[choice]}
            onChange={this.handleChange(choice)} key={choice} />
        ))}
      </Fabric.Stack>
      <div
        className="errorMessage"
        id={`FormFieldDescription--${this.context.Item?.Id}__${this.props.FieldName}`}
        role="alert"
        style={{ display: this.props.errorMessage ? "" : "none" }}>
        {this.props.errorMessage}
      </div>
    </>);
  }
}

在表单通过 REST API 检索数据后,该组件使用该数据更新其状态。虽然正确更新了状态并且将正确的值传递给props每个 Checkbox 组件,但 UI 具有误导性。例如,根据 Chrome DevTools 中的 React Components 检查器,checked以下值分别设置为falsetruefalsefalse和。false

包含一个 Checkbox 的 Stack 的初始呈现,选中设置为 true; 没有勾选复选框

显然,虽然props设置正确,但用户会看到五个未选中的复选框。当用户单击本应选中的复选框时,state会正确更新以反映所有五个复选框均未选中。这是用户单击第二个复选框后的样子。

更新了不包含复选框且选中设置为 true 的堆栈表示; 第二个复选框被勾选

用户与 Checkbox 组件交互,它们的行为与预期一样,但对于初始checked属性设置为 的任何位置,底层值都完全反转true

4

1 回答 1

0

我添加context到构造函数中并没有帮助,因此我将此类组件转换为功能组件。它按预期工作。这是功能组件的代码。

const MultiChoiceField = ({ errorMessage = "", Field, FieldName, onChange, stackTokens = {} } = {}) =>
{
  const context = useContext(FormContext);
  const [value, setValue] = useState({});
  const handleChange = (choice) => (event) =>
  {
    const bubbleOnChange = (event, value) => (void onChange?.(event, value));
    const getValueAsArray = (valueNew) =>
      (Object.entries(valueNew).filter(([, value]) => (value)).map(([key]) => (key)));
    const valueNew = { ...value, [choice]: !value[choice] };
    bubbleOnChange(event, getValueAsArray(valueNew));
  };
  const updateChoices = () =>
  {
    const reduceSelected = (valueContext) => (result, choice) =>
      ({ ...result, [choice]: ~valueContext.indexOf(choice) });
    const choices = Field?.Choices?.results || [];
    let valueContext = context?.Item?.[FieldName] || {};
    valueContext = Array.isArray(valueContext) ? valueContext : (valueContext.results || []);
    setValue(choices.reduce(reduceSelected(valueContext), {}));
  };
  useEffect(updateChoices, [Field?.Choices, FieldName, context?.Item]);
  const renderChoice = (choice) => (
    <Fabric.Checkbox checked={!!value[choice]} key={choice} label={choice} onChange={handleChange(choice)} />
  );
  return (<>
    <Fabric.Stack {...stackTokens}>{(Field?.Choices?.results || []).map(renderChoice)}</Fabric.Stack>
    <div
      className="errorMessage"
      id={`FormFieldDescription--${context.Item?.Id}__${FieldName}`}
      role="alert"
      style={{ display: errorMessage ? "" : "none" }}>
      {errorMessage}
    </div>
  </>);
};
MultiChoiceField.displayName = "MultiChoiceField";

注意接口是一样的,内部存储state本质上是一样的。

于 2021-05-03T16:28:59.067 回答