Targeting nested object properties inside an parent object - javascript

I'm trying to change the state of opening/closing times (and the post to an endpoint) on multiple days which are return to me in an object like so:
I have rendered these in a React component:
{this.state.time.map(each => (
<Fragment key={each.code}>
<OpeningHours
code={each.code}
day={each.description}
name={each.code}
open={each.open !== null ? each.open : '00:00'}
close={each.close !== null ? each.close : '00:00'}
onChange={this.onTimeChange}
/>
</Fragment>
))}
How would I iterate through the array of objects until I find the index of either the opening or closing time of this day? So far I've tried this, which works, but only if there was, say, just an opening time or one field in general. The issue arises since I have 2 fields to edit:
onTimeChange(e) {
let inputs = this.state.inputs.slice();
for(let i in inputs){
if(inputs[i].name == event.target.name){
inputs[i].value = event.target.value;
this.setState ({inputs});
break;
}
}
}

const newInputs = inputs.map(p =>
p.name === <unique_name/code>
? { ...p, [e.target.name]: e.target.value }
: p
);
this.setState({inputs:newInputs,});
Change the onTimeChange() method to get the unique_name:onTimeChange(e, code)
A simple example: https://codesandbox.io/s/v1y14rl2oy
Since index is unique, you can do
inputs[index] = { ...inputs[index], [e.target.name]: e.target.value }
too without using map() as in the sandbox.

Related

JavaScript logic problem with dynamically deleting component

