How to write nested loops in JSX to reduce repetitive code? - javascript

The following code outputs two separate form sections. Everything works fine but I have two for() loops that are similar and two map() functions that are also similar. I want to learn how to write a nested script with the for() loop and map() function so that I can add more properties to my state.program object and my form will update automatically without me having to add another for() or map().
Basically I'm trying to loop through the object, create arrays to map to my component props. I'm thinking about this the wrong way?
I hope the description of my problem makes sense. Here are the React Components...
class CreateProgram extends Component {
state = {
program: {
description: {
title: {
label: 'Program title',
value: ''
},
category: {
label: 'Program category',
value: ''
}
},
location: {
location_title: {
label: 'location title',
value: ''
},
location_coor: {
label: 'location coordinates',
value: null
}
}
}
};
render() {
return <Program items={this.state.program} />;
}
}
export default CreateProgram;
class Program extends Component {
render() {
const { items } = this.props;
const descriptionArray = [];
const locationArray = [];
for (let key in items.description) {
descriptionArray.push({
id: key,
value: items.description[key]
});
}
for (let key in items.location) {
locationArray.push({
id: key,
value: items.location[key]
});
}
return (
<>
<div className="form-section">
{descriptionArray.map(element => (
<Input
label={element.value.label}
value={element.value.value}
changed={event =>
changed(event, element.id, 'program', 'description')
}
/>
))}
</div>
<div className="form-section">
{locationArray.map(element => (
<Input
label={element.value.label}
value={element.value.value}
changed={event =>
changed(event, element.id, 'program', 'location')
}
/>
))}
</div>
</>
);
}
}
export default Program;

You could simplify the parent state:
state = {
description: {
title: {
label: "Program title",
value: ""
},
category: {
label: "Program category",
value: ""
}
},
location: {
location_title: {
label: "location title",
value: ""
},
location_coor: {
label: "location coordinates",
value: null
}
}
};
Then pass these separately to the child component:
render() {
return (
<Program
description={this.state.description}
location={this.state.location}
/>
);
};
And in the child component use Object.entries returned array to map each element:
Object.entries(this.props.description).map(([key, value]) => (
<Input
key={key}
label={value.label}
value={value.value}
changed={event => changed(event, key, "program", "description")}
/>
));
Object.entries(this.props.location).map(([key, value]) => (
<Input
key={key}
label={value.label}
value={value.value}
changed={event => changed(event, key, "program", "location")}
/>
));

Related

Generating a checkbox only on the first nested key/value of "Tasks" in nested data

