简体   繁体   English

受控 Fluent UI Checkbox 组件的渲染不正确

[英]Incorrect rendering of controlled Fluent UI Checkbox components

I have a form that interacts with a database via REST API and presents several controlled Fluent UI components.我有一个通过 REST API 与数据库交互的表单,并提供了几个受控的 Fluent UI 组件。 For a multi-choice field, I built a component that displays a Stack with an arbitrary number of controlled Checkbox components.对于多选字段,我构建了一个组件,该组件显示一个带有任意数量的受控 Checkbox 组件的 Stack。 Below is the component definition.下面是组件定义。

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>
    </>);
  }
}

After the form retrieves the data via REST API, this component uses that data to update its state.在表单通过 REST API 检索数据后,此组件使用该数据更新其 state。 While the state is correctly updated and the correct values are being passed to the props for each Checkbox component, the UI is misleading.虽然 state 已正确更新,并且正确的值正在传递给每个 Checkbox 组件的props ,但 UI 具有误导性。 For instance, the checked values below are set to false , true , false , false , and false respectively, according to the React Components inspector in Chrome DevTools.例如,根据 Chrome DevTools 中的 React Components 检查器,下面的checked值分别设置为falsetruefalsefalsefalse

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

Obviously, while the props are correctly set, the user is presented with five unticked checkboxes.显然,当props设置正确时,用户会看到五个未选中的复选框。 When the user clicks the checkbox that should have been ticked, the state is correctly updated to reflect that all five checkboxes are unticked.当用户单击本应选中的复选框时, state会正确更新以反映所有五个复选框均未选中。 This is what it looks like after the user clicks on the second checkbox.这是用户单击第二个复选框后的样子。

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

The user interacts with the Checkbox components and they behave as expected, but the underlying values are exactly inverted for any where the initial checked property was set to true .用户与 Checkbox 组件交互,它们的行为与预期一样,但对于任何初始checked属性设置为true的地方,底层值都会完全反转。

I added context to the constructor and that didn't help, so I converted this class component into a functional component.我在构造函数中添加了context但没有帮助,因此我将这个 class 组件转换为功能组件。 It worked as intended.它按预期工作。 Here is the code for the functional component.这是功能组件的代码。

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";

Note that the interface is the same and the internal storage of state is essentially the same.注意接口相同, state的内部存储基本相同。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM