This is my object:
const [opportunity, setOpportunity] = useState({
name: '',
description: '',
experience: '',
});
I want to store it in the opportunity experience property like a string separated by commas. Like this for example: 'experience_1,experience_2,experience_3'.
But when i type in the input field i want to be able to have space between the words. Like this for example: experience_1 experience_2 experience_3
this is my code for the input field:
<InputField
type='text'
value={opportunity.experience.split(',').join(' ')} // i think i need changes here
onChange={(e) => setOpportunity({ ...opportunity, experience: e.currentTarget.value.split(" ").filter(x => x !== '').toString() })} // i think this works
label={"Experiences"}
/>
Any Help Guys?
Replace .filter(x => x !== '') with .replace(/\s+/g, " ") and place it before you call the .split() method. This will replace multiple spaces with a single space, and prevent the user from entering in more than one space at a time, while letting them change the contents of your InputField component. Remember, .filter() works on a cloned copy of the string while .replace() works on the string itself, which in this case is the event current target value.
Now it should work as intended.
Like so:
<InputField
type="text"
value={opportunity.experience
.split(",")
.map((x) => x.trim()) // Added .trim() to keep consistent spacing...
.join(" ")} // ...before joining
onChange={(e) => {
setOpportunity({
...opportunity,
experience: e.currentTarget.value
.replace(/\s+/g, " ") // If you try to use .filter() here, it won't work
.split(" ")
.toString(),
});
}}
label={"Experiences"}
/>
I've also chained a trim operation on each value from the state property with .map() to keep the spacing consistent during presentation, in the event that the state property was initialized with lots of spaces in between each experience value.
Here's an updated example of my previous answer using useEffect to update the state based on the input value. This keeps the input value and the state value separate.
working code sandbox
import React from "react";
export default function App() {
return (
<div className="App">
<MyComponent />
</div>
);
}
const MyComponent = () => {
const [opportunity, setOpportunity] = React.useState({
name: "",
description: "",
experience: []
});
const [experienceValue, setExperienceValue] = React.useState("");
React.useEffect(() => {
const experience = experienceValue.split(" ").filter((x) => x !== "");
setOpportunity({ ...opportunity, experience });
}, [experienceValue, opportunity]);
const handleInput = (e) => {
setExperienceValue(e.currentTarget.value);
};
return (
<>
<input type="text" value={experienceValue} onChange={handleInput} />
<br />
{JSON.stringify(opportunity.experience)}
</>
);
};
Related
I need to make a large number of inputs and transfer this data to the server, I decided that the best solution would be to write all the options of these inputs into an array of objects, but I ran into the fact that I can’t get all my inputs to work. help me please
const test = [
{id: 1,state: 'city'},
{id: 2,state: 'language'},
{id: 3,state: 'brand'},
{id: 4,state: 'shop'},
]
const Auth = () => {
const [description, setDescription] = useState({city: "", language: "", brand: "", shop: ""});
const handleClick = async (event: any) => {
await store.update(description.city, description.brand);
};
const update = async (e: ChangeEvent<HTMLInputElement>) => {
setDescription({
...description,
city: e.target.value
});
};
return (
<>
{test.map(({ state, id}) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description.city}
onChange={update}
/>
))}
<Button
className={styles.saveButton}
variant="contained"
color="inherit"
id="login"
onClick={handleClick}
>
Save
</Button>
</>
)
}
You send to TextField description.city for every input. The correct props are like so:
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={update}
/>
See the change in the value prop.
Also, you only update city in the update function. You have to make it so that the update function adapts to what values you pass to it. If you pass the city then it should update the city, if the language then the language and so on.
Overall this is not a good way to implement inputs. I just suggest you do them one by one and send to each TextField its corresponding value and a separate setState for each one.
But just for the sake of the example. The way you can do it is by passing the state value to the Update function.
So your function will look like this:
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription((description) => {
...description,
[state]: e.target.value
});
};
Now you just need to make sure that in the TextField component when you call onChange, you pass to it the event e and state which you have received from props.
Note: If you want to use the value of a state variable in the setState itself, pass to it a callback function like I did in the setDescription
if you want to make it dynamic you would have to send the variable to save to your update method and retrieve your value with description[state]
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e)=>update(e, state)}
/>
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription({
...description,
[state]: e.target.value
});
};
I think first and foremost you need your configuration data to try and closely match the elements you're building. So instead of { id, state } use { id, type, name }.
(This may not have a huge effect on your example because you're specifically using a TextField component, but if you were using native HTML controls you could add in different input types like number, email, date etc, and your JSX could deal with it easily.)
Second, as I mentioned in the comments, you don't need for those functions to be async - for example, there's no "after" code in handleClick so there's no need to await anything.
So here's a working example based on your code. Note: I've stripped out the Typescript (because the snippet won't understand the syntax), and the references to the UI components you're using (because I don't know where they're from).
const { useState } = React;
// So, lets pass in out inputs config
function Example({ inputs }) {
// I've called the state "form" here as it's a little
// more meaningful
const [form, setForm] = useState({});
// `handleSave` is no longer `async`, and for the
// purposes of this example just logs the updated
// form state
function handleSave() {
console.log(form);
// store.update(form);
}
// Also no longer `async` `handleChange` destructures
// the name and value from the changed input, and updates
// the form state - a key wrapped with `[]` is a dynamic key
// which means you can use the value of `name` as the key value
function handleChange(e) {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
}
// In our JSX we destructure out the id, name, and
// type properties from each input object in the config
// and apply them to the various input element properties.
return (
<div>
{inputs.map(input => {
const { id, name, type } = input;
return (
<input
key={id}
type={type}
name={name}
placeholder={name}
value={form[name]}
onChange={handleChange}
/>
);
})}
<button onClick={handleSave}>Save</button>
</div>
);
}
// Our updated config data
const inputs = [
{ id: 1, type: 'text', name: 'city' },
{ id: 2, type: 'text', name: 'language' },
{ id: 3, type: 'text', name: 'brand' },
{ id: 4, type: 'text', name: 'shop' }
];
ReactDOM.render(
<Example inputs={inputs} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Pass both the key that you want to update and the value to the update function:
const update = (key: string, value: string) => {
setDescription({
...description,
[key]: value,
});
};
{test.map(({ state, id }) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
update(state, e.target.value)
}
/>
))}
I'm trying to display the value of my inputs from a from, in a list. Everytime I hit submit, I expect that it should display the inputs in order.
The problem I'm having is that when I try to submit my form and display inputs in a list, it display an empty value first. On the next submit and thereafter, it displays the previous value, not the new one on the input field.
There's also an error message but i'm not understanding how to relate it to the problem. It's a warning message regarding controlled/uncontrolled components.
I've tried to add if statements to check for empty values in each functions but the problem persists.I've tried to manage the error massage by being consistent with all input to be controlled elements using setState, but nothing works.
I looked through todo list examples on github. I guess i'm trying to keep it in one functional component versus multiple ones, and I'm not using class components. I tried to follow the wesbos tutorial on Javascript 30 day challenge, day 15: Local Storage and Event Delegation. I'm trying to use React instead of plain JS.
Here's what my component looks like.
import React, { useEffect, useState } from "react";
import "../styles/LocalStorage.css";
export const LocalStorage = () => {
const [collection, setCollection] = useState([]);
const [value, setValue] = useState();
const [item, setItem] = useState({ plate: "", done: false });
const [display, setDisplay] = useState(false);
//set the value of the input
const handleChange = (e) => {
if (e.target.value === "") return;
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (value === "" || undefined) return;
setItem((prevState) => {
return { ...prevState, plate: value };
});
addItem(item);
setDisplay(true);
setValue("");
};
const addItem = (input) => {
if (input.plate === "") return;
setCollection([...collection, input]);
};
return (
<div>
<div className="wrapper">
<h2>LOCAL TAPAS</h2>
<ul className="plates">
{display ? (
collection.map((item, i) => {
return (
<li key={i}>
<input
type="checkbox"
data-index={i}
id={`item${i}`}
checked={item.done}
onChange={() =>
item.done
? setItem((state) => ({ ...state, done: false }))
: setItem((state) => ({ ...state, done: true }))
}
/>
<label htmlFor={`item${i}`}>{item.plate}</label>
</li>
);
})
) : (
<li>Loading Tapas...</li>
)}
</ul>
<form className="add-items" onSubmit={handleSubmit}>
<input
type="text"
name="item"
placeholder="Item Name"
required
value={value}
onChange={handleChange}
/>
<button type="submit">+ Add Item</button>
</form>
</div>
</div>
);
};
Since the setState function is asynchronous, you cannot use the state value item right after you fire the setItem(...). To ensure you get the latest value for your addItem function:
setItem((prevState) => {
const newItem = { ...prevState, plate: value };
addItem(newItem); // Now, it's getting the updated value!
return newItem;
});
And regarding the controlled and uncontrolled components, you can read the docs about it here. To fix your problem, you can initialize the value state with an empty string:
const [value, setValue] = useState('');
I'm trying to find a neater way to handle this pattern I keep coming across with react when handling changes for form fields.
For each element of my form object that I handle a change in value for I find myself replicating this pattern quite a bit with the setter function of useState(). I've tried a couple of things like creating shallow copies of the formState and mutating that but the only way I can really get things to work is with the bellow pattern which feels a little repetitive.
const handleTitle = evt => {
props.setFormState({
title: evt.target.value,
bio: props.formState.bio,
formExpertise: props.formState.formExpertise,
formExpertiseYears: props.formState.formExpertiseYears
});
};
If you want to include this.props.formState you can spread the object into the new state. Further, you can use the input’s name as the state key so you don’t have to rewrite this for every input:
props.setFormState({
...this.props.formState, // copy props.formState in
[evt.target.name]: evt.target.value // use input name as state key
});
Suggestion:
You might consider moving the state merging up into the parent component:
// parent component
const [formState, setFormState] = React.useState({});
const onFieldChange = (field, value) => {
setFormState({
...formState,
[field]: value
});
}
return (
<MyFormComponent
formState={formState}
onFieldChange={onFieldChange}
/>
);
Each input can then invoke onFieldChange with the field name and value without concerning itself with the rest of the state:
function MyFormComponent ({onFieldChange}) {
const handler = ({target: {name, value}}) => onFieldChange(name, value);
return (
<div>
<input name="title" value={formState.title} onChange={handler} />
<input name="bio" value={formState.bio} onChange={handler} />
<input name="expertise" value={formState.expertise} onChange={handler} />
</div>
);
}
Part of the project is as follows:
...
const INITIAL_STATE = {
email: '',
password: '',
error: null
}
const SignInPage = () => {
return(
<div>
<h2>Sign In</h2>
<SignInForm/>
<SignUpLink/>
</div>
)
}
const SignInFormBase = props => {
const[init,setInit] = useState(INITIAL_STATE);
const onSubmit = () => {
}
const onChange = (event) => {
setInit({
[event.target.name]: event.target.value
})
}
const isInvalid = init.password === '' || init.email === '';
return(
<form onSubmit={onSubmit}>
<input
name='email'
value={init.email}
onChange={onChange}
type='text'
placeholder='Email Address'
/>
<input
...
/>
<button disabled={isInvalid} type='submit'>Sign In</button>
{init.error && <p>{init.error.message}</p>}
</form>
)
}
const SignInForm = compose(
withRouter,
withFirebase
)(SignInFormBase)
export default SignInPage;
export {SignInForm}
The problem is:
When I replace the values in init with setInit in the onChange function, I get the following error.
Warning: A component is changing a controlled input of type text to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa) . Decide between using a controlled or uncontrolled input element for the lifetime of the component.
Note: I have the same problem in the password section
You strip part of the code but I assume that you didn't read react hooks documentation good enough. By using hooks you won't get replacement for the setState which was previously merging the values. Therefore when you call
setInit({
[event.target.name]: event.target.value
})
you will replace whole init variable with the new object therefore other field will be pointing to undefined value and react will change component to uncontrolled, then again to controlled when you enter value. If you want to maintain object in state you need to do merging by yourself. Easiest way would be to use object spread as
setInit({
...init, // <- spread old state
[event.target.name]: event.target.value
})
With this code old state will remain between inputs. I would also suggest you to not infer state property from the field name as later you can easily introduce bug you can create curried global onChange as
const onChange = (field) => (event) => {
setInit({
...init,
[field]: event.target.value
})
}
return (
<input onChange={onChange('name')} />
)
This question may be more about opinion than fact, but I'm unsure so thought I'd ask.
I'm building some forms which will display data and allow edits, the field data comes from props (as a parent component is using a GraphQL query to pull a larger amount and pass to each child).
I'm finding some input data is evaluating to null (as it's not passed back from the query) which throws a warning as inputs don't like being assigned null values.
My question is, when passing these values, what's the cleanest way to run checks on each variable and assign an empty string if needed?
So far the two options i've tried are:
Conditionally assign each to the state object, but this feels clunky and is a lot of code:
const [state, setState] = useState({
telephone: props.telephone ? props.telephone : '',
nickname: props.nickname ? props.nickname : ''
etc...
});
Or to define a function which maps over props and checks values, before setting state:
useEffect( () => {
let state_arr = {};
Object.keys(props).map( (key) => {
if( !props[key] ) state_arr[key] = '';
else state_arr[key] = props[key];
} );
setState(state_arr);
}, [] )
Honestly this feels cleaner than the first option, but there are a number of places this will occur and to have to do this in each feels counter productive.
Any help/insight appreciated.
EDIT: It turns out OP is using Material UI for this..Meaning, the reason the input is showing a warning is due to Material UI using PropTypes. I suggested that OP create a wrapper for the <Input /> component and pass through all props. Inside of the wrapper component you can just do: <InputWrapper value={props.value || ""} {...rest} /> and this covers things..
Live Demo
InputWrapper:
import React from 'react';
import { Input } from '#material-ui/core';
export default function InputWrapper({ value, ...rest }) {
return <Input value={value || ""} {...rest} />
}
InputWrapper In Use:
import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';
import InputWrapper from './InputWrapper.js';
function App(props) {
const [state, setState] = useState({});
useEffect(() => {
setState({
name: props.name,
age: props.age,
hairColor: props.hairColor,
})
}, [props.name, props.age, props.hairColor]);
const handleChange = (event, inputType) => {
setState({...state, [inputType]: event.target.value})
}
return(
<div>
{/* Shows that you can pass through native <Input /> props: */}
{/* state.name is null here! Warning is NOT thrown in the console! */}
<InputWrapper value={state.name} fullWidth onChange={e => setState({...state, name: e.target.value})} />
<InputWrapper value={state.name} multiline onChange={e => setState({...state, name: e.target.value})} />
{Object.keys(state).map((item, index) => {
return (
<div>
<InputWrapper
key={`${item}_${index}`}
value={state[item]}
onChange={e => handleChange(e, item)} />
</div>
);
})}
</div>
);
}
render(
<App name={null} age={44} hairColor="blue" />,
document.getElementById('root')
);
ORIGINAL ANSWER:
What is your use case? There is no reason to run checks and assign empty strings...
If you are trying to enforce that certain properties are used, please look into PropTypes... If you are not wanting to enforce that certain props get used, I would recommend checking for a value during use of the variable. Even if you set it to an empty string initially, you could still encounter errors down the line - I don't understand what you gain from an empty string.
I don't understand the use case - can you elaborate more on why you need to set it to an empty string?
If you really wanted to, you could verify like: useState({two: props.two || ""}) ...but it is still unnecessary..
// Notice how prop "two" is not being used..
function Test(props) {
const [state, setState] = React.useState({
one: props.one,
two: props.two
})
return(
<div>
<p>{state.one}</p>
<p>Even though <code>state.two</code> does not exist, there are no errors.. (at least for this demonstration)</p>
<input type="text" value={state.two} />
<input type="text" value={state.two} defaultValue={"default"} />
<p><i>If you really wanted to, you could verify like:</i><code>useState({two: props.two || ""})</code><i>...but it is still unnecessary..</i></p>
</div>
);
}
ReactDOM.render(<Test one="I AM ONE" />, document.body)
code {
margin: 0 10px;
padding: 3px;
color: red;
background-color: lightgray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
What about making method KickOutNullValues() which will do what you want and then you can reuse it everywhere you need. That would be more elegant.
This is a tough question, i don't know the right answer. You already tried two ways, the different way that I normally do is,
If you just want to get the display right, i would just do
<Telephone data={props.telephone} />,
const Telephone = ({data}) => { if (!data) return null }
I found this is to allow the child component to ensure the validity of this issue rather than sorting out the data in the parent API level.
Telephone.defaultProps = {
data: ''
}
This further ensures that if the data is null, it'll be reset to '' by the defaultProps
The reason I prefer this way most of time is that I don't really want to mess with the origin TRUTH of the API data.
Of course your ways might be better if you do want to ensure the data is valid at all time :)
Your code will start to have spaghetti-like qualities if you put the raw algorithm inside your callback. I recommend writing a function outside.
Your usage of Array#map is not correct, or rather you are using it in an unintended way. Array#map is used to construct an entirely new array. You are simulating Array#forEach. Also, you're performing a falsey conditional check. null is one of many values that are considered false in JavaScript. Namely, your pain points will probably be undefined, 0, and ''. If the only invalid return value is null, then check for null explicitly.
The enumerable that is for your intended use case is Array#reduce:
function nullValueReplacer(obj) {
return Object.entries(obj).reduce((newStateArr, [currentKey, currentValue]) => {
if (currentValue === null) {
newStateArr[currentKey] = ''
} else {
newStateArr[currentKey] = currentValue
}
return newStateArr
}, {});
}
As a side note, you might want to update your variable names. It's pretty deceptive that you have a variable called state_arr that is an object.
Array of objects - little fix
You should not use key with map..
think about this: (similar to yours)
useEffect(() => {
let state_arr = Object.keys(props).map(prop => prop ? {prop} : { prop: '' });
setState(state_arr);
}, [])
By using this code you make an array with object and have easy access for every item
In case there is no nickname it will look like that:
[{ telephone: '245-4225-288' }, { nickname: '' }]
What do you think?