ReactJs Accordion Automatic Close Mechanism - javascript

I'm currently working on an accordion component in react version 16.3.2, that receives props from another component and displays the accordion accordingly. Unfortunately I cannot use hooks.
This works pretty smoothly, what I want to implement now is a way so that only one accordion can be opened at the same time on the same page. So, once you open one accordion (while another is already opened) it should automatically close the already opened one.
I have an Id (string which describes the current section, e.g 'contact', 'info', etc.) and the state of an accordion gets saved in the state (set to true when you toggle the accordion).
I'm not quite sure on how I could implement this mechanism and am looking for tips on how I could solve this in a smart way. Any pointers?
example:
https://codesandbox.io/s/reactjs-accordion-automatic-close-mechanism-6dys1
(I didn't add all of the styling, animations since this is more about functionality)

You could do something like this, using the state hook in the App component
export default function App() {
const items = [
{ id: 1, title: 'First Accordion', content: 'Hello' },
{ id: 2, title: 'Click me', content: 'Hello 2' },
{ id: 3, title: 'Third Accordion Accordion', content: 'Hello 3' },
]
const [selectedItem, setSelectedItem] = useState(1)
const handleClick = id => {
setSelectedItem(id)
}
return (
<div className="App">
{items.map(x => {
return (
<Accordion
key={x.id}
id={x.id}
title={x.title}
open={x.id === selectedItem}
onClick={handleClick}
>
<p>{x.title}</p>
</Accordion>
)
})}
</div>
);
}
Then your accordion component is a bit simpler
class Accordion extends React.Component {
accToggle() {
this.props.onClick(this.props.id);
}
sectionClasses() {
let classes = "accordion";
classes += this.props.open ? " sec-on" : "";
classes += "sec-underway";
return classes.trim();
}
render() {
return (
<section className={this.sectionClasses()} id={this.props.id}>
<div className="acc-title" onClick={this.accToggle.bind(this)}>
<h3 className="acc-text">{this.props.title}</h3>
<div className="acc-nav">
<span className="acc-toggle" />
</div>
</div>
<div className="acc-content">{this.props.children}</div>
</section>
);
}
}
Accordion.defaultProps = {
open: false
};
Accordion.propTypes = {
id: PropTypes.number.isRequired,
children: PropTypes.any,
onFocus: PropTypes.func,
progress: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
PropTypes.bool
]),
title: PropTypes.string,
open: PropTypes.bool
};
export default Accordion;
The accordion calls a function on the app component, which updates the state and the display logic is passed down in the props

You can find solution for your problem in the following codesandbox
https://codesandbox.io/s/reactjs-accordion-automatic-close-mechanism-yejc0
Change prop names as it fits your code base, but the logic is solid

Related

React changing the states of the whole elements in the map function, not only the button that is pressed

