How to create forms with MUI (material ui) - reactjs

Hiho,
I want to use mui in my current react project. Is there a different/better way to create forms than the following example?:
const [companyName, setCompanyName] = useState<string>("");
const [companyNameError, setCompanyNameError] = useState<boolean>(false);
const changeName = (event: React.ChangeEvent<HTMLInputElement>) => {
if(event.target.value === "") {
setCompanyNameError(true);
} else {
setCompanyNameError(false);
}
event.preventDefault();
setCompanyName(event.target.value);
}
const anyInputFieldEmpty = () => {
var result = false;
if(companyName === "") {
setCompanyNameError(true);
result = true;
} else {
setCompanyNameError(false);
}
// add the same check for all other fields. My real code has multiple input fields
return result;
}
const resetFields = () => {
setCompanyName("");
}
return (
<div>
<TextField
required
fullWidth
label="Company Name"
margin="dense"
name="companyName"
value={companyName}
onChange={changeName}
helperText={companyNameError ? "Company name is not allowed to be empty!" : ""}
error={companyNameError}
/>
<Button
sx={{ alignSelf: 'center', }}
variant="contained"
onClick={() => {
if(!anyInputFieldEmpty()) {
onSubmitClick(); // some function from somewhere else, which triggers logic
resetFields(); // This form is in a popover. The values should be resetted before the user open it again.
}
}}
>
Create
</Button>
</div>);
It feels wrong to do the validation this way if I use multiple textfields (up to 9). Its a lot of boilerplate code and if I add further validation rules (for example a minimum character count) it goes crazy.
Is this the right way to achive my goal?
T

As others have mentioned. Formik and Yup works great for validation. Formik also provides a way to easily disable your submit buttons. Here is a codesandbox : https://codesandbox.io/s/long-butterfly-seogsw?file=/src/App.js

I think you should check the Formik for hook-based validation and jsonforms, react-declarative from json-schema based view creation
Less code solutions is better on production, but for a learning reason it better to write real code based on hooks, contexts or redux reducers
import { useState } from "react";
import { One, FieldType } from "react-declarative";
const fields = [
{
type: FieldType.Text,
title: "First name",
defaultValue: "Peter",
name: "firstName"
},
{
type: FieldType.Text,
title: "Age",
isInvalid: ({ age }) => {
if (age.length === 0) {
return "Please type your age";
} else if (parseInt(age) === 0) {
return "Age must be greater than zero";
}
},
inputFormatterTemplate: "000",
inputFormatterAllowed: /^[0-9]/,
name: "age"
}
];
export const App = () => {
const [data, setData] = useState(null);
const handleChange = (data) => setData(data);
return (
<>
<One fields={fields} onChange={handleChange} />
<pre>{JSON.stringify(data, null, 2)}</pre>
</>
);
};
export default App;
An example project could be found on this codesandbox

MUI does not have a native form validator
i recommend using react-hook-form + yup it's pretty simple and has a lot of tutorials
https://react-hook-form.com/get-started
EXEMPLE
TextFieldComponent
import { OutlinedTextFieldProps } from '#mui/material';
import React from 'react';
import { Control, useController } from 'react-hook-form';
import { InputContainer, TextFieldStyled } from './text-field.styles';
export interface TextFieldProps extends OutlinedTextFieldProps {
control: Control;
helperText: string;
name: string;
defaultValue?: string;
error?: boolean;
}
export const TextField: React.FC<TextFieldProps> = ({
control,
helperText,
name,
defaultValue,
error,
...rest
}) => {
const { field } = useController({
name,
control,
defaultValue: defaultValue || ''
});
return (
<InputContainer>
<TextFieldStyled
helperText={helperText}
name={field.name}
value={field.value}
onChange={field.onChange}
fullWidth
error={error}
{...rest}
/>
</InputContainer>
);
};
Styles
import { TextField } from '#mui/material';
import { styled } from '#mui/material/styles';
export const TextFieldStyled = styled(TextField)`
.MuiOutlinedInput-root {
background: ${({ theme }) => theme.palette.background.paper};
}
.MuiInputLabel-root {
color: ${({ theme }) => theme.palette.text.primary};
}
`;
export const InputContainer = styled('div')`
display: grid;
grid-template-columns: 1fr;
align-items: center;
justify-content: center;
width: 100%;
`;
export const InputIconStyled = styled('i')`
text-align: center;
`;
Usage
// Validator
const validator = yup.object().shape({
email: yup
.string()
.required(translate('contact.email.modal.email.required'))
.email(translate('contact.email.modal.email.invalid')),
});
// HookForm
const {
control,
handleSubmit,
formState: { errors }
} = useForm({
resolver: yupResolver(validator)
});
// Compoenent
<TextField
label="label"
fullWidth
placeholder="placeholder
size="small"
control={control}
helperText={errors?.name?.message}
error={!!errors?.name}
name={'name'}
variant={'outlined'}
/>

