The Old County
The Old County

Reputation: 109

reactjs - redux form and material ui framework — autocomplete field

I am building a nested form framework that uses the redux form and material ui framework -- I've built the components here to date - https://codesandbox.io/s/bold-sunset-uc4t5

what I would like to do - is add an autocomplete field -- that only shows the possible results after 3 chars have been typed.

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

I want it to have similar properties/styles to the text field and select box


14th Dec - latest form framework https://codesandbox.io/s/cool-wave-9bvqo

Upvotes: 0

Views: 742

Answers (2)

The Old County
The Old County

Reputation: 109

Solution is here.

I created a standalone form component to handle just this autocomplete lookup.

-- 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) {
    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))

Upvotes: 2

Akshay Kumar
Akshay Kumar

Reputation: 1035

I am no expert in material UI but I think it just helps you with styling. I'll just try to answer this in a generalized manner. I am assuming you need something which:

  • allows the user to type something
  • calls an API to fetch suggestions when some condition is met. In this case, its whenever the input's value changes
    • In your case, we also need to ensure that the entered value's length is greater than 3
  • allows user to set the param value by clicking on a suggestion (this should not trigger another API request)

So we need to keep this information in the component's state. Assuming that you have the relevant redux slices set up, your component could look like this:

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

The suggestions component could look like:

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
}

Upvotes: 0

Related Questions