Hello everyone, I am trying to passing a method through a context api component to another component which, i have a map function there. I want my showInfo state changes to true or false depending on the button clicking, when i clicked the button, all the showInfo's of my states is changes, so thats not what i want, I want that specific item to change when i press to it. Can someone explaine where is the mistake that i've made?
MY CONTEXT APİ
import React from "react";
export const ToursContext = React.createContext();
class ToursContextProvider extends React.Component {
constructor(props) {
super(props);
this.changeState = this.changeState.bind(this);
this.state = {
tours: [
{
id: 0,
imageURL:
"https://images.unsplash.com/photo-1524231757912-21f4fe3a7200?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1351&q=80",
title: "İstanbul'un Güzelliğinin Sadece Bir Parçası Galata Kulesi",
showInfo: true,
info: "LOREM İPSUM AMET 1",
},
{
id: 1,
imageURL:
"https://images.unsplash.com/photo-1541432901042-2d8bd64b4a9b?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1319&q=80",
title: "Tarihi Süleymaniye Camii",
showInfo: true,
info: "LOREM İPSUM AMET 2",
},
],
};
}
changeState(itemdelete) {
this.setState({
showInfo: !this.state.showInfo,
});
console.log(itemdelete);
}
render() {
return (
<ToursContext.Provider
value={{ ...this.state, changeState: this.changeState }}
>
{this.props.children}
</ToursContext.Provider>
);
}
}
export default ToursContextProvider;
MY MAP LIST COMPONENT
import React from "react";
import { ToursContext } from "../contexts/Tours";
function Tours() {
return (
<div className="container">
<div className="row">
<ToursContext.Consumer>
{(value) => {
const { changeState } = value;
return value.tours.map((item) => (
<div className="col-md-4" key={item.id}>
<div className="card bg-dark text-white">
<img src={item.imageURL} className="card-img" alt="..." />
<div className="card-img-overlay">
<h5 className="card-title">{item.title}</h5>
<button
type="button"
onClick={changeState.bind(this, item)}
className="btn-sm btn-primary"
>
Bilgiyi Göster!
</button>
</div>
{value.showInfo ? "true" : "false"}
</div>
</div>
));
}}
</ToursContext.Consumer>
</div>
</div>
);
}
export default Tours;
You state is atomic. This means that it is treated as a single value. With classes, you have option to modify state object partially. For example, you have object with fields a and b. You can change both fields at once, only a or only b. But there is no option to modify state deeply. Let's imagine that you have state object like this:
{
"a": { "subfield_1": [], "subfield_2": "some string"},
"b": 3
}
You again, can modify a or b, but if you want to add item into array a.subfield_1 or change a.subfield_2, you will have to modify whole a, like this:
setState({
a: {
...a,
subfield_1: this.state.a.subfield_1.concat("new item"),
},
});
In you case, to change something inside tours key, you will have to modify whole tours key. It would be something like this:
changeState(itemdelete) {
this.setState({
tours: tours.map((item) =>
item.id !== itemdelete.id ? item : { ...item, showInfo: !item.showInfo }
),
});
}

JSX won't render properly

