reactjs - redux 表单和 material ui 框架——具有自动类型——和清除字段功能

reactjs - redux form and material ui framework -- with auto type - and clearing field functionality

我正在构建一个嵌套表单框架,它使用 redux 表单和 material ui 框架——我在这里构建了组件ui迄今为止 - https://codesandbox.io/s/heuristic-hopper-lzekw

我想做的是 - 将一些“动画”附加到字段 - 模仿输入 - 我已经通过一个小函数实现了这一点,该函数将获取初始文本并逐步处理字符 - 更新该字段初始值的属性。

现在我遇到的问题是——我需要在 textField 上创建一个 onClick——如果它是一个自动 typetext 字段——将值重置为空字符串——将这个 onclick 传递回父 shell -- 甚至返回到 typetext 函数来打破超时 --- 所以如果用户加载页面,他们会看到文本输入 - 但是 UI 功能改进 - 如果我在动画中间单击该字段- 我想要动画 stop/break,我想要清除字段。

我还想控制哪些字段应该被清除——所以在这种情况下——有一个参数——指示 onClickClear: true——以免破坏用户编辑个人资料的预填表格。

===没有 typetext 的沙箱——但是如何将这两个框架粘合在一起的良好基础 https://codesandbox.io/s/heuristic-hopper-lzekw?file=/src/Home.js

== 这是最新的自动输入沙箱 https://codesandbox.io/s/amazing-bell-z8nhf

var self = this;
typeAnimation(this.state.initial_search_term.search_term, 100, function(msg){
  self.setState({
    initial_search_term: {"search_term": msg}
  });
});

我知道这不是您要找的答案,但最简单的方法是为占位符文本而不是主要输入文本设置动画。然后你就什么都不用担心了,不管用户的动作如何,都可以让动画播放出来。

Now the problem I have -- is I need to create an onClick on the textField -- and if its an automated typetext field - reset the value to an empty string -- pass this onclick back up to the parent shells -- and even back up to the typetext function to break the timeout --- so if the user loads the page they see the text typing - but with UI functionality improvements - if I click on the field during mid-animation - I want the animation to stop/break, I want the field to clear.

I want to have control over also which fields should be cleared -- so in this case - have a param - that indicates onClickClear: true -- so as not to break user edit profile pre-filled forms.

所有这些都可以通过使用字段的占位符来满足(尽管 typetext 不会停止,因为没有必要,因为用户的文本/预填充文本将隐藏占位符)。我唯一没有连接的是停止 HomecomponentWillUnmount 上的 typetext,因为如果没有它,它会抛出警告消息,指出正在未安装的组件上调用 setState。

我不得不进行一些重构,因为 (toggleFieldVisibility in FieldMaker.js) and not updating this.state.fields when new props are passed down as the state was only being set in the constructor 之类的东西存在一些问题。我也在 FieldMaker.js 中重命名了一些东西(在这种情况下主要是由于个人喜好)。

无论你怎么做,尝试从 props 派生状态肯定存在问题:You Probably Don't Need Derived State

运行代码:

https://codesandbox.io/s/busy-davinci-mk0dq?file=/src/Home.js

Home.js

  state = {
    initial_search_term: { search_term: "" },
    searchPlaceholder: "",
    textPlaceholder: "",
    valPlaceholder: ""
  };

  componentDidMount() {
    typeAnimation("Search Text...", 100, (msg) => {
      this.setState({
        searchPlaceholder: msg
      });
    });
    typeAnimation("Just some super long text you used to know", 100, (msg) => {
      this.setState({
        textPlaceholder: msg
      });
    });
    typeAnimation("I'm a value, but am I valuable??", 100, (msg) => {
      this.setState({
        valPlaceholder: msg
      });
    });
  }

// Render funct:
  let fieldsSearchForm = [
      {
        type: "text",
        label: "Search Term",
        name: ["search_term"],
        props: { placeholder: this.state.searchPlaceholder },
        options: []
      },
      {
        type: "text",
        label: "Text",
        name: ["test"],
        props: { placeholder: this.state.textPlaceholder },
        options: []
      },
      {
        type: "text",
        label: "Value",
        name: ["test2"],
        props: { placeholder: this.state.valPlaceholder }
      }
    ];

