0

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

3
  • this is the latest fork - codesandbox.io/s/zealous-wind-tbwp5 -- I've added an autocomplete field - but its hooking into it the required properties - and building it without destroying the way the shell builds the fields. Commented Oct 28, 2020 at 1:25
  • So what problem have you experienced? Commented Oct 28, 2020 at 14:42
  • building the component alongside the other already made fields Commented Oct 28, 2020 at 14:57

2 Answers 2

2

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))
Sign up to request clarification or add additional context in comments.

Comments

0

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
}

6 Comments

Opps. Accidently posted it too early. Please wait for me to complete it.
but keen to see if it can be cleaned up - any notes on it?
I am not familiar with class components, so can't really help you there. I am more of a functional components guy 😅
You can just draw the whole data flow on a paper to understand what kind of code you want to write. Planning ahead really helps writing cleaner code. Correct me if I am wrong: In this case, whenever the user searches, your list of suggestions which is presumably sitting in the redux store has to be updated (either reset or with proper suggestions). Whenever the user clicks a suggestion, the searchParam needs to be updated and you also need to reset the suggestions list os that the component closes.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.