My JSX won't show up properly on my React webpage instead I get this output:
<div class='card'>NaNSasha<img src= NaN />Boddy Cane</div>.
The component:
import React, {Component} from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
class App extends Component{
state = {
string : '',
}
componentDidMount(){
let posts = [
{
title: 'somebody toucha my spaghet',
author: 'Karen',
image:'https://media-cdn.tripadvisor.com/media/photo-s/11/69/c7/f9/spagetti.jpg',
location: 'Jimmy John',
description: 'This spagetti is amazing'
},
{
title: `I love food`,
author: 'Sasha',
image:'https://hoodline.imgix.net/uploads/story/image/610603/donuts2.jpg?auto=format',
location: 'Boddy Cane',
description: 'cjndwsijnjcinjw'
}
];
for(let i =0; i < posts.length; i ++){
const header = `<div class='card'>${+posts[i].title}`;
const body = posts[i].author;
const image = `<img src= ${+posts[i].image} />`;
const description = `${posts[i].location}</div>`;
const concatThis = header + body + image + description
this.setState({
string: concatThis
});
};
};
render(){
return(
<div className='container'>
{this.state.string}
</div>
)
}
}
export default App;
P.S I'm a student
This is what you're looking for. The expression +{} is evaluated as NaN. But please use list rendering.
const image = `<img src= ${+posts[i].image} />`;
^ here
It seems that you are trying to build a string which you then store in a state and render that string after it has been updated. This is unfortunately not how you should use React.
The state should only be raw data, like the posts array with objects. It holds the content and data of the component and should not concern itself on other tasks than that. You obviously can put any type of data in a state, like a string.
state = {
title: 'My food blog',
description: 'Awesome stuff about food',
posts: [
{
title: 'somebody toucha my spaghet',
author: 'Karen',
image:'https://media-cdn.tripadvisor.com/media/photo-s/11/69/c7/f9/spagetti.jpg',
location: 'Jimmy John',
description: 'This spagetti is amazing'
},
{
title: `I love food`,
author: 'Sasha',
image:'https://hoodline.imgix.net/uploads/story/image/610603/donuts2.jpg?auto=format',
location: 'Boddy Cane',
description: 'cjndwsijnjcinjw'
}
]
}
The componentDidMount method is triggered whenever the component has been placed on the page and is now working. In there you can do things like making a change to the data or downloading data from the server. It would make sense that you would do that there because then you would first show your component, maybe show it that it is loading and then fetch data from the server. After that is done, update the state of the component with the new data and the render method will be called with the new data. For example (for illustration purposes):
componentDidMount() {
fetch('urlofdatathatyouwant') // Uses AJAX to get data from anywhere you want with the Fetch API.
.then(response => response.json()) // Tells it to read turn the response from JSON into an usable JavaScript values.
.then(data => {
this.setState({
posts: data // Use the new data to replace the posts. This will trigger a new render.
});
});
}
The render method should primarely concern itself with the presentation of the data in your state. In this case it should loop over the elements in the posts state and create a React element for each post.
render() {
const { posts } = this.state;
return(
<div className='container'>
{posts.map(({ title, author, image, location, description }) => (
// Loop over each post and return a card element with the data inserted.
<div className="card">
<span>{title}</span>
<span>{author}</span>
<img src={image} alt={title}/>
<span>{location}</span>
<span>{description}</span>
</div>
))}
</div>
)
}
All put together it would look like this the example below. So the state only holds the data, componentDidMount is a place to do manipulation of your data after the component is on the page and render only outputs the HTML that you need to create with JSX.
import React, { Component } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
class App extends Component{
state = {
posts: [
{
title: 'somebody toucha my spaghet',
author: 'Karen',
image:'https://media-cdn.tripadvisor.com/media/photo-s/11/69/c7/f9/spagetti.jpg',
location: 'Jimmy John',
description: 'This spagetti is amazing'
},
{
title: `I love food`,
author: 'Sasha',
image:'https://hoodline.imgix.net/uploads/story/image/610603/donuts2.jpg?auto=format',
location: 'Boddy Cane',
description: 'cjndwsijnjcinjw'
}
]
}
componentDidMount() {
// Do something here with the posts if you need to.
}
render() {
const { posts } = this.state;
return(
<div className='container'>
{posts.map(({ title, author, image, location, description }, index) => (
<div key={index} className="card">
<span>{title}</span>
<span>{author}</span>
<img src={image} alt={title}/>
<span>{location}</span>
<span>{description}</span>
</div>
))}
</div>
)
}
}
export default App;
You could even make it a bit nicer by making the card element a component as well. And since it does not have any functionality (yet) it only has to control the output.
const Card = ({ title, author, image, location }) => (
<div className="card">
<span>{title}</span>
<span>{author}</span>
<img src={image} alt={title}/>
<span>{location}</span>
<span>{description}</span>
</div>
)
And then import the card into your App component and use it in the render method.
// App.jsx render.
render() {
const { posts } = this.state;
return(
<div className='container'>
{ /* ...post taking all the properties of each post and passes them to the card element */ }
{posts.map((post, index) => <Card key={index} {...post} />)}
</div>
)
}

Why do these similar react function passing statements work in different ways? [duplicate]