FieldMaker.js

getDerivedStateFromProps 是这里真正的主要区别,这是在字段更改时根据字段填充 subs 数组(并设置可见性)。我不知道其中有多少是真正必要的,因为没有关于其中 any 实际上应该做什么的概念。所以它可能需要更多的工作才能让它完全正常工作。

另一个区别是在状态中有一个单独的 visiblity 对象而不是修改状态中的 fields

修改此文件的主要原因是确保对 fields 属性的更新转换为对子 Fields 的更新,以便占位符可以通过属性传递给 Field 因此 renderTextField

  state = {
    visibility: {}
  };

  static getDerivedStateFromProps(props, state) {
    let newState = { prevFields: props.fields };
    if (props.fields !== state.prevFields) {
      let visibility = state.visibility;
      let subs = props.fields.reduce((subs, field) => {
        if (field.sub) {
          subs.push(field.sub);
          visibility[field.name] = false;
        } else {
          visibility[field.name] = true;
        }
        return subs;
      }, []);
      newState.subs = subs;
    }
    return newState;
  }

  toggleFieldVisibility(pos, isVisibile) {
    let field = this.props.fields[pos].name;
    this.setState((prev) => {
      return { ...prev, [field]: isVisibile };
    });
    // This directly manipulates state, and is likely problematic in React
    // let fields = { ...this.state.fields };
    // fields[pos]["visibility"] = isVisibile;
  }

  componentDidMount() {
    this.hideSubs();
  }

