React.js - Show/Hide dynamically generated elements from component - javascript

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

Related

I want to control value of input in parent component (Child is PureComponent)

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

How can I open only the clicked row data in React?

I have defined a state variable called fullText.
fullText default value is false.
When Full Text is clicked, I want to reverse my state value and enable the text to be closed and opened. But when it is clicked, the texts in all rows in the table are opened, how can I make it row specific?
{
!this.state.fullText ?
<div onClick={() => this.setState({ fullText: !this.state.fullText })}
className="text-primary cursor-pointer font-size-14 mt-2 px-2"
key={props.data?.id}>
Full Text
</div>
:
<>
<div onClick={() => this.setState({ fullText: !this.state.fullText
})}className="text-primary cursor-pointer font-size-14 mt-2 px-2"
key={props.data?.id}>
Short Text
</div>
<span>{ props.data?.caption}</span>
</>
}
It appears that the code sample in the question is repeated for each row, but there is only 1 state fullText (showMore in the CodeSandbox) that is common for all these rows. Hence they all open or close together.
If you want individual open/close feature for each row, then you need 1 such state per row. An easy solution could be to embed this state with the data of each row:
export default class App extends Component {
state = {
data: [
{
id: 1,
description: "aaaaa",
showMore: false // Initially closed
},
// etc.
]
};
render() {
return (
<>
{this.state.data.map((e) => (
<>
{ /* Read the showMore state of the row, instead of a shared state */
e.showMore === false ? (
<div onClick={() => this.setState({ data: changeShow(this.state.data, e.id, true) })}>
Show description{" "}
</div>
) : (
<>
<div onClick={() => this.setState({ data: changeShow(this.state.data, e.id, false) })}>
Show less{" "}
</div>
<span key={e.id}>{e.description} </span>
</>
)}
</>
))}
</>
);
}
}
/**
* Update the showMore property of the
* row with the given id.
*/
function changeShow(data, id, show) {
for (const row of data) {
if (row.id === id) { // Find the matching row id
row.showMore = show; // Update the property
}
}
return data;
}
there are two specific desired outputs you might want. you can either make it open only one row at a time. you can also make them individual row specific to open or close independently.
For making specific row behave on its own you can store a list of ids in state for the expanded view and show the relevant component or behaviour.
class RowSpecific extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedIds: [],
data: [
{ id: "1", otherInfo: {} },
{ id: "2", otherInfo: {} },
{ id: "3", otherInfo: {} },
],
};
this.handleExpandHide = this.handleExpandHide.bind(this);
}
handleExpandHide(expand, id) {
if (!id) return;
let sIds = [...this.state.selectedIds];
if (expand) sIds.push(id);
else sIds.remove((rId) => rId === id);
this.setState({
selectedIds: sIds,
});
}
render() {
let list = this.state.data.map((data) => {
let show = this.state.selectedIds.find((id) => data.id === id);
return show ? (
<ExpandedView
data={data}
onExpandHide={(e) => {
this.handleExpandHide(true, data.id);
}}
/>
) : (
<CollapseView
data={data}
onExpandHide={(e) => {
this.handleExpandHide(true, data.id);
}}
/>
);
});
return list;
}
}
For making it open only one at a time you can save the clicked id in state and then when showing it only the matched id in state should be shown as open . rest are closed.
class SingleRow extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedId: "2",
data: [
{ id: "1", otherInfo: {} },
{ id: "2", otherInfo: {} },
{ id: "3", otherInfo: {} },
],
};
}
render() {
let list = this.state.data.map((data) => {
let show = data.id === this.state.selectedId;
return show ? <ExpandedView data={data} /> : <CollapseView data={data} />;
});
return list;
}
}

react input validation checking against last entered value instead of current value

