javascriptreactjsmaterial-uicodesandbox

Material UI Textfield 'error' and 'helper text' for elements in a loop


I am trying to create an application that has dynamic text field input using MUI textfield. There are two fields - From and To. When the "Add New Field" button is clicked, it generates two new fields. These two are part of the state object. Now, if the user enters a value in "To" field which is lesser than "from" field, it's supposed to display an error below the field as defined in the 'helper text'. However, in my case, the error appears in all the 'To' fields even though the error is supposed to appear only in the row where the input is wrong. It's repeating it in all the rows. How do I fix this? The code is as follows. It can be reproduced in sandbox directly.

import "./styles.css";
import React from "react";
import { Button, Grid, Paper } from "@mui/material";
import { TextField } from "@mui/material";

interface Props {}

interface State {
  serialInputObjects: any;
}

var fromErrorMessage = "";
var toErrorMessage = "";

class SerialQRScanClass extends React.PureComponent<Props, State> {
  state = {
    serialRegistrationTracker: [],
    serialInputObjects: {
      //0: { from: "", to: "", except: "" } }
    }
  };

  calculation = (key) => {
    let errors = this.getFromToSerialErrorMessages(
      this.state.serialInputObjects[key]["from"],
      this.state.serialInputObjects[key]["to"]
    );
    fromErrorMessage = errors.fromErrorMessage;
    toErrorMessage = errors.toErrorMessage;
    console.log(`Key ${key} From error message - ` + fromErrorMessage);
    console.log("To error message - " + toErrorMessage);
  };


  getSerialCodeErrorMessage = (serialCode) => {
    if (!serialCode) return "";
    if (String(serialCode).match(/[^0-9,]+/)) {
      return "Enter only numbers";
    }
    return "";
  };

  getFromToSerialErrorMessages = (fromSerial, toSerial) => {
    const fromErrorMessage = this.getSerialCodeErrorMessage(fromSerial);
    let toErrorMessage = this.getSerialCodeErrorMessage(toSerial);
    if (!fromErrorMessage && !toErrorMessage) {
      const diff = parseInt(toSerial) - parseInt(fromSerial);
      if (diff < 0) toErrorMessage = "To lower than starting point";
    }

    return { fromErrorMessage, toErrorMessage };
  };

  handleAdd = () => {
    const objectLength = Object.keys(this.state.serialInputObjects).length;
    console.log(objectLength);
    this.setState((prevState) => ({
      ...prevState,
      serialInputObjects: {
        ...prevState.serialInputObjects,
        [objectLength]: { from: "", to: "", except: "" }
      }
    }));
    console.log(this.state.serialInputObjects);
  };

  handleChangeFromSerials = (key: any, data: string) => {
    this.setState((prevState) => ({
      ...prevState,
      serialInputObjects: {
        ...prevState.serialInputObjects,
        [key]: { ...prevState.serialInputObjects[key], from: data }
      }
    }));
    console.log(this.state.serialInputObjects);
    this.calculation(key);
  };

  handleChangeToSerials = (key: any, data: string) => {
    this.setState((prevState) => ({
      ...prevState,
      serialInputObjects: {
        ...prevState.serialInputObjects,
        [key]: { ...prevState.serialInputObjects[key], to: data }
      }
    }));
    console.log(this.state.serialInputObjects);
    this.calculation(key);
  };

  render() {
    return (
      <Paper elevation={3} className="abc">
        <Button onClick={this.handleAdd}>ADD NEW FIELD</Button>
        {Object.keys(this.state.serialInputObjects).map((key) => (
          <div key={key}>
            <Grid container alignItems="flex-end">
              <Grid item className="bcd">
                <TextField
                  fullWidth
                  label={"FROM"}
                  placeholder={"Ex.100"}
                  value={this.state.serialInputObjects[key]["from"]}
                  onChange={(e) =>
                    this.handleChangeFromSerials(key, e.target.value)
                  }
                  error={Boolean(fromErrorMessage) || false}
                  helperText={fromErrorMessage}
                  margin="none"
                  size="small"
                />
              </Grid>
              <Grid item className="bcd">
                <TextField
                  fullWidth
                  label={"To"}
                  placeholder={"Ex.100"}
                  value={this.state.serialInputObjects[key]["to"]}
                  onChange={(e) =>
                    this.handleChangeToSerials(key, e.target.value)
                  }
                  error={Boolean(toErrorMessage) || false}
                  helperText={toErrorMessage}
                  margin="none"
                  size="small"
                />
              </Grid>
            </Grid>
          </div>
        ))}
      </Paper>
    );
  }
}

export default function App() {
  return (
    <div className="App">
      <SerialQRScanClass />
    </div>
  );
}

I want to be able to print the error only in that corresponding field in the loop.


Solution

  • We can do this by tracking all the errors for every individual input. First update the state with a fromErrorMessage and a toErrorMessage object. These will hold the errors for the inputs.

    state = {
      serialRegistrationTracker: [],
      serialInputObjects: {
        //0: { from: "", to: "", except: "" } }
      },
      fromErrorMessage: {},
      toErrorMessage: {},
    };
    

    Then we can update the calculation function to store the errors for the specific input. I added 2 new arguments fromValue and toValue these will help with checking the up-to-date value and prevent the error messages to be one state behind.

    calculation = (key, fromValue, toValue) => {
      let errors = this.getFromToSerialErrorMessages(fromValue, toValue);
      this.setState((prevState) => ({
        ...prevState,
        fromErrorMessage: {
          ...prevState.fromErrorMessage,
          [key]: errors.fromErrorMessage,
        },
        toErrorMessage: {
          ...prevState.toErrorMessage,
          [key]: errors.toErrorMessage,
        },
      }));
      console.log(
        `Key ${key} From error message - ` + this.state.fromErrorMessage[key]
      );
      console.log("To error message - " + this.state.toErrorMessage[key]);
    };
    

    Now we need to update the handlers to work with the new calculation function. We pass the current state from the to and the new data to check against and vice versa.

    handleChangeFromSerials = (key: any, data: string) => {
      ...
      this.calculation(key, data, this.state.serialInputObjects[key]["to"]);
    };
    
    handleChangeToSerials = (key: any, data: string) => {
      ...
      this.calculation(key, this.state.serialInputObjects[key]["from"], data);
    };
    

    Finally update the TextField components. And the same for the to input.

    <TextField
      ...
      error={Boolean(this.state.fromErrorMessage[key]) || false}
      helperText={this.state.fromErrorMessage[key]}
      ...
    />