Can I change the order of li elements conditionally in React - javascript

I have a list with side menu options, some of them are normal categories, some of them are static, here is my code:
//const type = the type is either categories or static
{items.map((item, index) => {
return (
<li>
{type !== 'categories' ? (
<> Static </>
) : (
<> Category </>
)}
</li>
);
})}
So in the items that are of type static I have a variable that comes from the back end which is called display, its value could be either 'top' or 'bottom', what I want to do is, if the type is equal to 'static', to move the order of the li to the value item.display has, for example:
<li>----</li> //type static //item.display: top
<li>++++</li> //type static //item.display: bottom
<li>""""</li> //type categories //item.display doesn't exist here
<li>::::</li> //type categories //item.display doesn't exist here
In the example above the li filled with the symbol '+' has a value of display: bottom,
so it should go to the bottom of the list and the list should now look like that:
<li>----</li> //type static //item.display: top
<li>""""</li> //type categories //item.display doesn't exist here
<li>::::</li> //type categories //item.display doesn't exist here
<li>++++</li> //type static //item.display: bottom
How can I implement that logic, I already tried doing it with inline styles like so:
<li style={{order: item.display === "top" ? items.length.toString() : "1"}}>
I am not even sure if this is a viable HTML, but it didn't work anyway, what are your suggestions for solving this problem?

