reactjs-redux表单和材料ui框架-自动完成字段

问题描述

我正在构建一个使用redux表单和材料ui框架的嵌套表单框架-到目前为止,我已经在此处构建了组件-https://codesandbox.io/s/bold-sunset-uc4t5

我想做的-添加一个自动完成字段-仅在键入3个字符后显示可能的结果。

https://material-ui.com/components/autocomplete/

我希望它具有与文本字段类似的属性/样式,然后选择框


12月14日-最新表格框架 https://codesandbox.io/s/cool-wave-9bvqo

解决方法

解决方案在这里。

我创建了一个独立的表单组件来处理此自动完成查找。

-renderAutocompleteField。

import React from "react";
import TextField from "@material-ui/core/TextField";
import FormControl from "@material-ui/core/FormControl";

import Autocomplete from "@material-ui/lab/Autocomplete";
import { Box } from "@material-ui/core";
import CloseIcon from "@material-ui/icons/Close";

const renderAutocompleteField = ({input,rows,multiline,label,type,options,optionValRespKey,onTextChanged,onFetchResult,placeholder,fieldRef,onClick,disabled,filterOptions,meta: { touched,error,warning } }) => {

  return (
    <FormControl
      component="fieldset"
      fullWidth={true}
      className={multiline === true ? "has-multiline" : null}
    >
      <Autocomplete
        freeSolo
        forcePopupIcon={false}
        closeIcon={<Box component={CloseIcon} color="black" fontSize="large" />}
        options={options.map((option) => 
          option[optionValRespKey]
        )}
        filterOptions={filterOptions}
        onChange={(e,val) => {
          onFetchResult(val);
        }}
        onInputChange={(e,val,reason) => {
          onTextChanged(val);
        }}
        renderInput={(params) => (
          <TextField
            label={label}
            {...params}
            placeholder={placeholder}
            InputLabelProps={placeholder? {shrink: true} : {}}
            inputRef={fieldRef}
            onClick={onClick}
            disabled={disabled}
            {...input}
          />
        )}
      />
    </FormControl>
  );
};

export default renderAutocompleteField;

-AutocompleteFieldMaker.js

import React,{ Component } from 'react'
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

//import Button from '@material-ui/core/Button';
import { Field,Fields } from 'redux-form';
import renderAutocompleteField from "./renderAutocompleteField";

import Grid from '@material-ui/core/Grid';
import { getToken } from '../../_SharedGlobalComponents/UserFunctions/UserFunctions';

import { createFilterOptions } from "@material-ui/lab/Autocomplete";

//import './OBYourCompany.scss';

class AutocompleteFieldMaker extends Component {
    
  constructor(props,context) {
    super(props,context);
    this.state = {
      searchText: "",autoCompleteOptions: []
    }

    this.fetchSuggestions = this.fetchSuggestions.bind(this);
    this.fetchContents = this.fetchContents.bind(this);
    this.onTextChanged = this.onTextChanged.bind(this);
  }
  
  fetchSuggestions(value){
    let that = this;
    let obj = {};
    obj[this.props.fields[0].name[0]] = value;

    this.props.fields[0].initialValLookup(obj,this.props.fields[0].paramsforLookup,function(resp){
        if(resp && resp.data && Array.isArray(resp.data)){
            that.setState({
              searchText: value,autoCompleteOptions: resp.data,lastOptions: resp.data
            });
        }
    });
  };

  fetchContents(val){
    let that = this;
    let result = this.state.lastOptions.filter(obj => {
        return obj[that.props.fields[0].optionValRespKey] === val
    })

    this.props.fieldChanged("autocomplete",result[0]);
  };

  onTextChanged(val) {
    if (val.length >= 3) {
      this.fetchSuggestions(val);
    } else {
      this.setState({ searchText: val,autoCompleteOptions: [] });
    }
  }