// In render:
    return (
      <>
        {this.props.fields.map((item, j) => {
          if (this.state.visibility[item.name]) {
            if (item.type !== "checkbox") {
              return (
                <Field
                  key={j}                  
                  name={item.name[0]}
                  props={item.props}
                  label={item.label}
// ...

renderTextField.js

在此,更改的目的只是将占位符向下传递给MUI TextField,并通过设置InputLabelProps = {shrink: true}

使MUI TextField的标签收缩
const renderTextField = ({
  input,
  rows,
  multiline,
  label,
  type,
  meta: { touched, error, warning },
  placeholder,
  InputLabelProps
}) => {
  // Ensure that the label is shrunk to the top of the input
  // whenever there's a placeholder set
  InputLabelProps = placeholder
    ? { ...(InputLabelProps ?? {}), shrink: true }
    : InputLabelProps;
  return (
    <FormControl
      component="fieldset"
      fullWidth={true}
      className={multiline === true ? "has-multiline" : null}
    >
      <TextField
        InputLabelProps={InputLabelProps}
        placeholder={placeholder}
        label={label}
        multiline={multiline}
        rows={rows}
        type={type}
        error={touched && (error && error.length > 0 ? true : false)}
        helperText={
          touched &&
          ((error && error.length > 0 ? error : null) ||
            (warning && warning.length > 0 ? warning : null))
        }
        {...input}
      />
    </FormControl>
  );
};


我以非常快速和肮脏的方式重做了解决方案,以避免 FieldMaker 文件中存在的陷阱,这些陷阱最初导致原始解决方案出现问题:

https://codesandbox.io/s/fervent-moser-0qtvu?file=/src/Home.js

我修改了 typeAnimation 以通过返回停止循环的取消函数来支持类似的取消,并使用回调将值设置为结束状态。

export function typeAnimation(text, timing, callback) {
  let concatStr = "";
  let canceled = false;
  function cancel() {
    canceled = true;
  }
  async function runAnimation() {
    for (const char of text) {
      concatStr += char;
      await sleep(timing);
      if (canceled) {
        break;
      }
      callback(concatStr);
    }
    if (canceled) {
      callback(text);
    }
  }
  runAnimation();
  return cancel;
}

然后在 Home.js 中,我修改了初始状态和 componentDidMount 以使用占位符并给我一个存储取消函数的位置。

  constructor(props, context) {
    super(props, context);
    this.state = {
      initial_search_term: { search_term: "" },
      placeholders: { search_term: "" }
    };
  }
  cancelAnimations = {};
  componentDidMount() {
    var self = this;
    this.cancelAnimations.search_term = typeAnimation(
      "Start typing...",
      100,
      function (msg) {
        self.setState((state) => ({
          placeholders: { ...state.placeholders, search_term: msg }
        }));
      }
    );
  }

我还添加了 fieldsExtras 并将其一直传递到 FieldMaker 组件,以通过与 fieldsSearchForm 数组匹配的索引向该组件中的 Field 添加额外的道具。

    let fieldsExtras = [
      {
        placeholder: this.state.placeholders.search_term,
        onClick: this.cancelAnimations.search_term
      }
    ];

然后,一旦额外的道具一直向下传递到字段中,在 renderTextField 中,我会做与以前相同的事情,但我还添加了 onClick 来调用传入的 onClick函数

const renderTextField = ({
  input,
  rows,
  multiline,
  label,
  type,
  meta: { touched, error, warning },
  placeholder,
  onClick,
  InputLabelProps
}) => {
  InputLabelProps = placeholder
    ? { ...(InputLabelProps ?? {}), shrink: true }
    : InputLabelProps;
  return (
    <FormControl
      component="fieldset"
      fullWidth={true}
      className={multiline === true ? "has-multiline" : null}
    >
      <TextField
        placeholder={placeholder}
        InputLabelProps={InputLabelProps}
        onClick={(e, value) => {
          onClick && onClick(e);
        }}
        label={label}
        multiline={multiline}
        rows={rows}
        type={type}
        error={touched && (error && error.length > 0 ? true : false)}
        helperText={
          touched &&
          ((error && error.length > 0 ? error : null) ||
            (warning && warning.length > 0 ? warning : null))
        }

        {...input}
      />
    </FormControl>
  );
};

我认为使用输入引用更新占位符 属性 是一个很好的解决方案,这样你就不需要更新输入值(避免组件重新渲染),并且你可以清除占位符点击事件的文本:

Home.js

class Home extends Component {
  constructor(props, context) {
    super(props, context);
    this.searchInputRef = React.createRef(null);
    this.state = { initial_search_term: { search_term: "" } };
  }

  componentDidMount() {
    var self = this;
    typeAnimation("Start typing...", 100, function (msg) {
      if (document.activeElement !== self.searchInputRef.current) {
        self.searchInputRef.current.setAttribute("placeholder", msg);
      } else {
        return true; // stop typings
      }
    });
  }


  render() {
    //...

    let fieldsSearchForm = [
      {
        id: "search-field",
        type: "text",
        label: "Search Term",
        name: ["search_term"],
        options: [],
        fieldRef: this.searchInputRef,
        onClick: () => (this.searchInputRef.current.placeholder = "")
      }
    ];
   //...
  }
}

FieldMaker.js

class FieldMaker extends Component {
  //...

  
  render() {
   
    return (
      <>
        {this.state.fields.map((item, j) => {
          if (item.visibility) {
            if (item.type !== "checkbox") {
              return (
                <Field
                  id={item.id}
                  //...other props
                  fieldRef={item.fieldRef}
                  onClick={item.onClick}
                />
              );
            } else {
              //...
            }
          } else {
            //...
          }
        })}
      </>
    );
  }
}

renderTextField.js

const renderTextField = ({
  id,
  input,
  rows,
  multiline,
  label,
  type,
  meta: { touched, error, warning },
  onClick,
  fieldRef
}) => (
  <FormControl
    component="fieldset"
    fullWidth={true}
    className={multiline === true ? "has-multiline" : null}
  >
    <TextField
      id={id}
      inputRef={fieldRef}
      onClick={onClick}
      // other props
    />
  </FormControl>
);

Utility.js

export async function typeAnimation(text, timing, callback) {
  let concatStr = "";
  for (const char of text) {
    concatStr += char;
    await sleep(timing);
    const shouldStop = callback(concatStr);
    if (shouldStop) break; // stop the loop
  }
}

styles.css // 保持占位符可见

#search-field-label {
  transform: translate(0, 1.5px) scale(0.75);
  transform-origin: top left;
}

#search-field::-webkit-input-placeholder {
  opacity: 1 !important;
}