This question already has answers here:
What are the differences (if any) between ES6 arrow functions and functions bound with Function.prototype.bind?
(3 answers)
Why and when do we need to bind functions and eventHandlers in React?
(2 answers)
Closed 2 years ago.
I have simple react page showing a Tree component and a series of fields. Right now I have the Tree hardcoded, but the fields as passed in as props. Also passed in as props are two parent callbacks. One for the Tree onSelect and one for the <input> onChange event. In both cases, the specific 'on' Event is a local function that in turn calls the parent's callback. This is all working....
In both cases the local functions reference the 'this' variable. In one local function, the Tree onSelect', I had to use the '.bind(this)' way but, in the other I did not. Both local functions can access the 'this.props.' values. However, if I remove the '.bind(this)' from the one used in the Tree component it fails. The 'this' is undefined.
I am new to react, so I'm just trying to figure what goes where. I'm guessing this has something to do with the Tree being a component and the <input> as something more basic?
Thanks for any insights...
import React, { Component } from "react";
import Tree from '#naisutech/react-tree'
import './RecipePage.css';
class RecipePage extends Component {
constructor(props){
super(props);
this.state = { items: props.items,};
}
onMySelect (props) {
debugger;
const items = this.state.items.slice();
console.log("HI" , props);
}
handleChange = ({ target }) => {
debugger;
const items = this.state.items.slice();
items[parseInt(target.id)+1].defaultValue = target.value;
this.setState({items: items,});
var props = {items, target};
this.props.onInputChanged(props); // call the parent's update function send relavent data.
};
render() {
const t8000 = [
{
label: 'Gauge 1',
id: 1234,
parentId: null,
items: null
},
{
label: 'Target',
id: 1236,
parentId: 1234,
items: null
},
{
label: 'Gage Factor',
id: 5678,
parentId: 1234,
items: null
},
{
label: 'Gauge 2',
id: 1235,
parentId: null,
items: null
},
{
label: 'Target',
id: 2236,
parentId: 1235,
items: null
},
]
const myTheme = {
'my-theme': {
text: '#161616',
bg: '#f1f1f1',
highlight: '#cccccc', // the colours used for selected and hover items
decal: 'green', // the colours used for open folder indicators and icons
accent: '#f1f1f1' // the colour used for row borders and empty file indicators
}
}
return(
<div id="recipePage" className="recipeMenu" >
<div className="closeButton" >
<img src={require('./../CommonImages/closeButton_W_90x90.png')} height="90px" onClick={() => this.props.close()} alt="Menu buttons"></img>
<Tree nodes={t8000} onSelect={this.onMySelect.bind(this)} theme = {'my-theme'} customTheme={myTheme} />
<form>
<fieldset>
<legend>this.state.items[0].label}</legend>
{this.state.items.map((item, key) =>(
<div className={item.show===1 && key!==0 ?"ShowInputs":"HideInputs"}>
<label htmlFor={item.id}>{item.label} </label>
<input type="text" name={item.id}
id={item.id} value={item.defaultValue}
onChange={this.handleChange} />
</div>
))}
</fieldset>
</form>
</div>
</div>
);
}
}
export default RecipePage;

React - changing the background of a single span class not working