Related

Unable to change the background color of the material ui date-picker in mobile view

I have the below code for my date picker. I am unable to change the background color in mobile view. I was adding #global so i guess its doing the change globally. But if i remove that i am unable to add the class. Any suggestions please
import React from 'react';
import { DatePicker } from '#material-ui/pickers/DatePicker';
import format from 'date-fns/format';
import isMobileview from '/isMobileview';
import { Theme } from '#material-ui/core/styles';
import makeStyles from '#material-ui/core/styles/makeStyles';
import Dialog from '#material-ui/core/Dialog';
export interface DateProps {
isOpen: boolean;
anchorEl?: Element | null;
onClose: () => void;
value: string | null;
onSubmit: (date: Date | null) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
'#global': {
'.MuiPickersCalendarHeader-switchHeader': ({ isMobileview }: { isMobileview: boolean }) => ({
background: isMobileview ? 'red': 'blue'
}),
},
})
);
const DatePop: React.FC<DatePopProps> = (props)=> {
const classes = useStyles();
const isMobileview = useIsMobileview();
return(
<DatePicker
variant="dialog"
open
value={format(new Date(), 'MM/dd/yyyy')}
value={
props.value
? format(new Date(props.value), 'MM/dd/yyyy')
: format(new Date(), 'MM/dd/yyyy')
}
InputProps={{
style: {
display: 'none',
},
}}
className={isMobileApp ? 'none' :classes['#global'] }
/>
)
}
export default DateP;

How to add thousand separator to Textfield?

I've tried to add thousand separator by using react-number-format package. However, I couldn't make it happen. I use react 18. And this what I tried:
function NumberFormatCustom(props) {
const { inputRef, onChange, ...other } = props;
return (
<NumericFormat
{...other}
getInputRef={inputRef}
onValueChange={(values) => {
onChange({
target: {
name: props.name,
value: values.value,
},
});
}}
thousandSeparator
/>
);
}
<TextField
required
error={loanType === "I" && totalAmount > 100000}
fullWidth
type="number"
label="Tota lAmount"
value={totalAmount}
onChange={(e) => setTotalAmount(e.target.value)}
InputProps={{
inputComponent: NumberFormatCustom,
startAdornment: (
<InputAdornment position="start">$</InputAdornment>
),
}}
/>
According to the current docs you can use react-number-format along with MUI TextField like this:
import { NumericFormat } from 'react-number-format';
import { TextField } from '#mui/material';
<NumericFormat value={12323} customInput={TextField} />;
In your case, your code can be like this:
import { InputAdornment, TextField } from "#mui/material";
import { useState } from "react";
import { NumericFormat } from "react-number-format";
const MyNumberComponent = () => {
const [totalAmount, setTotalAmount] = useState(52100);
const handleChange = (ev) => {
setTotalAmount(ev.floatValue);
};
const materialUiTextFieldProps = {
required: true,
error: totalAmount > 100000,
fullWidth: true,
label: "Total Amount",
InputProps: {
startAdornment: <InputAdornment position="start">$</InputAdornment>
}
};
return (
<>
<NumericFormat
value={totalAmount}
customInput={TextField}
onValueChange={handleChange}
thousandSeparator=","
decimalSeparator="."
{...materialUiTextFieldProps}
/>
binded value: {totalAmount}
</>
);
};
export default MyNumberComponent;
You can take a look at this sandbox for a live working example of this approach.
I am using this, it works well ->
export const numberWithCommas = (x: number) =>
x?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0';
And to change it back to number:
export const numberWithoutCommas = (x: number | string, isFloat?: boolean) => {
const str = x.toString().replace(/,/g, '');
return isFloat ? parseFloat(str) : parseInt(str, 10);
};

Handle controlled checbox