You can control the order using items.sort() before you map it into virtual dom nodes.
Example:
import React from 'react'
function App() {
let items = [
{ text: '----', type: 'static', display: 'top' },
{ text: '++++', type: 'static', display: 'bottom' },
{ text: '""""', type: 'categories' },
{ text: '::::', type: 'categories' },
]
return (
<ul>
{items
.sort((a, b) =>
a.display === b.display
? 0
: a.display === 'top'
? -1
: a.display === 'bottom'
? 1
: 0,
)
.map(item => (
<li>
{item.type !== 'categories' ? (
<> Static </>
) : (
<> Category </>
)}
</li>
))}
</ul>
)
}
export default App
output:
<ul>
<li>----</li>
<li>""""</li>
<li>::::</li>
<li>++++</li>
</ul>

A possible solution would be that you have a mapping from string to number and a little helper function
const displayMap = {
top: 0,
default: 1,
bottom: 2
};
const compareDisplay(a,b){
let disp1 = a.display ?? 'default';
let disp2 = b.display ?? 'default';
return displayMap[disp1] > displayMap[disp2] ? 1 : -1;
}
in case if display is not defined, it will take default, which is mapped to the middle value
In case display also contains other values than top or bottom, you need to add a bit of logic to the helper function
you can use this mapping, to sort your objects before mapping
{
items.sort((a,b) => compareDisplay(a,b)).map((item, index) => {
return (
<li>
{
type !== 'categories' ?
(<> Static </>) :
(<> Category </>)
}
</li>
);
})}

Related

"react" Each child in a list should have a unique "key" prop

An error seems to occur because the key value is not entered in the map function, but I do not know how to modify the code.
The array is structured like this:
const tabContArr=[
{
tabTitle:(
<span className={activeIndex===0 ? "is-active" : ""} onClick={()=>tabClickHandler(0)}>0</span>
),
},
{
tabTitle:(
<span className={activeIndex===1 ? "is-active" : ""} onClick={()=>tabClickHandler(1)}>1</span>
),
},
{
tabTitle:(
<span className={activeIndex===2 ? "is-active" : ""} onClick={()=>tabClickHandler(2)}>2</span>
),
},
{
tabTitle:(
<span className={activeIndex===3 ? "is-active" : ""} onClick={()=>tabClickHandler(3)}>3</span>
),
}
];
An error occurs in the map function part.
{tabContArr.map((section)=>{
return section.tabTitle
})}
Try with React Fragments with a key attribute as mentioned in React docs
{tabContArr.map((section, index)=>{
return <React.Fragment key={`section-tab-${index}`}>{section.tabTitle}</React.Fragment>
})}
What you have done is not the right way.
If you have data, instead of passing the ReactElement into the array you should pass it into the map function like this:
{tabContArr.map((tab, index)=>{
return <span
className={activeIndex === index ? "is-active" : ""}
onClick={()=>tabClickHandler(index)}
key={`tab-${index}`}>index</span>
})}

select upto 3 in category list from array in list in react.js

I am working in react, fixed-sized array but I am not getting output when I am select up to 3 record but it show only 1 record select.
CODE:
this.state = {
type: [3], //select upto 3 record(type select from category)
categoryList: null, // category-list
};
changeCategory(o){
this.setState({type: o})
}
<div>
{ categoryList.map((o,index)=>{
return <div key={index} className={"rounded " + (type==o.slug ?"selected" : '')} onClick={()=>this.changeCategory(o.slug)} style={{padding: "2px 5px"}}>{o.name}</div>
})
}
</div>
ISSUE
type is a state array and you are comparing it like the value in the render method. In this way, you always have just one item selected instead of 3 items.
SOLUTION
This code will help you to select 3 items
this.state = {
type: [],
....
};
// this will add selected item in `type` array if length not exceed
changeCategory(o){
if(type.length !== 3){ // for limit of 3
const newType = [...this.state.type];
newType.push(o.slug);
this.setState({type: newType});
}
}
<div>
{categoryList.map((o, index) => {
return (
<div
key={index}
className={
"rounded " + (this.state.type.includes(o.slug) ? "selected" : "")
}
onClick={() => this.changeCategory(o.slug)}
style={{ padding: "2px 5px" }}
>
{o.name}
</div>
);
})}
</div>

Expand/Collapse all data

I am making a Accordion and when we click each individual item then its opening or closing well.
Now I have implemented expand all or collapse all option to that to make all the accordions expand/collapse.
Accordion.js
const accordionArray = [
{ heading: "Heading 1", text: "Text for Heading 1" },
{ heading: "Heading 2", text: "Text for Heading 2" },
{ heading: "Heading 3", text: "Text for Heading 3" }
];
.
.
.
{accordionArray.map((item, index) => (
<div key={index}>
<Accordion>
<Heading>
<div className="heading-box">
<h1 className="heading">{item.heading}</h1>
</div>
</Heading>
<Text expandAll={expandAll}>
<p className="text">{item.text}</p>
</Text>
</Accordion>
</div>
))}
And text.js is a file where I am making the action to open any particular content of the accordion and the code as follows,
import React from "react";
class Text extends React.Component {
render() {
return (
<div style={{ ...this.props.style }}>
{this.props.expandAll ? (
<div className={`content open`}>
{this.props.render && this.props.render(this.props.text)}
</div>
) : (
<div className={`content ${this.props.text ? "open" : ""}`}>
{this.props.text ? this.props.children : ""}
{this.props.text
? this.props.render && this.props.render(this.props.text)
: ""}
</div>
)}
</div>
);
}
}
export default Text;
Here via this.props.expandAll I am getting the value whether the expandAll is true or false. If it is true then all accordion will get the class className={`content open`} so all will gets opened.
Problem:
The open class is applied but the inside text content is not rendered.
So this line doesn't work,
{this.props.render && this.props.render(this.props.text)}
Requirement:
If expand all/collapse all button is clicked then all the accordions should gets opened/closed respectively.
This should work irrespective of previously opened/closed accordion.. So if Expand all then it should open all the accordion or else needs to close all accordion even though it was opened/closed previously.
Links:
This is the link of the file https://codesandbox.io/s/react-accordion-forked-sm5fw?file=/src/GetAccordion.js where the props are actually gets passed down.
Edit:
If I use {this.props.children} then every accordion gets opened.. No issues.
But if I open any accordion manually on click over particular item then If i click expand all then its expanded(expected) but If I click back Collapse all option then not all the accordions are closed.. The ones which we opened previously are still in open state.. But expected behavior here is that everything should gets closed.
In your file text.js
at line number 9. please replace the previous code by:
{this.props.children}
Tried in the sandbox and worked for me.
///
cant add a comment so editing the answer itself.
Accordian.js contains your hook expandAll and the heading boolean is already happening GetAccordian.js.
I suggest moving the expand all to GetAccordian.js so that you can control both values.
in this case this.props.render is not a function and this.props.text is undefined, try replacing this line
<div className={`content open`}>
{this.props.render && this.props.render(this.props.text)}
</div>
by this:
<div className={`content open`}>
{this.props.children}
</div>
EDIT: //
Other solution is to pass the expandAll property to the Accordion component
<Accordion expandAll={expandAll}>
<Heading>
<div className="heading-box">
<h1 className="heading">{item.heading}</h1>
</div>
</Heading>
<Text>
<p className="text">{item.text}</p>
</Text>
</Accordion>
then in getAccordion.js
onShow = (i) => {
this.setState({
active: this.props.expandAll ? -1: i,
reserve: this.props.expandAll ? -1: i
});
if (this.state.reserve === i) {
this.setState({
active: -1,
reserve: -1
});
}
};
render() {
const children = React.Children.map(this.props.children, (child, i) => {
return React.cloneElement(child, {
heading: this.props.expandAll || this.state.active === i,
text: this.props.expandAll || this.state.active + stage === i,
onShow: () => this.onShow(i)
});
});
return <div className="accordion">{children}</div>;
}
};
Building off of #lissettdm answer, it's not clear to me why getAccordion and accordion are two separate entities. You might have a very valid reason for the separation, but the fact that the two components' states are interdependent hints that they might be better implemented as one component.
Accordion now controls the state of it's children directly, as before, but without using getAccordion. Toggling expandAll now resets the states of the individual items as well.
const NormalAccordion = () => {
const accordionArray = [ //... your data ];
const [state, setState] = useState({
expandAll: false,
...accordionArray.map(item => false),
});
const handleExpandAll = () => {
setState((prevState) => ({
expandAll: !prevState.expandAll,
...accordionArray.map(item => !prevState.expandAll),
}));
};
const handleTextExpand = (id) => {
setState((prevState) => ({
...prevState,
[id]: !prevState[id]
}));
};
return (
<>
<div className="w-full text-right">
<button onClick={handleExpandAll}>
{state.expandAll ? `Collapse All` : `Expand All`}
</button>
</div>
<br />
{accordionArray.map((item, index) => (
<div key={index}>
<div className="accordion">
<Heading handleTextExpand={handleTextExpand} id={index}>
<div className="heading-box">
<h1 className="heading">{item.heading}</h1>
</div>
</Heading>
<Text shouldExpand={state[index]}>
<p className="text">{item.text}</p>
</Text>
</div>
</div>
))}
</>
);
};
Heading passes back the index so the parent component knows which item to turn off.
class Heading extends React.Component {
handleExpand = () => {
this.props.handleTextExpand(this.props.id);
};
render() {
return (
<div
style={ //... your styles}
onClick={this.handleExpand}
>
{this.props.children}
</div>
);
}
}
Text only cares about one prop to determine if it should display the expand content.
class Text extends React.Component {
render() {
return (
<div style={{ ...this.props.style }}>
<div
className={`content ${this.props.shouldExpand ? "open" : ""}`}
>
{this.props.shouldExpand ? this.props.children : ""}
</div>
</div>
);
}
}

Using onClick to call the same function works for one thing and not another?

I'm creating an application where a user can search for a device with a search bar, or look through a nested menu. When the user finds the device they want, they click on the green plus to add it to their "bag". For some reason the addDevice function I've written only works for the search function, and not when it's called in the menu. It seems to partially work, but not correctly. Does anyone know what in my code could be causing this? Please see the images below for more details.
I'm also making two different API calls, one for the search bar (called in its own function) and one for the menu (called in componentDidMount). Could this possibly be what's causing the error?
I won't include every line of code because it's a lot, but please let me know if you'd like to see anything else.
class App extends React.Component {
state = {
devices: [],
bag: [],
objectKeys: null,
tempKeys: []
};
this is the function that gets called by the onClick's
addDevice = (e, deviceTitle) => {
const array = Array.from(this.state.bag || []);
if (array.indexOf(deviceTitle) === -1) {
array.push(deviceTitle);
} else {
return;
}
localStorage.setItem("list", JSON.stringify(array));
this.setState({
bag: array
});
};
This is the function that doesn't seem to work when addDevice is called
makeMenuLayer = layer => {
const { objectKeys } = this.state;
if (layer == null) {
return null;
}
const layerKeys = Object.entries(layer).map(([key, value]) => {
{/*If value has children, display an arrow, if not, do nothing*/}
var arrow = Object.keys(value).length ? (
<i
className="fas fa-angle-right"
style={{ cursor: "pointer", color: "gray" }}
/>
) : (
""
);
{/*If value has no children, display an plus sign, if not, do nothing*/}
var plus =
Object.keys(value).length === 0 ? (
<i
className="fas fa-plus"
style={{ cursor: "pointer", color: "green" }}
onClick={e => this.addDevice(e, this.value)}
/>
) : (
""
);
return (
<ul key={key}>
<div onClick={() => this.handleShowMore(key)}>
{key} {arrow} {plus}
</div>
{objectKeys[key] && this.makeMenuLayer(value)}
</ul>
);
});
return <div>{layerKeys}</div>;
};
this is where the addDevice that does work gets called
render () {
return(
<div className="search-results">
{(this.state.devices || []).map(device => (
<a key={device.title}>
<li>
{device.title}{" "}
<i
className="fas fa-plus plus input"
style={{ cursor: "pointer", color: "green" }}
onClick={e => this.addDevice(e, device.title)}
/>
</li>
</a>
))}
</div>
)}
This is what it looks like when devices are added from the search (works fine)
This is what happens when I try to add "Necktie" from the menu. It doesn't let me add anything else after that
In this line onClick={e => this.addDevice(e, this.value)}
The value of this points to the class itself. Thus, this.value is undefined, It's not not allowing you to add anything more because undefined is already in the array and undefined === undefined is actually true.
To fix this, you need to pass the correct value of the device title.

Mapping a different icon depending on property value in React

https://codesandbox.io/s/r546o8v0kq
My above sandbox shows basic mapping of an array of items. This forms a list of notes, dates and an icon depending on what type of item it is.
I am working some logic that maps each item to find out what value it is, based on that I assign the value a string to complete the type of font awesome logo.
const noteType = _.uniq(notes.map(value => value.intelType));
const noteIcon = [
`${noteType}`.toUpperCase() == "EDUCATION"
? "paper-plane"
: `${noteType}`.toUpperCase() == "ELIGIBILITY"
? "heart"
: `${noteType}`.toUpperCase() == "GENERAL"
? "twitter"
: null
];
If "intelType" has a value of "education" it would return the "paper-plane" string to complete the icon. e.g fa fa-${noteIcon}
<List>
{notes.map((note, index) =>
note !== "" ? (
<React.Fragment>
<ListItem className="pl-0">
<i class={`fa fa-${noteIcon}`} aria-hidden="true" />
<ListItemText
secondary={moment(note.createdAt).format("DD-MMM-YYYY")}
/>
</ListItem>
<p>{note.note}</p>
<Divider />
</React.Fragment>
) : null
)}
</List>
Its not getting mapped and returning all three values, which does not meet any criteria therefore returns null as requested. I'm a bit stuck as what to do next here.
You can define an object that maps intel type to icon names:
const noteIconMap =
{ "EDUCATION": "paper-plane",
"ELIGIBILITY": "heart",
"GENERAL": "twitter",
};
And look it up this way inside the render:
<i class={`fa fa-${noteIconMap[note.intelType.toUpperCase()]}`} aria-hidden="true" />
Although, beware, if there is a case where note can have intelType undefined, toUpperCase call will throw an error.
Here's a link to working modified sandbox:
https://codesandbox.io/s/ojz2lzz03z
I'm not sure what's going on with this bit of code:
const noteIcon = [
`${noteType}`.toUpperCase() == "EDUCATION"
? "paper-plane"
: `${noteType}`.toUpperCase() == "ELIGIBILITY"
? "heart"
: `${noteType}`.toUpperCase() == "GENERAL"
? "twitter"
: null
]
But it makes more sense to me to store that information as an object and then access the icon type that way.
const icons = {
EDUCATION: 'paper-plane',
ELIGIBILITY: 'heart',
GENERAL: 'twitter'
}
And then going:
icons[noteType]
You need to get an icon in the map for each note.intelType. Since you're passing an array of ids, none of the icons is matched, and the result is always null.
A simple solution is to create a Map of types to icons (iconsMap), and get the icon from the Map using note.intelType.
btw - currently note.intelType us always uppercase, so you don't need to transform it.
sandbox
const iconsMap = new Map([
['EDUCATION', 'paper-plane'],
['ELIGIBILITY', 'heart'],
['GENERAL', 'twitter'],
]);
class Notes extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { notes, classes } = this.props;
return (
<React.Fragment>
<List>
{notes.map((note, index) =>
note !== "" ? (
<React.Fragment>
<ListItem className="pl-0">
<i class={`fa fa-${iconsMap.get(note.intelType)}`} aria-hidden="true" />
<ListItemText
secondary={moment(note.createdAt).format("DD-MMM-YYYY")}
/>
</ListItem>
<p>{note.note}</p>
<Divider />
</React.Fragment>
) : null
)}
</List>
</React.Fragment>
);
}
}

Categories

Resources