  render() {

    //console.log(",this.state.autoCompleteOptions",this.state.autoCompleteOptions)

    return (
      <div className="Page">       
        <Field
            name={this.props.fields[0].name[0]} 
            label={this.props.fields[0].label} 
            component={renderAutocompleteField}
            options={this.state.autoCompleteOptions}
            optionValRespKey={this.props.fields[0].optionValRespKey}
            placeholder={this.props.fields[0].placeholder}
            //rows={item.type === "comment" ? 4 : null}
            //multiline={item.type === "comment" ? true : false}
            //onChange={(item.type === "slider" || item.type === "date" || item.type === "buttons")? null : (e,value) => {
            //  this.onHandle(e.target.name,value);
            //}}
            //onHandle={this.onHandle}
            fieldRef={this.props.fields[0].fieldRef}
            onClick={this.props.fields[0].onClick}
            disabled={this.props.fields[0].disabled}
            onTextChanged={this.onTextChanged}
            onFetchResult={this.fetchContents}
        filterOptions= {createFilterOptions({
          stringify: (option) => option + this.state.searchText
      })}
        />
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
     
  };
}

function mapDispatchToProps(dispatch) {
 return bindActionCreators({ },dispatch);
}

export default withRouter(connect(mapStateToProps,mapDispatchToProps)(AutocompleteFieldMaker))

-AutocompleteFormShell.js

import React,{ Component } from 'react';
import { reduxForm } from 'redux-form';

import Button from '@material-ui/core/Button';
import AutocompleteFieldMaker from './AutocompleteFieldMaker';


class AutocompleteFormShell extends Component {
 
 constructor(props,context);
    this.fieldChanged = this.fieldChanged.bind(this);
    this.submitBundle = this.submitBundle.bind(this);

    this.state = {
      bundle: ""
    }
 }

  fieldChanged(field,value){
      //console.log("Fields have changed",field,value);
      let bundle = {}
      bundle[field] = value;

      this.setState({ bundle: bundle });

      //if it doesn't have any submit buttons -- then submit the form on change of fields
      if(!this.props.buttons.length > 0){
        //console.log("submit the form as a buttonless form");
        setTimeout(() => {
          this.submitBundle();
        },1);        
      }
 }

 isDisabled(){
  let bool = false;

  if(this.state.bundle === ""){
    bool = true;
  }

  return bool;
 }

 submitBundle(){
    this.props.onSubmit(this.state.bundle);
 }

 render(){
  const { handleSubmit,pristine,reset,previousPage,submitting } = this.props

  return (
    <form onSubmit={handleSubmit}>
      <AutocompleteFieldMaker fields={this.props.fields} fieldChanged={this.fieldChanged} />
      <Button 
        variant={this.props.buttons[0].variant} 
        color={this.props.buttons[0].color} 
        disabled={this.isDisabled()}
        onClick={this.submitBundle}
      >
        {this.props.buttons[0].label}
      </Button>
    </form>
  )
 }

}

export default reduxForm()(AutocompleteFormShell)

-AutocompleteForm.js

import React,{ Component } from 'react'
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import Grid from '@material-ui/core/Grid';

import { uuid } from '../Utility/Utility';

// components
import AutocompleteFormShell from './AutocompleteFormShell';

import '../../../forms.scss';
import './AutocompleteForm.scss';

class AutocompleteForm extends Component {

  constructor(props,context);
    this.state = {
      uuid: this.props.uuid? this.props.uuid : uuid(),theme: this.props.theme? this.props.theme : "light"
    };

    //if uuid is not supplied generate it. (uuid should be the same in a wizzardform)
    //if theme is not provided default it to light (legible on white backgrounds)