I have a form where I am rendering checkbox from map, how can I handle when someone unchecked box? Now i am using isChecked to set it.
import React, {ChangeEvent, Fragment, useCallback, useEffect, useState} from 'react';
import Button from '#atlaskit/button/standard-button';
import {Checkbox} from '#atlaskit/checkbox';
import {Grid, GridColumn} from '#atlaskit/page';
import Form, {CheckboxField, Field, FormFooter} from '#atlaskit/form';
import {ValueType as Value} from "#atlaskit/select/types";
import Select from "#atlaskit/select";
import {sentinelVulnerabilities} from "../constants";
import {invoke} from "#forge/bridge";
interface Option {
label: string;
value: string;
}
BasicConfiguration.defaultProps = {
jiraIssuePriorities: [],
}
const columns = 12;
export default function BasicConfiguration({jiraIssuePriorities, initPriorites, allowedVulnerabilities}: any) {
const [allowedVul, setAllowedVul] = useState<any | null>(undefined);
useEffect(() => {
(async () => {
await invoke("getStorage", {name: 'vulnerabilities_allowed'}).then(setAllowedVul);
})();
}, [])
const jiraIssuePrioritiesOptions = jiraIssuePriorities.map(({name, id}: any) => ({
label: name,
value: id,
}));
const shouldBySelected = (prioritySentinel: string) => {
if (initPriorites === undefined || Object.keys(prioritySentinel).length === 0.)
return '';
return initPriorites[prioritySentinel];
}
const shouldBeChecked = (vulnName: string): boolean => {
if (allowedVul === undefined || Object.keys(allowedVul).length === 0.) {
return false;
}
return allowedVul.includes(vulnName);
}
const onSubmit = async (data: any) => {
//Store mapping
await invoke("setStorage", {name: "vulnerabilities_allowed", data: data.vulnerabilities});
let priorities = {
note: undefined,
critical: undefined,
high: undefined,
medium: undefined,
low: undefined
};
if (data.hasOwnProperty('critical')) {
priorities.critical = data.critical.label;
}
if (data.hasOwnProperty('high')) {
priorities.high = data.high.label;
}
if (data.hasOwnProperty('medium')) {
priorities.medium = data.medium.label;
}
if (data.hasOwnProperty('low')) {
priorities.low = data.low.label;
}
if (data.hasOwnProperty('note')) {
priorities.note = data.note.label;
}
await invoke("setStorage", {name: 'vuln_priorities', data: priorities});
}
return (
<div style={{
display: 'flex',
width: '600px',
margin: '0 auto',
flexDirection: 'column',
paddingTop: 50,
}}>
<h3>Map Sentinel Vulnerabilities and Jira Issues</h3>
<Form onSubmit={onSubmit}>
{({formProps}) => (
<form {...formProps}>
{
sentinelVulnerabilities.map((element) => {
const isChecked = shouldBeChecked(element.value);
return <div>
<Grid spacing="compact" columns={columns}>
<GridColumn medium={4} css={{paddingTop: '5px'}}>
<CheckboxField name="vulnerabilities" value={element.value}>
{({fieldProps}) => <Checkbox {...fieldProps} label={element.label} isChecked={isChecked}
/>}
</CheckboxField>
</GridColumn>
<GridColumn medium={8}>
<Field<Value<Option>>
name={element.value}
isRequired={true}
defaultValue={{
value: shouldBySelected(element.value).toLowerCase(),
label: shouldBySelected(element.value)
}}
>
</Field>
</GridColumn>
</Grid>
</div>
})
}
</div>
);
}
What i want to achive is when page render have checkbox checked based on function shouldBeChecked() but I want that user can uncheck the box and submit the form. For now user is not able to unchecked the box, checkbox is always checked.
isChecked should be in a state, so it's value can be changed between different renders, otherwise it's value will always be the same returend from const isChecked = shouldBeChecked(element.value); on the first render within the map function.
And it's better to evaluate isChecked outside map function, because every time the component renders, the shouldBeChecked function will be running again which assins value to isChecked. So it'd be better to put this const isChecked = shouldBeChecked(element.value); in useEffect with empty dependency.

React Hook Form Register Different Forms With Same Field Names

I have a material ui stepper in which there are multiple forms using react-hook-form. When I change between steps, I would expect a new useForm hook to create new register functions that would register new form fields, but when I switch between steps, the second form has the data from the first. I've seen at least one question where someone was trying to create two forms on the same page within the same component but in this case I am trying to create two unique forms within different steps using different instances of a component. It seems like react-hook-form is somehow not updating the useForm hook or is recycling the form fields added to the first register call.
Why isn't react-hook-form using a new register function to register form fields to a new useForm hook? Or at least, why isn't a new useForm hook being created between steps?
DynamicForm component. There are two of these components (one for each step in the stepper).
import { Button, Grid } from "#material-ui/core";
import React from "react";
import { useForm } from "react-hook-form";
import { buttonStyles } from "../../../styles/buttonStyles";
import AppCard from "../../shared/AppCard";
import { componentsMap } from "../../shared/form";
export const DynamicForm = (props) => {
const buttonClasses = buttonStyles();
const { defaultValues = {} } = props;
const { handleSubmit, register } = useForm({ defaultValues });
const onSubmit = (userData) => {
props.handleSubmit(userData);
};
return (
<form
id={props.formName}
name={props.formName}
onSubmit={handleSubmit((data) => onSubmit(data))}
>
<AppCard
headline={props.headline}
actionButton={
props.actionButtonText && (
<Button className={buttonClasses.outlined} type="submit">
{props.actionButtonText}
</Button>
)
}
>
<Grid container spacing={2}>
{props.formFields.map((config) => {
const FormComponent = componentsMap.get(config.component);
return (
<Grid key={`form-field-${config.config.name}`} item xs={12}>
<FormComponent {...config.config} register={register} />
</Grid>
);
})}
</Grid>
</AppCard>
</form>
);
};
N.B. The images are the same because the forms will contain the same information, i.e. the same form fields by name.
Entry for first form:
Entry for the second form:
Each form is created with a config like this:
{
component: DynamicForm,
label: "Stepper Label",
config: {
headline: "Form 1",
actionButtonText: "Next",
formName: 'form-name',
defaultValues: defaultConfigObject,
formFields: [
{
component: "AppTextInput",
config: {
label: "Field 1",
name: "field_1",
},
},
{
component: "AppTextInput",
config: {
label: "Field2",
name: "field_2",
},
},
{
component: "AppTextInput",
config: {
label: "Field3",
name: "field_3",
},
},
{
component: "AppTextInput",
config: {
label: "Field4",
name: "field4",
},
},
],
handleSubmit: (formData) => console.log(formData),
},
},
And the active component in the steps is handled like:
import { Button, createStyles, makeStyles, Theme } from "#material-ui/core";
import React, { useContext, useEffect, useState } from "react";
import { StepperContext } from "./StepperProvider";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
buttonsContainer: {
margin: theme.spacing(2),
},
buttons: {
display: "flex",
justifyContent: "space-between",
},
})
);
export const StepPanel = (props) => {
const { activeStep, steps, handleBack, handleNext, isFinalStep } = useContext(
StepperContext
);
const [activeComponent, setActiveComponent] = useState(steps[activeStep]);
const classes = useStyles();
useEffect(() => {
setActiveComponent(steps[activeStep]);
}, [activeStep]);
return (
<div>
<activeComponent.component {...activeComponent.config} />
{
isFinalStep ? (
<div className={classes.buttonsContainer}>
<div className={classes.buttons}>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button
variant="contained"
color="primary"
onClick={props.finishFunction}
>
Finish And Submit
</Button>
</div>
</div>
)
:
null
}
</div>
);
};
From your image, every form looks the same. You should try providing a unique key value to your component so React know that each form is different. In this case it can be the step number for example:
<activeComponent.component {...props} key='form-step-1'>