I have some nested data that needs to generate a form of checkboxes dynamically. The "Tasks" data, needs a parent checkbox, as per MaterialUI's docs under "Indeterminate" in their Checkboxes example . I'm struggling to understand how to apply their example in conjunction with my code.
Current data used to generate dynamic checkboxes:
const availableFilters = useMemo(
() => [
{
title: "Status",
filterOptions: [
{ label: "Ready for Review"},
{ label: "Ready for Techcheck"},
],
},
{
title: "Offices",
filterOptions:
[
{ label: "London" },
{ label: "Berlin"},
}],
},
{
title: "Tasks",
filterGroups:
[
{
title: "3D"
filterOptions: [
{label: "Animation"},
{label: "Lighting"},
],
},
{
title: "Comp"
filterOptions: [
{label: "Compositing"},
{label: "Prep"}
],
},
],
},
{
title: "Creator",
filterOptions: [{ label: "Alex"}, { label: "John"}],
},
],
[filterInfo, taskGroups]
);
Getting confused rather easily with the nesting and some recursive typescript stuff.
This is the handlerFunction in the parent component(AddtoReviewMenu) with "Lodash":
const [checkedValues, setCheckedValues] = useState<{[key: string]: string[];}>({});
const handleCheckboxChange = useCallback(
(checked: boolean, title: string, value: string) => {
if(checked) {
if (Object.keys(checkedValues).includes(title)) {
setCheckedValues({
...checkedValues,
[title]: [...checkedValues[title], value],
});
} else {
setCheckedValues({
...checkedValues,
[title]: [value],
});
} else {
setCheckedValues({
...checkedValues,
[title]: _(checkedValues[title])
.filter((c) => c !== value)
.value(),
});
}
},
[checkedValues]
);
Here is the child component that populates the the checkboxes based on the data:
import React from "react";
import IconButton, { IconButtonProps } from "#mui/material/IconButton";
import Box, { FormControl, Stack } from "#mui/material/";
interface ExpandMoreProps extends IconButtonProps {
expand: Boolean;
}
const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props;
return <IconButton {...other} />;
});
interface Props extends FilterGroup {
handleCheckboxChange: (
checked: boolean,
title: string,
value: string
) => void;
}
export default function FilterOptionGroup(props: Props) {
const { filterGroups, title, handleCheckboxChange } = props;
const [expanded, setExpanded] = useState(true);
const handleExpandClick = () => {
setExpanded(!expanded);
};
return (
<Box>
<FormControl>
<Stack>
<ExpandMore expand={expanded} onClick={handleExpandClick}>
<ArrowUpIcon />
</ExpandMore>
<FormLabel> {title} </FormLabel>
</Stack>
<Collapse in={expanded}>
{filterOptions
? filterOptions?.map((item) => (
<FormControlLabel
control={
<Checkbox
onChange={(event, checked) =>
handleCheckboxChange(checked, title, item.label)
}
/>
}
label={item.label}
value={item.label}
/>
))
: filterGroups?.map((item) => (
<FilterOptionGroup
title={item.title}
filterOptions={item.filterOptions}
filterGroups={item.filterGroups}
handleCheckboxChange={handleCheckboxChange}
/>
))}
</Collapse>
</FormControl>
</Box>
);
}
And a "Types.ts" file:
export interface FilterGroup {
title: string;
filterOptions?: FilterOption[];
filterGroups?: FilterGroup[];
}
export interface FilterOption {
label: string;
}

React map over the array object

I'm quite new with react stuff, what I am trying is to generate some dynamic stuff using .map()
This is my component:
import React, { Component } from "react";
class DynamicStuff extends Component {
state = {
options: [
{ id: 1, optionOne: "OptionOne" },
{ id: 2, optionTwo: "OptionTwo" },
{ id: 3, optionThree: "OptionThree" }
]
};
render() {
const options = [...this.state.options];
return (
<>
{options.map((option) => {
return {option}
})}
<span>{options.optionOne}</span>
<span>{options.optionTwo}</span>
<span>{options.optionThree}</span>
</>
);
}
}
export default DynamicStuff;
What I am doing wrong here and why the map is not generating expected result ?
Is it ok?
import React, { Component } from "react";
class DynamicStuff extends Component {
state = {
options: [
{ id: 1, value: "OptionOne" },
{ id: 2, value: "OptionTwo" },
{ id: 3, value: "OptionThree" }
]
};
render() {
const options = [...this.state.options];
return (
<>
{options.map((option) => {
return <span>{option.value}</span>
})}
</>
);
}
}
export default DynamicStuff;
You have made your options object incorrectly. We need to have a same attribute over all the objects in the array.
class App extends React.Component {
state = {
options: [
{ id: 1, option: "OptionOne" },
{ id: 2, option: "OptionTwo" },
{ id: 3, option: "OptionThree" }
]
};
render() {
const options = [...this.state.options];
return (
<>
{options.map((option, index) => (
<li key={index}>{option.option}</li>
))}
</>
);
}
}
Another thing, If you need to map an array. You don't need to have many spans. Having a one is just enough. The map function will iterate and give you all the things.
The map used here is actually to convert the js object into a react element, but your usage here is still a js object after the map conversion. The react element may be a <p key = {option.id}> {option. optionOne} </p>.
If there is a react element after the return in your map, it is correct.
{options.map((option) => {
return <p key = {option.id}> {option. optionOne} </p>
})}
or
{options.map((option) => <p key = {option.id}> {option. optionOne} </p>)}
YOu need to map and return the HTML element
return ({
options.map((option) => {
return `<span key={option.id}>${option. option}</span>`
})
});
You should do something like
render() {
const { options } = this.state;
return (
<div className="option-wrapper">
{options.length > 0 && options.map(option => {
return <span key={option.id}>{option.option}</span>
})}
</div>
);
}

