首页 > 解决方案 > 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

标签: javascriptreactjsmaterial-ui

解决方案


解决方案在这里。

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

-- 渲染自动完成字段。

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) {
    super(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) {
    super(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({ }, dispatch);
}

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

推荐阅读