React Native login form

I am trying to create Login page in React native using functional component. But it is not working. As soon as enter any text throwing error. value is not changing.
import React from "react";
import { View, Button, Text } from "react-native";
import Inputs from "../../utils/Form/Input";
const LoginForm = () => {
const [formData, setForm] = React.useState({
email: {
value: "",
valid: false,
type: "textinput",
rules: {
isRequired: true,
isEmail: true
}
},
password: {
value: "",
valid: false,
type: "textinput",
rules: {
isRequired: true,
minLength: true
}
}
});
const handleChange = () => {
setForm({ ...formData });
console.log(formData.email);
};
return (
<View>
<Text>Login</Text>
<Inputs
placeholder="Enter email address"
placeholdercolor="red"
autoCapitalize={"none"}
keyboardType={"email-address"}
onChangeText={value => handleChange("email", value)}
value={formData.email.value}
type={formData.email.type}
/>
<Inputs
placeholder="Password"
placeholdercolor="red"
autoCapitalize={"none"}
type={formData.password.type}
value={formData.password.value}
onChangeText={value => setForm("password", value)}
/>
</View>
);
};
export default LoginForm;
Util file
import React from "react";
import { View, Button, TextInput, Picker, StyleSheet } from "react-native";
const Inputs = props => {
let template = null;
switch (props.type) {
case "textinput":
template = (
<TextInput {...props} style={[styles.input, props.overrideStyle]} />
);
break;
default:
return template;
}
return template;
};
const styles = StyleSheet.create({
input: {
width: "100%",
borderBottomWidth: 2,
borderBottomColor: "blue",
fontSize: 16,
padding: 5,
marginTop: 10
}
});
export default Inputs;
You are missing parameters in handleChange function. It should be
const handleChange = (key, value) => {
let data = formData;
data[key].value = value;
setForm(data);
console.log(formData.email);
};
your handleChange change function is not proper and producing error, change your handle change method to this function
const handleChange = (val, data) => {
if (val === 'email') {
setForm({
...formData,
email: {
...formData.email,
value: data,
},
});
} else {
setForm({
...formData,
password: {
...formData.password,
value: data,
},
});
}
};
and change your onChangeText prop of password input to
onChangeText={value => handleChange('password', value)}

Resources