I am mapping values from an array to create a < select> dropdown. I want to be able to dynamically create and delete them. I have figured out creation (using mapping) but I have an issue with deletion. I'm using a key in the array to differentiate values but my problem is that when I go to delete a specific < select>, only the newest created gets deleted. This is because the key being passed into the delete method is the newest key create (line 2: let key = array.key;). What is a workaround solution that will let me pass the correct key into the delete method while keeping line 2. Hopefully this makes sense. Thanks
EDIT: The solution below works just make sure to update the components you are mapping in the state or the old array will be used to map
{this.state.AdditionQueryArray.map((array) => {
let key = array.key;
return (
<div>
<Select
onChange={(e) => this.HandleChange(e, key)}
options={this.state.OperatorOptions}
placeholder="Select Operator"
menuPortalTarget={document.body}
menuPosition={"fixed"}
/>
<button onClick={() => this.delete(key)}>Delete</button>
<div>
)
}
delete(key) {
const state = this.state;
for (var i = state.AdditionQueryArray.length - 1; i >= 0; --i) {
if (state.AdditionQueryArray[i].key == key) {
state.AdditionQueryArray.splice(i, 1);
}
}
this.setState(state);
}
Why wouldn't you pass the index instead of key?
If you're using index, you can exclude it with filter. As Example
{this.state.AdditionQueryArray.map((array, index) => {
return (
<div>
<Select
onChange={(e) => this.HandleChange(e, key)}
options={this.state.OperatorOptions}
placeholder="Select Operator"
menuPortalTarget={document.body}
menuPosition={"fixed"}
/>
<button onClick={() => this.delete(index)}>Delete</button>
<div>
)
}
delete(index) {
let tempArray = [...this.state.AdditionQueryArray]
let filteredArray = tempArray.filter((item, arrayIndex) => arrayIndex !== index)
this.setState({
AdditionQueryArray: filteredArray
})
I usually use this when I want to delete an element of list. I also add [...array] to make it didn't edit state directly
easiest way might be adding custom(data) attribute to that button tag and set it to key and read that attribute with event object in delete function
check: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes

React useState pre populated from previous component render

I'm trying to create a Quiz component rendering one Question at time and changing it when the user chooses one of the alternatives.
However, every time it renders the next Question it has already the chosenOption variable set from the previous Question. This happens because before I change the Question, I set the new state of the current Question with that chosenOption and strangely(to me) this is already set when the next Question component is rendered.
For me, the setChosenOption would set only for the current Question and when the Quiz renders the next Question its chosenOption would be null initially. I may be missing something from how functional components render... So why is it happening?
Thanks in advance!
const Quiz = () => {
const [currentQuestion, setCurrentQuestion] = React.useState(0)
const [answers, updateAnswers] = React.useState({})
const numberOfQuestions = questions.length
const onChoosenOptionCallbackHandler = ({ hasChosenCorrectOption, chosenOption}) => {
updateAnswers(prevAnswers => ({...prevAnswers, [currentQuestion]: hasChosenCorrectOption }))
setCurrentQuestion(currentQuestion + 1)
}
return (
<QuizBackground>
<QuizContainer>
<Question
question={questions[currentQuestion]}
index={currentQuestion}
numberOfQuestions={numberOfQuestions}
onChoosenOptionCallback={onChoosenOptionCallbackHandler}
/>
</QuizContainer>
</QuizBackground>
)
}
Here, apart from the first Question, the 'Chosen Option: ' log always show the chosenOption from the previous Question rendered and not null.
const Question = ({ question, index, numberOfQuestions, onChoosenOptionCallback }) => {
const [chosenOption, setChosenOption] = React.useState(null)
console.log('Chosen Option: ', chosenOption)
const hasChosenCorrectOption = chosenOption !== null ? (chosenOption == answer) : false
const selectOption = (optionIndex) => {
setChosenOption(optionIndex)
console.log('SELECTED: ', optionIndex, hasChosenCorrectOption, chosenOption)
onChoosenOptionCallback({ hasChosenCorrectOption, optionIndex })
}
return (
{/* I removed other parts not relevant. The RadioOption goes inside a map() from the question alternatives */}
<RadioOption
questionName={questionName}
option={option}
chosen={chosenOption === index}
onSelect={() => selectOption(index)}
key={index}
/>
)
}
Your issue is a result of not assigning keys to your Question components, that are being rendered using a map function.
The omission of proper keys (i.e. a unique property of each element in the rendered array) results in all sorts of weird behaviours, such as what you were describing.
The reason for that is that React uses these indices to optimize, by re-rendering only the components whose props were changed. Without the keys the whole process isn't working properly.

Cannot read property 'address_1' of undefined - React

I have a code like this
class EventDetails extends React.Component {
constructor( props ) {
super (props);
this.state={
startdate: props.startdate || "",
enddate: props.enddate || "",
venue: props.venue || ""
}
}
componentDidMount() {
fetch(`https://www.eventbriteapi.com/v3/venues/${this.state.venue}/?token=EventBrite'sTOKEN`)
.then(response => response.json())
.then(eventvenue => this.setState({ venue: eventvenue }))
}
render() {
const { startdate, enddate, venue } = this.state;
const getDateWhenTheEventStart = new Date(Date.parse(startdate));
const theDateWhenTheEventStart = getDateWhenTheEventStart.toDateString();
const theHourWhenTheEventStart = getDateWhenTheEventStart.getHours();
const theMinuteWhenTheEventStart = getDateWhenTheEventStart.getMinutes();
const getDateWhenTheEventEnd = new Date(Date.parse(enddate));
const theDateWhenTheEventEnd = getDateWhenTheEventEnd.toDateString();
const theHourWhenTheEventEnd = getDateWhenTheEventEnd.getHours();
const theMinuteWhenTheEventEnd = getDateWhenTheEventEnd.getMinutes();
function checkTime(time) {
if (time < 10) {
time = '0' + time
}
return time;
}
return(
<React.Fragment>
<Container text>
<Header as="h1">Date and Time</Header>
<p><strong>Starts:</strong> {theDateWhenTheEventStart} | {checkTime(theHourWhenTheEventStart)}:{checkTime(theMinuteWhenTheEventStart)}</p>
<p><strong>Ends:</strong> {theDateWhenTheEventEnd} | {checkTime(theHourWhenTheEventEnd)}:{checkTime(theMinuteWhenTheEventEnd)}</p>
<Header as="h1">Location</Header>
<List>
<List.Item>{venue.name}</List.Item>
{venue.address.address_1 != undefined && <List.Item>{venue.address.address_1}</List.Item>}
{venue.address.localized_area_display != undefined && <List.Item>{venue.address.localized_area_display}</List.Item>}
</List>
</Container>
</React.Fragment>
)
}
}
export default EventDetails;
The problem of the code is here
{venue.address.address_1 != undefined && <List.Item>{venue.address.address_1}</List.Item>} // gives me the error of 'Cannot read property 'address_1' of undefined'
I suspect that this problem occured because the component render first and the program breaks because the state is not updated yet.
Do you have any ideas how to solve this problem? Thank you
Make sure that the venue.address is not null before using venue.address.address_1
{venue.address && venue.address.address_1 !== undefined && <List.Item>{venue.address.address_1}</List.Item>}
You would have to do full error checking elsewhere - for example when you set the state.
But if you would like to do a one liner and do a full Error Check, following would be possible
<List>
<List.item>Date and Time</List.item>
{venue ? venue.address ? venue.address.address_1 ? <List.Item>{venue.address.address_1}</List.Item> : <List.Item>Loading..</List.Item> : <List.Item>Loading..</List.Item> : <List.Item>Loading..</List.Item>
</List>
However, as you can see, it's a very ugly way of doing it so do validity checking elsewhere if you want to use ternary
This is the safest full check if your object is defined or not with all properies during rendering (I think the cleanest too). It will render element if your object is fully valid.
{!!venue && !!venue.address && !!venue.address.address_1
&& <List.Item>{venue.address.address_1}</List.Item>
}
the best way of doing this is to use Object.prototype.hasOwnProperty() to check whether your object contains a particular key or not :
{venue.hasOwnProperty(address) &&
venue.address.hasOwnProperty(address_1) &&
<List.Item>{venue.address.address_1}</List.Item>}
Check something like below
{venue && venue.address && venue.address_1 ? .
{venue.address.address_1} : ''}
Here you can see, first we are checking if venue object is available then we check for the inner one and then nested object. This is the best practice instead of directly displaying the value. Hope this helps :)
It's always a good idea to check than an object and all nested objects you want to access have loaded. Please find an example of how I would check. When using the && operator, Javascript will check for the first false statement and then stop. In other words, if venue is "" (an empty string is considered false), null, or undefined, Javascript will stop checking at venue, before an error similar to the one you posted above is thrown.
If venue exists, it will then check venue.address, and so on, until finally, it will return the last statement "{venue.address.address_1}". If you try the code below, you'll see what I mean.
One last thing - please make sure the venue props are actually being passed to your component, or the two List.Item components will not render (though you will not get an error).
<List>
{venue && venue.name && <List.Item>{venue.name}</List.Item>}
{venue && venue.address && venue.address.address1 && <List.Item>{venue.address.address_1}</List.Item>}
{venue && venue.address && venue.address.localized_area_display && <List.Item>{venue.address.localized_area_display}</List.Item>}
</List>
Instead of long checks like a && a.b && a.b.c, use lodash get.
https://lodash.com/docs/4.17.10#get

if or condition doesn't work

I'm new to javascript and I want to run some code depending if the state.value != null or "".it doesn't throw an error but freezes there. please see my code down below.any help would be really appreciated.
constructor(){
super();
this.state = {
value:null,
list:[]
}
}
handleList = () => {
//let list = this.state.list.slice();
if (this.state.value != null || this.state.value.length() > 0 ) {
let list = [...this.state.list];
list.push(<li>{this.state.value}</li>);
this.setState({list});
console.log(list.length,this.state.list.length);
}else{
console.log("Cant enter null");
}
}
render() {
return(
<div className = 'global'>
<button onClick={() => {this.handleList()}
}>Add-New</button>
<input
onChange = {
(e)=>{this.setState({value: e.target.value})}
}
type = 'text'
placeholder = 'Enter New Todo!'/>
<hr/>
<ul>
{
this.state.list.map((li) => {
return (li);
})
}
</ul>
</div>
);
}
}
Evaluating the existence of Strings
In JavaScript: empty Strings '' are falsey (evaluate to false).
const x = ''
if (x) console.log('x = true')
else console.log('x = false')
As a result, the existence of this.state.value be tersely verified as follows:
if (this.state.value) .. // Do something if this.state.value != ''
This strategy can be leveraged and chained by simply referencing variables followed by && (which results in only the last truthy variable being returned). If no truthy variable is found: false is returned. ie. in the case of the onClick method of the <button/> tag below.
Rendering Lists
In React: it is typical to store lists of plain variables (Strings, Objects, etc) and handle conversion to element form on the fly.
Rendering Strings representing HTML elements is a security flaw. In production: someone could very easily type a malicious todo and ruin your entire application. You may need to use dangerouslySetInnerHTML if you wish to continue down that path.
See the docs for more info on how to render lists.
Example
See below for a rough example of a todo container.
// Container.
class Container extends React.Component {
// Constructor.
constructor(props) {
super(props)
this.state = {
value: '',
list: []
}
}
// Render.
render = () => (
<div className = 'global'>
<button onClick={() => this.state.value && this.setState({value: null, list: [...this.state.list, this.state.value]})}>Add</button>
<input value={this.state.value} onChange={(e) => this.setState({value: event.target.value})} placeholder="Todo.."/>
<hr/>
<ul>
{(this.state.list.length > 0 && (this.state.list.map((todo, index) => <li key={index}>{todo}</li>))) || '..'}
</ul>
</div>
)
}
// Mount.
ReactDOM.render(<Container/>, document.querySelector('#root'))
<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="root"></div>
Because you are using OR, both criteria are checked. So even if value is NULL, the code is still attempting to check the length of the string. But a NULL object doesn't have a "length" property, so this will result in an "value does not have property: length" error. To fix this, using AND ( && ) would be more appropriate.
Additionally, the "length" property is a value, not a function, so attempting to call as function will result in an "length is function of value" error.
These errors should appear in the console when viewing your web-page. If you press F12, a window should appear at the bottom of your browser. If you then select the console tab, you should be able to see all errors output. You might need to make sure you aren't filtering error messages.

Remove value from state (set new state)

I am trying to remove a value from my state.
I am using .filter as I believe this is the simplest way of doing it. I also want to implement an undo function ( but that's outside the scope of this question).
I have put this code in a sandbox
https://codesandbox.io/s/yrwo2PZ2R
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
movies: x.movies,
};
}
remove = (e) => {
e.preventDefault();
console.log('remove movie.id:', e.target.value)
const index = e.target.value
this.setState({
movies: this.state.movies.filter((_, e) => e.id !== index)
});
}
render() {
return (
<div>
{this.state.movies.map(e =>
<div key={e.id}>
<li>{e.name} {e.id}</li>
<button value={e.id} onClick={this.remove}>remove</button>
</div>,
)}
</div>
);
}
}
Two problems.
First of all, the index you're getting from the event target value is a string, but you're comparing against a number. Change the index declaration as follows:
const index = Number(e.target.value);
Secondly, your filter is a little off. This will work:
this.state.movies.filter(movie => movie.id !== index)
The problem is index has string type, but id in objects has number type. You need type cast, for example:
const index = Number(e.target.value);
Other than that, you have some wrong _ in callback of filter function call. You don't need it. You need:
this.state.movies.filter(e => e.id !== index)
By the way I don't recommend to name values this way. Why e? You have array of movies. Use movie. Why index? You have id to remove. Then use idToRemove name.
You also have problem with adding items.
Firstly, you can add items like this:
this.setState({
movies: [...this.state.movies, { name: item.value.name, id: item.value.id }],
})
Another point: you have to autoincrement id. You can store last value in a variable. this.idCounter for example. And add will look like:
this.setState({
movies: [...this.state.movies, { name: item.value.name, id: this.idCounter++ }],
})
Example: https://codesandbox.io/s/2vMJQ3p5M
You can achieve the same in the following manner
remove = (e) => {
e.preventDefault();
console.log('remove movie.id:', e.target.value)
const index = e.target.value
var movies = [...this.state.movies]
var idx = movies.findIndex((obj) => obj.id === parseInt(index))
movies.splice(idx, 1);
this.setState({
movies
});
}
Also use parseInt to convert index to a string before comparing.
Directly setting the current state from the previous state values can cause problems as setState is asynchronous. You should ideally create a copy of the object and delete the object using splice method
CODESANDBOX

Categories