My problem is that currently the ref returns a validation message for only the last component, when I want it return from both or either one if the input component is empty.
I have tried to add a index and get undefined value or if I dynamically
added it the ref with a unique value 'name'
I read somewhere that the ref need to be unique - but how do I make it unique?
May aim is trigger a validation function from the child component to show an the errors message on form submit.
can anyone suggest how I can do this.
PARENT
class MainForm extends Component {
handleSubmit(e) {
e.preventDefault();
this.inputRef.handleInputChange(e);
}
render() {
const nameFields = [
{
id: 'firstame', type: 'text', name: 'firstname', label: 'First name', required: true,
},
{
id: 'lastName', type: 'text', name: 'lastname', label: 'Last name', required: true,
},
];
return (
<div>
<form onSubmit={e => (this.handleSubmit(e))} noValidate>
<div className="form__field--background">
<p> Please enter your name so we can match any updates with our database.</p>
<div>
{
nameFields.map(element => (
<div key={element.id} className="form__field--wrapper">
<InputField
type={element.type}
id={element.name}
name={element.name}
required={element.required}
placeholder={element.placeholder}
label={element.label}
pattern={element.pattern}
isValid={(e, name, value) => {
this.inputFieldValidation(e, name, this.handleFieldValues(name, value));
}}
ref={c => this.inputRef[`${element.name}`] = c}
/>
</div>
))
}
</div>
</div>
<button type="submit" className="btn btn--red">Update Preferences</button>
</form>
</div>
);
}
}
export default MainForm;
CHILD
class InputField extends Component {
constructor() {
super();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleOnBlur = this.handleOnBlur.bind(this);
this.state = {
valid: null,
message: '',
};
}
/**
* Calls helper function to validate the input field
* Sets the the state for the validation and validation message
*/
validateField(e) {
const props = {
field: e.target,
value: e.target.value,
label: this.props.label,
required: this.props.required,
min: this.props.min,
max: this.props.max,
pattern: this.props.pattern,
emptyError: this.props.emptyFieldErrorText,
invalidError: this.props.invalidErrorText,
};
let validation = this.state;
// helper function will return an updated validation object
validation = fieldValidation(props, validation);
this.setState(validation);
return validation;
}
/**
* Calls validateField method if field is a checkbox.
* Handles the callback isValid state to parent component.
*/
handleInputChange(e) {
if ((e.target.required && e.target.type === 'checkbox')) {
this.validateField(e);
}
}
handleOnBlur(e) {
if (e.target.type !== 'checkbox') {
this.validateField(e);
}
}
render() {
return (
<div >
<label id={`field-label--${this.props.id}`} htmlFor={`field-input--${this.props.id}`}>
{this.props.label}
</label>
{this.props.helpText &&
<p className="form-help-text">{this.props.helpText}</p>
}
<input
type={this.props.type}
id={`field-input--${this.props.id}`}
name={this.props.name && this.props.name}
required={this.props.required && this.props.required}
placeholder={this.props.placeholder && this.props.placeholder}
onBlur={e => this.handleOnBlur(e)}
onChange={e => this.handleInputChange(e)}
ref={this.props.inputRef}
/>
}
{this.state.valid === false &&
<span className="form-error">
{this.state.message}
</span>
}
</div>
);
}
}
export default InputField;
Related
Before question, sorry for massy code because I'm newbie at code.
I made Input component by PureComponent.
But I have no idea how to control(like change state) & submit its value in Parent component.
this is Input PureComponent I made:
index.jsx
class Input extends PureComponent {
constructor(props) {
super(props);
this.state = {
value: props.value,
name: props.value,
};
this.setRef = this.setRef.bind(this);
this.handleChange = this.handleChange.bind(this);
}
setRef(ref) {
this.ref = ref;
}
handleChange(e) {
const { name, onChange, type } = this.props;
onChange(name, e.target.value);
this.setState({ isInputError: checkError, value: e.target.value });
}
render() {
const { label, name, type} = this.props;
return (
<div className="inputBox">
<input
id={name}
value={this.state.value}
type={type}
ref={this.setRef}
onChange={this.handleChange}
className="BlueInput"
/>
<label>{label}</label>
</div>
);
}
}
Input.propTypes = {
label: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
type: PropTypes.oneOf(["text", "password", "number", "price", "tel"]),
onChange: PropTypes.func,
}
Input.defaultProps = {
value: "",
type: "text",
onChange: () => { },
}
export default Input;
And this is Parent component where I want to control value
Join.js
function Join() {
const [idMessage, setIdMessage] = useState("");
const [isId, setIsId] = useState(false);
const onChangeId = (name, value) => {
const currentId = value;
const idRegExp = /^[a-zA-z0-9]{4,12}$/;
if (!idRegExp.test(currentId)) {
setIdMessage("Wrong format");
setIsId(false);
} else {
setIdMessage("You can use this");
setIsId(true);
}
}
const handleSubmit = (e) => {
e.preventDefault();
let formData = new FormData();
console.log(formData);
alert("Join completed.");
}
return (
<div className="join-wrap">
<form encType="multipart/form-data" onSubmit={handleSubmit}>
<Input name="id" label="ID" onChange={onChangeId} />
<p className={isId ? 'possible' : 'impossible'}>{idMessage}</p>
<button type="submit">Join</button>
</form>
</div>
)
}
export default Join;
How can I control or submit value?
For anyone landing here with a similar question, I would advise you to use functional components. For the custom input component, you do not really need to define any state in the custom component. You can pass the state (value of input) as well as the change handler as prop to the child component. In case you need refs, use forwardRef.
Intentionally, I am not spoon-feeding here. I am just giving food for thought and some keyword to do a quick research and enhance your skills.
index.js
handleChange(e, fieldName) {
this.props.setState({...this.props.state, [fieldName]:e.target.value})
}
-----------------------
<input
id={this.props.name}
value={this.props.state[name]}
type={type} // you can also add type in state
onChange={(e)=>this.handleChange(e, name)}
className="BlueInput"
/>
join.js
const [state,setState] = useState({fName:'',lNAme:''})
{Object.keys(state).map((value)=>{
return <Input name={value} label="ID" state={state} setState = {setState}/>
})}
I am trying to wrap content in my React app to use a different top-level element.
In my state I have defined wrapContent: false
I have defined a toggleWrap method.
toggleWrap = () => {
this.setState(state => state.wrapContent = !state.wrapContent);
}
And finally in my input checkbox I have included the toggleWrap method in
onChange={this.toggleWrap}
Unfortunately when I run my code, pressing on the checkbox does not wrap my component content in the following code inside my wrapContent method
wrapContent(content){
return this.state.wrapContent
? <div className="bg-secondary p-2">
<div className="bg-light"> {content} </div>
which wraps the contents of my render(){} return in this component.
What is strange is that when I manually change wrapContent: false in my state to True, I see that the checkbox is checked in my browser and that the code within my render is properly wrapped.
So it seems that just my toggleWrap function is not working as it should.
Could someone help here?
Full code:
import React, { Component } from 'react'
import { ValidationDisplay } from './ValidationDisplay';
import { GetValidationMessages } from './ValidationMessages';
export class Editor extends Component {
constructor(props) {
super(props);
this.formElements = {
name: { label: "Name", name: "name", validation: { required: true, minLength: 3 } },
category: { label: "Category", name: "category", validation: { required: true, minLength: 5 } },
price: { label: "Price", name: "price", validation: { type: "number", required: true, min: 5 } }
}
this.state = {
errors: {},
wrapContent: false
}
}
// 06.11.21- method is invoked when the content is rendered
setElement = (element) => {
if (element !== null) {
this.formElements[element.name].element = element;
}
}
// handleChange = (event) => {
// event.persist()
// this.setState(state => state[event.target.name] = event.target.value);
// }
handleAdd = () => {
if (this.validateFormElements()) {
let data = {};
Object.values(this.formElements)
.forEach(v => {
data[v.element.name] = v.element.value;
v.element.value = "";
});
this.props.callback(data);
this.formElements.name.element.focus();
}
}
validateFormElement = (name) => {
let errors = GetValidationMessages(this.formElements[name].element);
this.setState(state => state.errors[name] = errors);
return errors.length === 0;
}
validateFormElements = () => {
let valid = true;
Object.keys(this.formElements).forEach(name => {
if (!this.validateFormElement(name)) {
valid = false;
}
})
return valid;
}
toggleWrap = () => {
this.setState(state => state.wrapContent = !state.wrapContent);
}
wrapContent(content) {
return this.state.wrapContent
? <div className="bg-secondary p-2">
<div className="bg-light"> {content} </div>
</div>
: content;
}
render() {
return this.wrapContent(
<React.Fragment>
<div className="form-group text-center p-2">
<div className="form-check">
<input className="form-check-input"
type="checkbox"
checked={this.state.wrapContent}
onChange={this.toggleWrap} />
<label className="form-check-label">Wrap Content</label>
</div>
</div>
{
Object.values(this.formElements).map(elem =>
<div className="form-group p-2" key={elem.name}>
<label>{elem.label}</label>
<input className="form-control"
name={elem.name}
autoFocus={elem.name === "name"}
ref={this.setElement}
onChange={() => this.validateFormElement(elem.name)}
{...elem.validation} />
<ValidationDisplay
errors={this.state.errors[elem.name]} />
</div>)
}
<div className="text-center">
<button className="btn btn-primary" onClick={this.handleAdd}>
Add
</button>
</div>
</React.Fragment>)
}
}
This was an issue with how you update the state
Try below
toggleWrap = () => {
this.setState({ wrapContent: !this.state.wrapContent });
};
or
toggleWrap = () => {
this.setState((state) => {
return { ...state, wrapContent: !this.state.wrapContent };
});
};
instead of
toggleWrap = () => {
this.setState(state => state.wrapContent = !state.wrapContent);
}
I am just started on react and I have a challenge with implementing dynamic checkboxes. I have the checkbox items in a firestore collection and fetch them as an array which I store into something that looks like this:
const checkboxes = [
{
name: 'check-box-1',
key: 'checkBox1',
label: 'Check Box 1',
id: 'first',
value: false
},
{
name: 'check-box-2',
key: 'checkBox2',
label: 'Check Box 2',
id: 'second',
value: true
},
{
name: 'check-box-3',
key: 'checkBox3',
label: 'Check Box 3',
id: 'third',
value: false
},
{
name: 'check-box-4',
key: 'checkBox4',
label: 'Check Box 4',
id: 'fourth',
value: false
},
];
My form is in a class component and I render it like this
class CheckIn extends Component {
state = {
data1: ''
}
render() {
return(
<div>
{
checkboxes && checkboxes
.map(checkbox => {
return(
<div className="checkbox-area" key={checkbox.id}>
<label htmlFor={ checkbox.id }>
<input type="checkbox"
name="check-box"
id={checkbox.id}
value = {checkbox.value}
onChange = {this.handleCheck}
className='filled-in' />
<span>{checkbox.label}</span>
</label>
</div>
)
})
}
</div>
)
}
}
I am not sure what to do in my handleCheck in order for the form to update state as true if it is checked and false it it is unchecked. For now, this only updates the checked ones and sends 'undefined'
handleCheck = (e) => {
this.setState({
[e.target.id] : true
})
}
Please help. Thanks
For checkbox type input, the value is checked not value.
This should work for you:
<div className="checkbox-area" key={checkbox.id}>
<label htmlFor={ checkbox.id }>
<input type="checkbox"
name="check-box"
id={checkbox.id}
checked={checkbox.value}
onChange={this.handleCheck}
className='filled-in' />
<span>{checkbox.label}</span>
</label>
</div>
handleCheck = e => {
this.setState({
[e.target.id]: e.target.checked
})
}
Based on your needed in your comment on #ulou answer post
You need to set all checkboxs state false first after component is mounted
class CheckIn extends Component {
constructor() {
super();
state = {
data1: ''
}
}
componentdidmount(){
checkboxes.map(item => {
this.setState({
[item.id]: false
})
}
}
render() {
return(
<div>
{ checkboxes && checkboxes
.map(checkbox => {
return(
<div className="checkbox-area" key={checkbox.id}>
<label htmlFor={ checkbox.id }>
<input type="checkbox"
name="check-box"
id={checkbox.id}
value = {checkbox.value}
onChange = {this.handleCheck}
className='filled-in' />
<span>{checkbox.label}</span>
</label>
</div>
)
})
}
</div>
)
}
}
I want the focus on an "input" element to remain after keypress
The Code:
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<NewContactBox/>
)
}
}
class NewContactBox extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
email: '',
phone: '',
}
this.fieldRefs = {
name: React.createRef(),
email: React.createRef(),
phone: React.createRef(),
}
}
hc = (ev, type, ref) => {
this.setState({
[type]: ev.target.value
})
// console.log(ref)
ref.focus()
}
render() {
const ContactInput = ({fields}) => (
fields.map((f) => (
<input
key={f.id}
className={`p-1 my-3 w-100 mr-3 ${f.id}`}
size="16px"
placeholder={f.placeholder}
value={this.state[f.id]}
ref={this.fieldRefs[f.id]}
onChange={(e) => this.hc(e, f.id, this.fieldRefs[f.id].current)}
/>
))
)
return (
<ContactInput
fields={[
{ placeholder: "Name", id: 'name' },
{ placeholder: "Phone number", id: 'phone' },
{ placeholder: "Email", id: 'email' },
]}
/>
)
}
}
export default App
I've tried
Change01 - declaring the refs inside the Input tag in another way
Change02 - not passing the ref to onChange explicitly and then accessing the ref directly from this.fieldrefs
constructor(props) {
this.fieldRefs = {} // Change01
}
hc = (ev, type) => { //Change02
this.setState({
[type]: ev.target.value
})
// console.log(this.fieldRefs[type].current)
this.fieldRefs[type].current.focus()
}
...
<input
...
ref={(el) => this.fieldRefs[f.id] = el} //Change01
onChange={(e) => this.hc(e, f.id)} //Change02
/>
But it didn't help, and after every keypress, the body element became the active element.
Maybe you need to move your ContactInput declaration outside of your render(), else you will be recreating a new component every time it rerenders. For example,
render() {
return (
<this.ContactInput
fields={[
{ placeholder: "Name", id: 'name' },
{ placeholder: "Phone number", id: 'phone' },
{ placeholder: "Email", id: 'email' },
]}
/>
)
}
ContactInput = ({fields}) => (
// If react complains about returning multiple component, add this React.Fragment short syntax
<React.Fragment>
{fields.map((f) => (
<input
key={f.id}
className={`p-1 my-3 w-100 mr-3 ${f.id}`}
size="16px"
placeholder={f.placeholder}
value={this.state[f.id]}
ref={this.fieldRefs[f.id]}
onChange={(e) => this.hc(e, f.id, this.fieldRefs[f.id].current)}
/>
))}
<React.Fragment/>
)
I'm generating form field elements using a component template importing an array of data. I want to be able to hide some of these elements and show them when some other's element's criteria has been met; this is fairly common in form fields, e.g. When you select item A, form field X appears, when you select item B, form field X is hidden.
I've read a fair bit on conditional rendering on the React docs site but these examples don't really work for what I'm doing although the Preventing Component from Rendering section is close.
I made a Codepen demoing my setup, what I want to do is show the second field if the criteria for the first field is met (in this example, the first field should have 5 characters entered). I've passed through a prop to set the initial hiding but how can I find that specific hidden element and unhide it?
// Field data
const fieldData = [{
"type": "text",
"label": "First",
"name": "first",
"placeholder": "Enter first name",
"hidden": false
}, {
"type": "text",
"label": "Surname",
"name": "surname",
"placeholder": "Enter surname",
"hidden": true
}];
// Get form data
class FormData extends React.Component {
constructor(props) {
super(props);
this.state = {
items: props.data
};
}
render() {
let els = this.state.items;
return els.map((el, i) => {
return <Input key={i} params={el} />
});
}
}
// Input builder
class Input extends React.Component {
constructor(props) {
super(props);
// Keep state value
this.state = {
value: '',
valid: false,
hidden: this.props.params.hidden
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({
value: e.target.value,
valid: e.target.value.length < 5 ? false : true
});
}
render() {
// Element attributes
const {type, label, name, placeholder, hidden} = this.props.params;
const isValid = this.state.valid === true ? <span>Valid! Should show Surname field.</span> : <span>Not valid. Should hide Surname field.</span>;
if (!hidden) {
return (
<div>
{label ? <label htmlFor={name}>{label}</label> : null}
<input
type={type}
name={name}
placeholder={placeholder || null}
value={this.state.value}
onChange={this.handleChange}
/>
{isValid}
</div>
);
} else {
return null;
}
}
}
// App
class App extends React.Component {
render() {
return (
<div>
<h1>Show/Hide test</h1>
<p>What we want here is the surname to appear when firstname has a value (say, it has 5 characters) and hide surname when firstname doesn't.</p>
<FormData data={fieldData} />
</div>
);
}
}
ReactDOM.render(
<form>
<App />
</form>,
document.getElementById('app')
);
You can lift the state up so a parent of the Input will handle the state and validations.
You can conditionally invoke the "validator" of the surname property or any other property only if it exists and do a convention with yourself that a validate method will get the name of: propertNameValidator .
So for example when you do the loop over the inputs, you could check if there is
a validate method named surnameValidator and invoke it against the hidden prop that you will pass the Input, if it is not exists then just pass false.
Here is a small example with your code:
// Field data
const fieldData = [
{
type: "text",
label: "First",
name: "first",
placeholder: "Enter first name",
hidden: false
},
{
type: "text",
label: "Surname",
name: "surname",
placeholder: "Enter surname",
hidden: true
}
];
// Get form data
class FormData extends React.Component {
constructor(props) {
super(props);
this.state = {
items: props.data.map(el => ({...el, value: ''})) // add the value property
};
}
onInputChange = (inputId, value) => {
const { items } = this.state;
const nextState = items.map((item) => {
if (inputId !== item.name) return item;
return {
...item,
value,
}
});
this.setState({ items: nextState });
}
surnameValidator = () => {
const { items } = this.state;
const nameElement = items.find(item => item.name === 'first');
return nameElement && nameElement.value.length >= 5
}
render() {
let els = this.state.items;
return (
<div>
{
els.map((el, i) => {
const validator = this[`${el.name}Validator`];
return (
<Input
key={i}
{...el}
inputId={el.name}
hidden={validator ? !validator() : el.hidden}
onInputChange={this.onInputChange}
/>
);
})
}
</div>
)
}
}
// Input builder
class Input extends React.Component {
handleChange = ({ target }) => {
const { inputId, onInputChange } = this.props;
onInputChange(inputId, target.value);
}
render() {
// Element attributes
const { type, label, name, placeholder, hidden, value } = this.props;
return (
<div>
{
hidden ? '' : (
<div>
{label ? <label htmlFor={name}>{label}</label> : null}
<input
type={type}
name={name}
placeholder={placeholder || null}
value={value}
onChange={this.handleChange}
/>
</div>
)
}
</div>
);
}
}
// App
class App extends React.Component {
render() {
return (
<div>
<h1>Show/Hide test</h1>
<p>
What we want here is the surname to appear when firstname has a value
(say, it has 5 characters) and hide surname when firstname doesn't.
</p>
<FormData data={fieldData} />
</div>
);
}
}
ReactDOM.render(
<form>
<App />
</form>,
document.getElementById("app")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
If you already created the element and you just want to hide / show it, then you can conditionally add or remove a CSS class that hides it.
<input className={`${!isValid && 'hide'}`} />
If I'm not mistaken, what you want is to conditionally show or hide an input element. See if this helps.
render() {
// Element attributes
const {type, label, name, placeholder, hidden} = this.props.params;
const isValid = this.state.valid === true ? <span>Valid! Should show Surname field.</span> : <span>Not valid. Should hide Surname field.</span>;
if (!hidden) {
return (
<div>
{label ? <label htmlFor={name}>{label}</label> : null}
<input
type={type}
name={name}
placeholder={placeholder || null}
value={this.state.value}
onChange={this.handleChange}
/>
{this.state.valid && <input placeholder="add surname" />}
</div>
);
} else {
return null;
}
}