dynamic setState to add new dynamic property in react

I have this scenario where I have to add multiple and dynamic property to an array of object. Says it's lang = ['en', 'fr', 'more'], how can I produce a object structure like this
Below is my failed attempt:
class App extends React.Component {
state = {
lang: ["en", "fr"],
items: [
{
id: 1,
value: {
en: "abc",
fr: "hello"
}
}
]
};
onChange = (e, i) => {
this.setState({
items: this.state.items.map(o => ({
...o,
value: {
[this.state.lang[i]]: e.target.value //need fix
}
}))
});
};
render() {
return (
<div className="App">
{this.state.lang.map((o, index) => (
<div>
<input
onChange={e => this.onChange(e, index)}
placeholder={o}
type="text"
/>
<br />
</div>
))}
<br />
<pre>{JSON.stringify(this.state, null, 2)}</pre>
</div>
);
}
}
https://codesandbox.io/s/p746jn313q
If I understood what you're trying to do correctly, you just needed to spread the value object inside of your map:
onChange = (e, i) => {
this.setState({
items: this.state.items.map(o => ({
...o,
value: {
...o.value, // <- this
[this.state.lang[i]]: e.target.value
}
}))
});
};
I've also edited the codesandbox: https://codesandbox.io/s/13vo5rrjwj

React js conversion to typescript, adding data to const, interface

I have started working on react typescript. I am creating a drop down component using semantic ui. The problem is that semantic Ui provides code in java script format which is easier to understand. I need to convert the below code to typescript. I am successful in doing some of it but having problem converting handleAddition while adding new value to memberOptions.
Below is the code of JS.
I am not sure if I can use setState in typescript.
const memberOptions = [
{
text: 'Bruce',
value: 'bruce'
},
{
text: 'Clark',
value: 'clark'
},
{
text: 'Diana',
value: 'diana'
},
{
text: 'Peter',
value: 'peter'
}
];
class DropdownExampleAllowAdditions extends Component {
state = { memberOptions }
handleAddition = (e, { value }) => {
this.setState({
memberOptions: [{ text: value, value },
...this.state.memberOptions],
})
}
handleChange = (e, { value }) => this.setState({ currentValues: value })
render() {
const { currentValues } = this.state
return (
<Dropdown
options={this.state.options}
placeholder='Choose Languages'
value={currentValues}
onAddItem={this.handleAddition}
onChange={this.handleChange}
/>`enter code here`
)
}
}
I need to convert handleAddition to typescript. are there some rules regarding them?
Here is the solution. The trick is how you manage the state.
const options = [
{
text: 'Bruce',
value: 'bruce'
},
{
text: 'Clark',
value: 'clark'
},
{
text: 'Diana',
value: 'diana'
},
{
text: 'Peter',
value: 'peter'
}
];
interface Props {
options? : any
}
export default class DropdownExampleAllowAdditions extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
this.state = {options:{}};
}`enter code here`
const handleAddition = (e: {}, {value}: {value:string}) => {
this.setState({
options: [{ text: value, value }, ...this.state.options],
})
}
const handleChange = (e: {}, { value }: { value: string }) =>
form.setFieldValue(fieldName, value);
render() {
const { options } = this.props;
return (
<Dropdown
options={this.state.options}
placeholder='Choose Languages'
value={currentValues}
onAddItem={handleAddition}
onChange={handleChange}
/>`enter code here`
)
}
}

React.js not re-rendering app on state update

