ReactJS manage lots of select state - javascript

Given I have this code (I removed a lot of the select items so it wasn't a massive list but there would be an extra 20 or so):
import * as React from "react";
import { Form, Card, Grid } from "tabler-react";
import { Button } from "semantic-ui-react";
class Skills extends React.Component {
constructor(props) {
super(props);
this.state = {
showSaveButton: false,
showCancelButton: false,
};
}
onChange = (event) => {
this.setState(
{
showSaveButton: true,
showCancelButton: true,
});
}
cancelChanges = () => {
this.setState(
{
showSaveButton: false,
showCancelButton: false,
});
}
render() {
return (
<div className="card" name="skills">
<Card.Body>
<Card.Title>Top skills</Card.Title>
<Grid.Row>
<Grid.Col offset={1} md={10}>
<Form.Group name="softskills" label="Soft Skills">
<Form.SelectGroup canSelectMultiple pills onChange={this.onChange}>
<Form.SelectGroupItem
label="Communication"
name="communication"
value="Communication"
/>
<Form.SelectGroupItem
label="Teamwork"
name="teamwork"
value="Teamwork"
/>
</Form.SelectGroup>
</Form.Group>
</Grid.Col>
</Grid.Row>
<Button content='Cancel changes' floated='left' color='red' basic hidden={this.state.showCancelButton ? '' : 'hidden'} onClick={this.cancelChanges}/>
<Button content='Save changes' floated='right' color='green' basic hidden={this.state.showSaveButton ? '' : 'hidden'}/>
</Card.Body>
</div>
);
}
}
export default Skills;
The current functionality is that on change, 2 buttons will appear that are cancel or accept.
I need the below functionality but I can't work out how to do it unless I have like 60+ states (an initial and a working state for each option) which seems ridiculous:
The initial state is pulled from a database in a JSON array whereby everything that appears in that array should start out as selected (checked=true). For example, if the array is ["communication", "timemanagement"] I need to set the Communication and Time Management options to checked=true.
The initial state needs to be saved so that if anything changes and then the user clicks cancel, the checked boolean for each option is reset to what it was originally
If accept is clicked, the information needs to be sent to the database and so it needs to know what options have checked=true and be able to grab their names
So is there a way to do this without having a massive amount of states?

What you can do is create a mapping in state for all 60. When you get the results from the database, store them in state with fields to track checked and changed statuses:
// isDirty is a flag to say there are pending changes on the option
const options = arrayFromDatabase.map(arrayElement => ({ name: arrayElement, checked: true, isDirty: false })
then store that array in your state, e.g.,
this.setState({...this.state, options })
When a change is made, mark the specific option as dirty -> isDirty = true. If it's cancelled, flip the flag back to false -> isDirty = false.
Should look something like,
this.setState({
...state,
options: this.state.map(
option => option.name === valueToChange ? {
...option,
isDirty: true | false } :
option
)
})
Handle your check-changed in the same way.

Related

Manage state and submission of checkboxes

Very new to React. I'm using (the equivalent of) checkboxes as a way for a user to select/deselect their skills and I'm wondering what's the best way to handle the state?
Essentially the flow is like this:
Initial values are pulled from the database. The API will provide a
list of skills (by name) and all of those skills are the ones that should
start out as checked whilst the others are unchecked
The user can freely check/uncheck boxes and if they click the cancel button the states will return to the initial values
If the user presses the submit button, the state will be changed and the form submitted with the value
I've done the same thing on another page but that was text fields and there was only 5 of them so I just stored their values in the state individually. Since I have over 30 checkboxes that seems like it'd be very messy. Is there a good way to condense this sort of thing and also how would I map this?
They are checked/unchecked as indicated by their property "checked: (true/false)"
This is the code at the moment (I removed most of the select items so it wasn't a massive list):
import * as React from "react";
import { Form, Card, Grid } from "tabler-react";
import { Button } from "semantic-ui-react";
class Skills extends React.Component {
constructor(props) {
super(props);
this.state = {
showSaveButton: false,
showCancelButton: false,
};
}
onChange = (event) => {
this.setState(
{
showSaveButton: true,
showCancelButton: true,
});
if(event.target.checked){
console.log(event.target.checked)
} else {
console.log("not checked")
}
}
cancelChanges = () => {
this.setState(
{
showSaveButton: false,
showCancelButton: false,
});
}
render() {
return (
<div className="card" name="skills">
<Card.Body>
<Card.Title>Top skills</Card.Title>
<Grid.Row>
<Grid.Col offset={1} md={10}>
<Form.Group name="softskills" label="Soft Skills">
<Form.SelectGroup canSelectMultiple pills onChange={this.onChange}>
<Form.SelectGroupItem
label="Communication"
name="communication"
value="Communication"
/>
<Form.SelectGroupItem
label="Teamwork"
name="teamwork"
value="Teamwork"
/>
</Form.SelectGroup>
</Form.Group>
</Grid.Col>
</Grid.Row>
<Button content='Cancel changes' floated='left' color='red' basic hidden={this.state.showCancelButton ? '' : 'hidden'} onClick={this.cancelChanges}/>
<Button content='Save changes' floated='right' color='green' basic hidden={this.state.showSaveButton ? '' : 'hidden'}/>
</Card.Body>
</div>
);
}
}
export default Skills;
With plain react (without redux and hooks) you can maintain a state object of type map for checked items like this
selectedItems: new Map(),
then in onChange event update selctedItems.
onChange = (event) => {
this.setState(
{
showSaveButton: true,
showCancelButton: true,
});
this.setState(prevState => ({
selectedItems: prevState.selectedItems.set(event.target.name, event.target.checked),
showSaveButton: true,
showCancelButton: true
}));
}
React doc has explained this with a nice example on how to maintain state of multiple inputs (here checkboxes).
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}

How to select group based checkbox antd in reactjs

I want to select a group based checkbox. The problem is that when I click on the group, the entire checkbox is selected. I don't want to select the entire checkbox. this is my Initial State.
const plainOptions = ["can_view", "can_create", "can_update"];
state = {
checkedList: [],
indeterminate: true,
checkAll: false
};
Method: onchange method basically works each individual checkbox.
onChange = checkedList => {
console.log(checkedList);
this.setState({
checkedList,
indeterminate:
!!checkedList.length && checkedList.length < plainOptions.length,
checkAll: checkedList.length === plainOptions.length
});
};
This method works for selected all checkbox
onCheckAllChange = e => {
console.log(e.target.checked);
this.setState({
checkedList: e.target.checked ? plainOptions : [],
indeterminate: false,
checkAll: e.target.checked
});
};
{
["group", "topGroup"].map(item => (
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.state.indeterminate}
onChange={this.onCheckAllChange}
checked={this.state.checkAll}
>
{item}
</Checkbox>
<CheckboxGroup
options={plainOptions}
value={this.state.checkedList}
onChange={this.onChange}
/>
</div>
));
}
However, my accepted Data format is
{group:["can_view","can_create"],topGroup:["can_view","can_create"}
I want to get this format output when user selected on the checkbox
Here is the code sandbox : https://codesandbox.io/s/agitated-sea-1ygqu
The reason both groups change when you click something in one of them is because both groups use the same internal state.
["group", "topGroup"].map(item => (
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.state.indeterminate}
onChange={this.onCheckAllChange}
checked={this.state.checkAll}
>
{item}
</Checkbox>
<CheckboxGroup
options={plainOptions}
value={this.state.checkedList}
onChange={this.onChange}
/>
</div>
));
Both the group and topGroup use the same this.state.checkList state.
The easiest way to solve this is by extracting each group into its own component. This way they have their own state separate of each other.
You could also opt to keep one component, but you must manage multiple internal states. You could for example use state = { checkList: [[], []] } where the first sub-array is to store the group state and the second sub-array is to store the topGroup state.
If groups are dynamic you can simply map over the groups and create your states that way:
state = { checkList: groups.map(() => []) };
You would also need to manage multiple indeterminate and checkAll states. This can be avoided when you deduce those from the checkList state. For example:
isIndeterminate(index) {
const checkList = this.state.checkList[index];
return checkList.length > 0 && checkList.length < plainOptions.length;
}
This would also avoid conflicting state, since there is one source of truth.

Can you add a component to state with Hooks?

I have a modal that I'm using to allow editing of individual pieces of a page with a lot of discrete sections. This is a lot more user-friendly than passing them to a form - the form would be enormous.
The sections across the page vary though. Some are simple text and a simple textarea or input will suffice. Some data though can only be edited with a select (or conceivably multiple selects).
For the textareas, I am using the following:
/* inplace-edit dialog */
const [dialog, setDialog] = useState({
open: false, // whether dialog should show
fieldName: null, // reference to field for db update on submit
title: '', // dialog title
content: '', // data to show in dialog content
})
const setDialogState = update => () => {
setDialog({ ...dialog, ...update })
}
As a functional component is essentially a function, is it viable to add that component to the state and then use that component to render the specific form structure when the dialog needs to show?
I've done some more investigation and it seems viable to add a stateless component to state using hooks.
I've modified the state handling code to:
const [dialog, setDialog] = useState({
open: false,
title: '',
formComponent: () => null,
})
const setDialogState = update => () => {
setDialog({ ...dialog, ...update })
}
In the above formComponent is just a default function that returns null.
In a page section that I want to edit, there is a boolean showEditIcons that shows the edit icon if the viewer has appropriate permissions. On clicking the icon, it sets the state. And most importantly, it sets formComponent as a reference to the stateless function:
{showEditIcons && (
<IconButton
onClick={setDialogState({
open: true,
title: 'Project Summary',
formComponent: TextForm,
})}
>
<EditIcon />
</IconButton>
)}
where TextForm is just a function:
const TextForm = ({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="content">{({ field }) => <TextArea field={field} />}</Field>
</form>
)
I don't see any issue with assigning a function as an object property. Happens all the time.
The real interesting part of course is how I use TextForm. I pass the dialog values as props to a Dialog component:
<Dialog {...dialog} />
And in the part of the Dialog where I need the form, I use the TextForm to render the form with createElement
<DialogHeader>{title}</DialogHeader>
<DialogContent>
{React.createElement(formComponent)}
</DialogContent>
<DialogFooter ... />

react-dropdown - display will not change if setState is called

I have a React form. I'm using the react-dropdown dependency. I'm having a very strange issue. There is an onChange prop passed to the dropdown component. When is comes back, it sends the value from the dropdown menu back up to the parent component (the form in my case).
One would THINK you could take that response and set its value to the state via this.setState().
Except when I use setState(), in any way, the display value for the select menu stops changing. It shows me my Select your Business text instead of the selected value. If I remove the setState(), it changes.
What.....?
Here is a trimmed down version of the component:
import React from 'react';
import Dropdown from 'react-dropdown'
import FormInput from '../FormInput/FormInput';
import FormCheckbox from '../FormCheckbox/FormCheckbox';
import './RegistrationForm.css'
import 'react-dropdown/style.css'
export default class RegistrationForm extends React.Component {
constructor(props) {
super(props);
this.state={
error_business_name: false,
error_business_email: false,
error_username: false,
error_password: false,
error_type: false,
error_terms: false,
error_policy: false,
email: null,
business_name: null,
username: null,
password: null,
website: null,
terms: false
}
}
handleSelect(e) {
console.log(e.value)
this.setState({ type: e.value })
}
render() {
return (
<main role="main" className="RegistrationForm">
<img alt="Simplr Logo" src={require("../../images/logo.png")} />
<form onSubmit={e => this.handleSubmit(e)}>
<section className={this.state.error_type ? "error" : ""}>
<label htmlFor="type">Type of Business</label>
<Dropdown
className={this.state.error_type ? "dropdown error-dropdown" : "dropdown"}
options={["Law Office", "Accounting Firm", "Construction"]}
// onChange={e => this.setState({ type: e.value })}
onChange={e => this.handleSelect(e)}
placeholder="Select your Business" id="type"
/>
<p className="error-message">Please select a valid business type</p>
</section>
<button>REGISTER</button>
</form>
</main>
)
}
}
handleSelect() gets called on change. If I remove this.setState({ type: e.value }) from that method, the display changes. But if I put it in, I can still get the value, but the display then won't change from the default Select your Business text. The value gets set to the state, but it doesn't appear to be selected to the user.
I have no idea how these two processes are even connected. To my mind, once things are sent off to handleSelect(), the dropdown's job is over. But clearly, the setState() part is impacting the dropdown.
Help!
React dropdown component needs an array of objects as options [{label : "", value : ""}]
So instead of passing a string array pass an array of objects and set whole selected object as state and assign the selected state to value in Dropdown.

Dynamically Creating a React Modal Using Portals

How can I append a component to a modal using a nested portal?
I am building a page that displays several modals. I want to reuse the same modal but when a button is clicked, the app loads different content into the modal.
WIP CodePen:
https://codepen.io/jtsharpsite/pen/gorvjR
render() {
return ReactDOM.createPortal(
this.props.children,
domNode,
);
}
I have it pretty close to how I think it might work, but I cannot figure out how to append another component to an already appended modal component.
I have a button which calls a handler that opens the modal and specifies the component type.
<button onClick={this.handleShow.bind(this, "p3009", "product")}>
Product 3009
</button>
<button onClick={this.handleShow.bind(this, "s1", "special")}>
Special #1
</button>
The handler is in the App context and opens the modal sibling:
handleShow(modalId, modalType) {
this.setState({ showModal: true });
}
When the modal component mounts, I then try to append the Product when it mounts.
componentDidMount() {
//TODO how to append to parent modal?
modalRoot.appendChild(this.el);
}
How can I move the <Product> or the <Special> up into the <Modal>?
Here's an example of a reusable modal form... Theres a lot here you don't need, but just pay attention to the handleSubmit. Within the handleSubmit we call onComplete() which can be any function we pass through to the reusable modal when we call it, based on what function we want our modal to do... in our case it was capturing username, password, etc.
class ReusableModalForm extends React.Component {
constructor(props){
super(props);
this.state ={
};
}
handleChange(e) {
let {name, value} = e.target;
this.setState({
[name]: value,
usernameError: name === 'username' && !value ? 'username must have a value' : null,
emailError: name === 'email' && !value ? 'email must have a value' : null,
passwordError: name === 'password' && !value ? 'password must have a value' : null,
});
}
handleSubmit(e) {
e.preventDefault();
this.props.onComplete(this.state)
}
render() {
return (
<Modal
open={this.state.createAccountModalOpen}
trigger={<Link size="m" theme="bare" href="#" className="main-menu-item" onClick={this.handleSubmit}>{this.props.buttonText}</Link>}
closeIcon
onClose={() => { this.setState({
createAccountModalOpen: false,
}); }}
>
<Header icon='add user' content='Create account' />
<Modal.Content>
<Form />
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.handleSubmit}>
<Icon name='add user' /> {this.props.buttonText}
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default ReusableModalForm;
Based on this logic, you could construct a modal with a series of "trigger texts" to only render certain things based on what kind of props are passed down when you use your reusable modal.
Example.
<ReusableModal triggerText={'showAdress'} onComplete={this.showUsersHomeOnGoogle} />
Then in your reusableModal somewhere....
{this.props.triggerText === showAdress ? this.setState=({showHomeAdressPortion: true)}
UPDATES
class App extends React.Component {
constructor(props) {
super(props);
this.state = { showModal: false };
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
handleShow(modalId, modalType) {
console.log(
"Append content with ID: " + modalId + " of component type: " + modalType
);
//Here you should do a "fetch" first and grab your entire product information based on chosen ID and store it to state as an object...
//setState({activePortal: someContentObject})
//Then... I would use promise or aync await, show modal...
//Have Modal display from this state object
this.setState({ showModal: true, activePortal: });
//if it is a product
if (modalType == "product") {
//fetch additional images and add to state
//add images to {product.additionalPics}
}
//TODO re-render the modal with the appending element portal
}
handleHide() {
this.setState({ showModal: false });
}
render() {
//console.log("RENDER App");
// Show a Modal on click.
const modal = this.state.showModal ? (
<Modal>
<div className="modal">
<div className="modal-content-wrapper">
{/* I think what you're asking to do here is display basically "any" information that comes from a product fetch. This isnt possible without templeting somehow.. You will have to hard code like "title" "body" "image" into this modal...
<h1>this.state.activeProduct.title</h1>*/}
{/* CONTENT NEEDS TO GO HERE SOMEHOW */}
{this.props.children}
</div>
<button onClick={this.handleHide}>Hide modal</button>
</div>
</Modal>
) : null;
Have you tried using a parent-child pattern in the portal?
For example, make your children like this and just have ModalDisplay render its children. I don't know if what you are trying to do is possible because I think it requires portals to support fragments and I'm not sure if they do.
I don't know if I got the context of your question right but the idea is the same you have a single child that itself has children instead of having more than 1 child in the portal call.
<ModalDisplay>
<Special title="special" text="text" />
<Product title="product" pic="picture" />
</ModalDisplay>

Categories

Resources