I am new to React so my apologies if the question, or the thing I am trying to achieve is just weird (and please do tell if there is a better / more logic way to do this).
I am using the List Fabric React component in my React application, which is based on the ListGridExample component which is found here:
https://developer.microsoft.com/en-us/fabric#/components/list
I have set it up but I can't seem to accomplish the following:
When a span class (which is actually an item) in the List component is clicked, I want to change it's background color, to do this I have followed the instructions in the following post:
https://forum.freecodecamp.org/t/react-js-i-need-a-button-color-to-change-onclick-but-cannot-determine-how-to-properly-set-and-change-state-for-that-component/45168
This is a fairly simple example but this changes all my grid cells / span classes to the color blue instead of only the clicked one. Is there a way I can make just the clicked span class change it's background?
The Initial state:
The state after clicking one span class (which is wrong):
Implementation code (ommitted some unecesary code):
class UrenBoekenGrid extends React.Component {
constructor(props) {
super(props);
this.state = {
bgColor: 'red'
}
}
render() {
return (
<FocusZone>
<List
items={[
{
key: '#test1',
name: 'test1',
},
{
name: 'test2',
key: '#test2',
},
{
name: 'test3',
key: '#test3',
},
{
name: 'test4',
key: '#test4',
},
..... up to 32 items
]}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
changeColor(item){
this.setState({bgColor: 'blue'});
console.log('clicked item == ' + item.name)
}
_onRenderCell = (item, index) => {
return (
<div
className="ms-ListGridExample-tile"
data-is-focusable={true}
style={{
width: 100 / this._columnCount + '%',
height: this._rowHeight * 1.5,
float: 'left'
}}
>
<div className="ms-ListGridExample-sizer">
<div className="msListGridExample-padder">
{/* The span class with the click event: */}
<span className="ms-ListGridExample-label" onClick={this.changeColor.bind(this, item)} style={{backgroundColor:this.state.bgColor}}>{`item ${index}`}</span>
<span className="urenboeken-bottom"></span>
</div>
</div>
</div>
);
};
}
I now have attached the click event to the span class itself but I would think it is way more logic to have the click event on the item(s) (array) itself, however I could not find a way to achieve this either.
----UPDATE----
#peetya answer seems the way to go since #Mario Santini answer just updates a single cell, if another cell is clicked then the previous one returns back to normal and loses it's color.
So what I did is adding the items array to the state and adding the bgColor property to them:
this.state = {
items: [
{
key: '#test1',
name: 'test1',
bgColor: 'blue',
},
{
name: 'test2',
key: '#test2',
bgColor: 'blue',
},
{
name: 'test3',
key: '#test3',
bgColor: 'blue',
},
{
name: 'test4',
key: '#test4',
bgColor: 'blue',
},
],
}
Now in my List rendering I have set the items to the state items array and added the onClick event in the _onRenderCell function:
render() {
return (
<FocusZone>
<List
items={this.state.items}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
renderedWindowsAhead={4}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
_onRenderCell = (item, index) => {
return (
<div
className="ms-ListGridExample-tile"
data-is-focusable={true}
style={{
width: 100 / this._columnCount + '%',
height: this._rowHeight * 1.5,
float: 'left'
}}
>
<div className="ms-ListGridExample-sizer">
<div className="msListGridExample-padder">
<span className="ms-ListGridExample-label"
onClick={this.onClick(item.name)}
style={{backgroundColor: item.bgColor}}
>
{`item ${index}`}
</span>
<span className="urenboeken-bottom"></span>
</div>
</div>
</div>
);
};
The problem is that I can't add the onClick event in the _onRenderCell function as this will give the following error:
I want to keep the Fabric List component as it also has functions for rendering / adjusting to screen size, removing the list component entirely and just replacing it with what #peetya suggested works:
render() {
<div>
{this.state.items.map(item => (
<div onClick={() => this.onClick(item.name)} style={{backgroundColor: item.bgColor}}>
{item.name}
</div>
))}
</div>
}
But this will also remove the List component functionality with it's responsive functions.
So my last idea was to just replace the items of the List with the entire onClick div and removing the _onRenderCell function itself, but this makes the page blank (can't see the cells at all anymore..):
render() {
return (
<FocusZone>
<List
items={this.state.items.map(item => (
<div onClick={() => this.onClick(item.name)} style={{backgroundColor: item.bgColor}}>
{item.name}
</div>
))}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
renderedWindowsAhead={4}
// onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
I thought that perhaps the css ms-classes / div's should be in there as well because these have the height/width properties but adding them (exactly as in the _onRenderCell function) does not make any difference, the page is still blank.
The problem is that you are storing the background color in the state of the Grid and assign this state to every element of the grid, so if you update the state, it will affect every element. The best would be if you create a separate component for the Grid elements and store their own state inside there or if you want to use only one state then store the items array inside the state and add a new bgColor attribute for them so if you want to change the background color only for one item, you need to call the setEstate for the specific object of the items array.
Here is a small example (I did not tested it):
class UrenBoekenGrid extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [
{
key: '#test1',
name: 'test1',
bgColor: 'blue',
},
],
};
}
onClick(name) {
this.setState(prevState => ({
items: prevState.items.map(item => {
if (item.name === name) {
item.bgColor = 'red';
}
return item;
})
}))
}
render() {
<div>
{this.state.items.map(item => (
<div onClick={() => this.onClick(item.name)} style={{backgroundColor: item.bgColor}}>
{item.name}
</div>
))}
</div>
}
}
Actually you are changing the color of all the span elements, as you set for each span the style to the state variable bgColor.
Insteas, you should save the clicked item, and decide the color based on that:
this.state = {
bgColor: 'red',
clickedColor: 'blue
}
In the constructor.
Then in the click handler:
changeColor(item){
this.setState({selected: item.name});
console.log('clicked item == ' + item.name)
}
So in the renderer (I just put the relevant part):
<span ... style={{backgroundColor: (item.name === this.state.selected ? this.state.clickedColor : this.state.bgColor)}}>{`item ${index}`}</span>

Modify a child state from parent "one hit"

I've been searching for a few days and can't seem to get this to work and it's driving me insane!
The aim is an accordion with the addition of an "Expand All"/"Collapse All" button. I managed to get this to work at one point by passing an expandAll property to the child component through props and updating the expand state of the child in componentWillReceiveProps, but if the parent component is set to auto-fetch data on a timer, then it meant that the child state was updated to collapse or expand all the panels after the second fetch which isn't the required behaviour.
class App extends React.Component {
constructor(props){
super(props);
this.state = {
"teams": [{ Name: "Liverpool", Players: ["Lovren", "Coutinho"]}, {Name:"Arsenal", Players: ["Ozil", "Welbeck"]},{Name: "Chelsea", Players: []}],
toggleAll: false,
toggleText: "Expand All"
}
}
_toggleAll(event){
event.preventDefault();
let text = "Expand All";
if (!this.state.toggleAll === true){
text = "Collapse All"
}
this.setState({toggleAll: !this.state.toggleAll, toggleText: text});
console.log("toggleAll", this.state.toggleAll);
}
render() {
return (
<div className="headers">
<button onClick={this._toggleAll.bind(this)}>{this.state.toggleText}</button>
{this.state.teams.map((team, index) =>
<Header className="" key={index} title={team.Name} players={team.Players} />
)}
</div>
);
}
}
class Header extends React.Component {
constructor(props){
super(props);
this.state = {
expanded: false
}
}
componentWillMount(){
console.log(this.props.title);
console.log(this.props.players);
}
_expand(event){
event.preventDefault();
this.setState({expanded: !this.state.expanded});
}
render() {
let players;
if(this.state.expanded)
{
players= <ul>
{this.props.players.map((player, index) => <li key={index}>{player}</li>)}
</ul>
}
return (
<div>
<div className="header" >
<p onClick={this._expand.bind(this)}>{this.props.title}</p>
</div>
{players}
</div>
);
}
}
React.ReactDOM.render(
<App />,
document.getElementById('container')
);
Fiddle provided below
https://jsfiddle.net/aamirjaffer/x52bhdq6/
You need to pass a prop from the the App component to give the header it's "collapseAll" state. I updated the rendering of the header in App like so:
<Header
className = ""
key={index}
title={team.Name}
players = {team.Players}
allExpanded={this.state.toggleAll}
/>
Then the Header component needs to listen for that prop change on conponentWillReceiveProps and set it's own state accordingly like so:
componentWillReceiveProps(nextProps) {
if (nextProps.allExpanded !== this.props.allExpanded) {
this.setState({ expanded: nextProps.allExpanded });
}
}
I forked your fiddle made, those changes and it all works just fine: https://jsfiddle.net/xkp4snmx/
You could store collapsed state of each team.
this.state = {
teams: [
{
Name: "Liverpool",
Players: ["Lovren", "Coutinho"],
collapsed: true
},
{
Name: "Arsenal",
Players: ["Ozil", "Welbeck"],
collapsed: true
},
{
Name: "Chelsea",
Players: ["Hazard", "Oscar", "Willian"],
collapsed: true
}
]
}
js fidds
ps: I would probably render the players list regardless, and just hide it via css - less computation and with css you can give it neat animations for a true accordion feel.
pss: want to throw it in that your spacing & indentation made it that much harder to read your code =|

Categories

Resources