I'm working on a React.js based outliner (in similar vein to workflowy). I'm stuck at a point. When I press enter on any one item, I want to insert another item just below it.
So I have used setState function to insert an item just below the current item as shown by the code snippet below:
this.setState({
items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
})
However, the app is re-rendering as blank without any error showing up.
My full source so far:
import './App.css';
import React, { Component } from 'react';
import ContentEditable from 'react-contenteditable';
const items = [
{
_id: 0,
content: 'Item 1 something',
note: 'Some note for item 1'
},
{
_id: 5,
content: 'Item 1.1 something',
note: 'Some note for item 1.1'
},
{
_id: 1,
content: 'Item 2 something',
note: 'Some note for item 2',
subItems: [
{
_id: 2,
content: 'Sub Item 1 something',
subItems: [{
_id: 3,
content: 'Sub Sub Item 4'
}]
}
]
}
];
class App extends Component {
render() {
return (
<div className="App">
<Page items={items} />
</div>
);
}
}
class Page extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.items
};
this.insertItem = this.insertItem.bind(this);
}
render() {
return (
<div className="page">
{
this.state.items.map(item =>
<Item _id={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
insertItem(itemID) {
console.log(this.state.items);
this.setState({
items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
})
console.log(this.state.items);
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
content: this.props.content,
note: this.props.note
};
this.saveItem = this.saveItem.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
}
render() {
return (
<div key={this.props._id} className="item">
<ContentEditable html={this.state.content} disabled={false} onChange={this.saveItem} onKeyPress={(event) => this.handleKeyPress(this.props._id, event)} />
<div className="note">{this.state.note}</div>
{
this.props.subItems &&
this.props.subItems.map(item =>
<Item _id={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
saveItem(event) {
this.setState({
content: event.target.value
});
}
handleKeyPress(itemID, event) {
if (event.key === 'Enter') {
event.preventDefault();
console.log(itemID);
console.log(event.key);
this.props.insertItem(itemID);
}
}
}
export default App;
The github repo: https://github.com/Hirvesh/mneme
Can anybody help me understand as to why it's not rendering again after I update the items?
Edit:
I have update the code to add keys, as suggested below, still rendering as blank, despite the state updating:
import './App.css';
import React, { Component } from 'react';
import ContentEditable from 'react-contenteditable';
const items = [
{
_id: 0,
content: 'Item 1 something',
note: 'Some note for item 1'
},
{
_id: 5,
content: 'Item 1.1 something',
note: 'Some note for item 1.1'
},
{
_id: 1,
content: 'Item 2 something',
note: 'Some note for item 2',
subItems: [
{
_id: 2,
content: 'Sub Item 1 something',
subItems: [{
_id: 3,
content: 'Sub Sub Item 4'
}]
}
]
}
];
class App extends Component {
render() {
return (
<div className="App">
<Page items={items} />
</div>
);
}
}
class Page extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.items
};
this.insertItem = this.insertItem.bind(this);
}
render() {
return (
<div className="page">
{
this.state.items.map(item =>
<Item _id={item._id} key={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
insertItem(itemID) {
console.log(this.state.items);
this.setState({
items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
})
console.log(this.state.items);
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
content: this.props.content,
note: this.props.note
};
this.saveItem = this.saveItem.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
}
render() {
return (
<div className="item">
<ContentEditable html={this.state.content} disabled={false} onChange={this.saveItem} onKeyPress={(event) => this.handleKeyPress(this.props._id, event)} />
<div className="note">{this.state.note}</div>
{
this.props.subItems &&
this.props.subItems.map(item =>
<Item _id={item._id} key={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
saveItem(event) {
this.setState({
content: event.target.value
});
}
handleKeyPress(itemID, event) {
if (event.key === 'Enter') {
event.preventDefault();
console.log(itemID);
console.log(event.key);
this.props.insertItem(itemID);
}
}
}
export default App;
Edit 2:
As per the suggestions below, added key={i._id}, still not working, pushed latest code to github if anybody wants to take a look.
this.state.items.map((item, i) =>
<Item _id={item._id} key={i._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
You miss to put a key on Item, because of that React could not identify if your state change.
<div className="page">
{
this.state.items.map((item, i) =>
<Item _id={item._id}
key={i} // <---- HERE!!
content={item.content}
note={item.note}
subItems={item.subItems}
insertItem={this.insertItem} />
)}
</div>
There are multiple issues with your code:
in you are passing this.insertItem into SubItems (which does not exists)
your this.state.items.splice, changes this.state.items, but returns []. So your new State is []
try this:
this.state.items.splice(/* your splice argument here */)
this.setState({ items: this.state.items }, () => {
// this.setState is async! so this is the proper way to check it.
console.log(this.state.items);
});
To accomplish what you actually want, this will not be enough.. But at least, when you have fixed it, you can see some results.

Categories

Resources