I'm trying to validate my input , if the number is between 100 and 200 is should display valid or invalid , the problem i'm having is that it seems to be checking against the last entered value, so for instance if the user enters 1222 this will display valid as I believe it is actually checking against the last entered number of 122 and also if I then delete 2 charachers so it displays 12 this will also display valid. I believe this is because of how the state is set but I am not sure how to correctly get this to work.
How can I change this so it will check against the correct value and validate correctly?
Textbox.js
class TextBox extends React.Component {
state = {
valid: '',
value: ''
}
onChange = (event) => {
this.props.onChange(event.target.value);
this.validation()
}
validation() {
(this.props.value > 100 && this.props.value < 200) ? this.setState({value: this.props.value}) : this.setState({value: this.props.value})
}
onChangeInput(e) {
const { name, value } = e.target;
this.setState({
[name]: value
}, () => console.log(this.state.mail === this.state.confMail));
}
render() {
return (
<Box
invalid={!this.props.isValid}
>
<Label
rollo={this.props.designation === 'rollo'}
pleating={this.props.designation === 'pleating'}
>{this.props.label}</Label>
<span>
<Input
type={this.props.type && this.props.type}
defaultValue={this.props.defaultValue}
onChange={this.onChange}
placeholder={this.props.placeholder}
value={this.props.value || ''}
/>
<Tooltip>{this.state.valid}</Tooltip>
</span>
</Box>
);
}
};
export default TextBox;
component.js
<TextBox
type="number"
label="Fenstertyp"
defaultValue={ this.props.height / 100 * 66 }
onChange={newValue => this.props.selectOperationChainLength(newValue)}
tooltip="invalid"
value={this.props.operationChainLength.value}
/>
actions.js
export function selectOperationChainLength(operationChainLength) {
return {
type: SELECT_OPERATION_CHAIN_LENGTH,
operationChainLength
}
}
You can shift the validation logic to onChange method on event.target.value, there is no need to create the separate method. It will then look like this.
class TextBox extends React.Component {
state = {
valid: false,
value: ''
}
onChange = (event) => {
const value = event.target.value;
(value > 100 && value < 200) ? this.setState({value, valid: true}) : this.setState({value, valid: false})
this.props.onChange(value);
}
onChangeInput(e) {
const { name, value } = e.target;
this.setState({
[name]: value
}, () => console.log(this.state.mail === this.state.confMail));
}
render() {
return (
<Box
invalid={!this.props.isValid}
>
<Label
rollo={this.props.designation === 'rollo'}
pleating={this.props.designation === 'pleating'}
>{this.props.label}</Label>
<span>
<Input
type={this.props.type && this.props.type}
defaultValue={this.props.defaultValue}
onChange={this.onChange}
placeholder={this.props.placeholder}
value={this.props.value || ''}
/>
<Tooltip>{this.state.valid}</Tooltip>
</span>
</Box>
);
}
};
export default TextBox;
Ok, so there are some things going wrong here.
import React, { Component } from "react";
import { render } from "react-dom";
class TextBox extends Component {
constructor(props){
super(props);
// 1.
// You only need to store the `isValid` property.
// The value is needed only for the validation, right?
this.state = {
isValid: false
}
}
onChange(e) {
const { target } = e;
const { value } = target;
// 2.
// If the value is in the right range : isValid = true
// else : isValid = false
if( value > 100 && value < 200 ) {
this.setState({ isValid: true });
} else {
this.setState({ isValid: false });
}
}
render() {
// 3.
// Always use destructuring. It's way easier to follow ;)
const { type } = this.props;
const { isValid } = this.state;
return (
<Fragment>
<input
type={type}
onChange={e => this.onChange(e)}
/>
{/* 4. */}
{/* Assign the right text to your tooltip */}
<p>{ isValid ? "valid" : "invalid" }</p>
</Fragment>
);
}
}
ReactDOM.render(
<TextBox type="number" />,
document.getElementById('root')
);
<div id="root"></div>
<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>
I simplified the example so it can be easier to follow.
Here is a working example
I think you need to set your valid as false to begin with
state = {
valid: false,
value: ''
}

How do I make ref unique on multiple component react

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;

Show and Hide specific component in React from a loop

I have a button for each div. And when I press on it, it has to show the div with the same key, and hide the others.
What is the best way to do it ? This is my code
class Main extends Component {
constructor(props) {
super(props);
this.state = {
messages: [
{ message: "message1", key: "1" },
{ message: "message2", key: "2" }
]
};
}
handleClick(message) {
//something to show the specific component and hide the others
}
render() {
let messageNodes = this.state.messages.map(message => {
return (
<Button key={message.key} onClick={e => this.handleClick(message)}>
{message.message}
</Button>
)
});
let messageNodes2 = this.state.messages.map(message => {
return <div key={message.key}>
<p>{message.message}</p>
</div>
});
return <div>
<div>{messageNodes}</div>
<div>{messageNodes2}</div>
</div>
}
}
import React from "react";
import { render } from "react-dom";
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: [
{ message: "message1", id: "1" },
{ message: "message2", id: "2" }
],
openedMessage: false
};
}
handleClick(id) {
const currentmessage = this.state.messages.filter(item => item.id === id);
this.setState({ openedMessage: currentmessage });
}
render() {
let messageNodes = this.state.messages.map(message => {
return (
<button key={message.id} onClick={e => this.handleClick(message.id)}>
{message.message}
</button>
);
});
let messageNodes2 = this.state.messages.map(message => {
return (
<div key={message.key}>
<p>{message.message}</p>
</div>
);
});
const { openedMessage } = this.state;
console.log(openedMessage);
return (
<div>
{openedMessage ? (
<div>
{openedMessage.map(item => (
<div>
{" "}
{item.id} {item.message}{" "}
</div>
))}
</div>
) : (
<div> Not Opened</div>
)}
{!openedMessage && messageNodes}
</div>
);
}
}
render(<Main />, document.getElementById("root"));
The main concept here is this following line of code.
handleClick(id) {
const currentmessage = this.state.messages.filter(item => item.id === id);
this.setState({ openedMessage: currentmessage });
}`
When we map our messageNodes we pass down the messages id. When a message is clicked the id of that message is passed to the handleClick and we filter all the messages that do not contain the id of the clicked message. Then if there is an openedMessage in state we render the message, but at the same time we stop rendering the message nodes, with this logic {!openedMessage && messageNodes}
Something like this. You should keep in state only message key of visible component and in render method you should render only visible component based on the key preserved in state. Since you have array of message objects in state, use it to render only button that matches the key.
class Main extends Component {
constructor(props) {
super(props);
this.state = {
//My array messages: [],
visibleComponentKey: '',
showAll: true
};
handleClick(message) {
//something to show the specific component and hide the others
// preserve in state visible component
this.setState({visibleComponentKey : message.key, showAll: false});
};
render() {
const {visibleComponentKey, showAll} = this.state;
return (
<div>
{!! visibleComponentKey && ! showAll &&
this.state.messages.filter(message => {
return message.key == visibleComponentKey ? <Button onClick={e => this.handleClick(message)}>{message.message}</Button>
) : <div /> })
}
{ !! showAll &&
this.state.messages.map(message => <Button key={message.key} onClick={e => this.handleClick(message)}>{message.message}</Button>)
}
</div>
);
}
}
I haven't tried it but it gives you a basic idea.
I cannot reply to #Omar directly but let me tell you, this is the best code explanation for what i was looking for! Thank you!
Also, to close, I added a handleClose function that set the state back to false. Worked like a charm!
onCloseItem =(event) => {
event.preventDefault();
this.setState({
openedItem: false
});
}

Categories

Resources