    this.submit = this.submit.bind(this);
    this.validateHandler = this.validateHandler.bind(this);
    this.warnHandler = this.warnHandler.bind(this);
  }

  submit(data) {
    this.props.submitHandler(data);
  }

  validateHandler(values) {  
      const errors = {}

      for (let i = 0; i < this.props.fields.length; ++i) {

        let field = this.props.fields[i];        
        
        //loop through the field names -- checkbox will likely have more than 1
        for (let j = 0; j < field.name.length; ++j) {

          let fieldName = field.name[j];
          if(field.validate !== undefined){
            //create validation

            if(field.validate.includes("email")) {
              //email
              if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values[fieldName])) {
                errors[fieldName] = 'Invalid email address'
              }
            }

            if(field.validate.includes("minLength")) {
              //minLength
              if (values[fieldName] !== undefined && values[fieldName].length < 3) {
                errors[fieldName] = 'Must be 3 characters or more'
              }
            }

            if(field.validate.includes("required")) {
              //required
              if (!values[fieldName] && typeof values[fieldName] !== "number") {
                errors[fieldName] = 'Required'
              }
            }
          }

        }

      }

    return errors;
  }


  warnHandler(values) {

      const warnings = {}

      for (let i = 0; i < this.props.fields.length; ++i) {
        
        let field = this.props.fields[i];

        //loop through the field names -- checkbox will likely have more than 1
        for (let j = 0; j < field.name.length; ++j) {

          let fieldName = field.name[j];

          if(field.warn !== undefined){
            //create warn

            //rude
            if(field.warn.includes("git")) {
              //required
              if (values[fieldName] === "git") {
                warnings[fieldName] = 'Hmm,you seem a bit rude...'
              }
            }
          }

        }

      }

      return warnings;
  }

 
  render() {    
    let errorPlaceholder = this.props.errorPlaceholder;


    //light or dark theme for the form

    return (
      <div className={"Page form-components generic-form-wrapper " + this.state.theme}>
          <Grid container spacing={1}>
            <Grid item xs={12}>
              {/*{this.state.uuid}*/}
              <AutocompleteFormShell 
                initialValues={this.props.initialValues} 
                enableReinitialize={this.props.enableReinitialize? this.props.enableReinitialize: true}//allow form to be reinitialized
                fields={this.props.fields} 
                buttons={this.props.buttons}
                form={this.state.uuid}// a unique identifier for this form
                validate={this.validateHandler}// <--- validation function given to redux-form
                warn={this.warnHandler}//<--- warning function given to redux-form
                onSubmit={this.submit}
                previousPage={this.props.previousPage}
                destroyOnUnmount={this.props.destroyOnUnmount}// <------ preserve form data
                forceUnregisterOnUnmount={this.props.forceUnregisterOnUnmount}// <------ unregister fields on unmount 
                keepDirtyOnReinitialize={this.props.keepDirtyOnReinitialize}
              />
            </Grid>
            {errorPlaceholder && errorPlaceholder.length > 0 &&
              <Grid item xs={12}>
                <div className="error-text">
                  {errorPlaceholder}
                </div>
              </Grid>
            }
          </Grid>
      </div>
    )
  }

}

function mapStateToProps(state) {
  return {   
  };
}

function mapDispatchToProps(dispatch) {
 return bindActionCreators({ },mapDispatchToProps)(AutocompleteForm))
,

我不是材料UI方面的专家,但我认为它只能帮助您进行样式设置。 我将尝试以广义的方式回答这个问题。 我假设您需要以下东西:

  • 允许用户键入某些内容
  • 在满足某些条件时调用API来获取建议。在这种情况下,只要输入值改变
    • 对于您而言,我们还需要确保输入值的长度大于3
  • 允许用户通过单击建议来设置参数值(这不会触发另一个API请求)

因此,我们需要将此信息保持在组件的状态。 假设您已设置了相关的redux slice,则您的组件可能如下所示:

const SearchWithAutocomplete = () => {
  const [searchParam,setSearchParam] = useState({ value: '',suggestionRequired: false })

  const onSearchParamChange = (value) => setSearchParam({ value,suggestionRequired: value.length > 3 /*This condition could be improved for some edge cases*/ })

  const onSuggestionSelect = (value) => setSearchParam({ value,suggestionRequired: false }) //You could also add a redux dispatch which would reset the suggestions list effectively removing the list from DOM

  useEffect(() => {
    if(searchParam.suggestionRequired) {
      // reset list of suggestions
      // call the API and update the list of suggestions on successful response
    }
  },[searchParam])

  return (
    <div>
      <input value={searchParam.value} onChange={event => onSearchParamChange(event.target.value)} />
      <Suggestions onOptionClick={onSuggestionSelect} />
    </div>
  )
}         

建议组件可能类似于:

const Suggestions = ({ onOptionClick }) => {
  const suggestions = useSelector(state => state.suggestions)
  return suggestions.length > 0 ? (
    <div>
      {suggestions.map((suggestion,index) => (
        <div onClick={() => onOptionClick(suggestion)}></div>
      ))}
    </div>
  ) : null
}