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>
);
}
}
Related
I am currently stuck on creating this dropdown filter functionality in React. I have this array here that I have in a solutions.json file:
[
{"name": "Employer Branding", "id": "415"},
{"name": "Account Based Marketing", "id": "414"},
{"name": "Thought Leadership", "id": "413"}
]
I want to filter out the case studies 'solutions' that I have displaying dynamically through a REST API. I currently have it showing ALL case studies. This is my code:
<div className={classes.cardContainer}>
<ul className="ulGrid">
{caseStudies.map((result) => {
return (
<li className={classes.liGrid} key={result.id}>
<LayeredCard
href={`/case-studies/${result.slug}`}
key={result.id}
title={result.title.rendered}
icon={result.acf.icon}
subtitle={result.acf.case_study_pre_heading}
readMore="Read More"
></LayeredCard>
</li>
);
})}
</ul>
</div>
I want to be able to filter these case studies through their solutions category and make it a condition that if the "solutions" ids match then render filtering result.
API data from a case study
So far I have created a dropdown filter without it actually filtering in a dropdown.js file:
import { useState, useRef, useEffect } from "react";
export default function Dropdown({
options,
prompt,
value,
onChange,
id,
label,
}) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
//tracking when the dropdown is clicked
useEffect(() => {
document.addEventListener("click", close); //when click happens we call the close function
return () => document.removeEventListener("click", close);
}, []);
// (&& logical AND operator), if our dropdown is open and we click outside of the dropdown than the e.target is the
// html document which will NOT be equal to ref.current (our dropdown) which will close the dropdown as a result
function close(e) {
setOpen(e && e.target === ref.current);
}
return (
<div className={classes.dropdown}>
<div className={classes.control} onClick={() => setOpen((prev) => !prev)}>
<div className={classes.selectedValue} ref={ref}>
{/* if there is a value clicked then that shows if not the prompt shows */}
{value ? value[label] : prompt}
</div>
<div
className={`${classes.arrow} ${open ? `${classes.open}` : null} `}
></div>
<div
className={`${classes.options} ${open ? `${classes.open}` : null} `}
>
{/* className={classes.arrow open ? classes.open} */}
{options.map((option) => (
//the selected value is indicated with a css style
<div
key={option[id]}
className={`${classes.option} ${
value === option ? `${classes.selected}` : null
} `}
onClick={() => {
onChange(option); //change this
setOpen(true); //when you click the option the dropdown then closes.
}}
>
{option[label]}
</div>
))}
</div>
</div>
</div>
);
}
I am calling the Dropdown function in my casestudies.js file like so:
import solutions from "../data/solutions.json";
import Dropdown from "../components/dropdown/filterDropdown/Dropdown";
<Dropdown
options={solutions}
prompt="Solutions"
id="id"
label="name"
value={value}
onChange={(val) => setValue(val)}
/>
<div className={classes.cardContainer}>
<ul className="ulGrid">
{caseStudies.map((result) => {
return (
<li className={classes.liGrid} key={result.id}>
<LayeredCard
href={`/case-studies/${result.slug}`}
key={result.id}
title={result.title.rendered}
icon={result.acf.icon}
subtitle={result.acf.case_study_pre_heading}
readMore="Read More"
></LayeredCard>
</li>
);
})}
</ul>
</div>
Not sure how to render the filtering result. Can anyone help?
I'm trying to get the Menu (from #szhsin/react-menu module) element buttons to show up to the right of the previous generated item, however I'm a bit lost as to how to get it to do so. Everything results in the element showing below previous.
import React from 'react';
import {
Menu,
MenuItem,
MenuButton,
SubMenu
} from '#szhsin/react-menu';
import '#szhsin/react-menu/dist/index.css'
class TopMenuDropdown extends React.Component {
constructor(props) {
super(props);
}
render () {
return (
<div>
{this.props.TMPMenuTestCategory.map (({name,items},i) =>
{
return <Menu
align={'end'}
key={i}
menuButton={<MenuButton>{name}</MenuButton>}
reposition={'initial'}
>
{items.map((item,j) =>
{
console.log(item,j);
return <MenuItem key={j}>{item}</MenuItem>
}
)}
</Menu>
} )}
</div>
)
}
}
I was looking through the documentation on https://szhsin.github.io/react-menu/docs , however, me trying the following has had no effect:
Assigning the display:'inline' or 'flex' the <Menu> or to a <div><Menu> as I attempted to give each menu it's own div when generated.
Wrapping each generated menu in a <span>
Fiddling with the Menu item's props like 'align' , 'position' , and 'reposition' (though I'm guessing Reposition needs an additional RepositionFlag to work if I understand it correctly)
Here's the snippet of index.JS it is part of
const basicMenuArray = [
{ name: 'ProTIS', items: [ 'Login', 'Exit' ] },
{ name: 'Project', items: [ 'Open', 'Info' ] },
]
class App extends React.Component {
state={
language:'sq'
}
render () {
return (
<div >
<div style={{display:'flex', width:'75%', float:'left' }}>
<span> Temp Text </span>
</div>
<div style={{display:'flex', width:'25%'}}>
<span style={{marginLeft:'auto'}}>
<DataComboBox
dropdownOptions={languages}
value={this.state.language}
valueField='language_code'
textField='language_full_name'
onChange={(value) => alert(JSON.stringify(value))}
/>
</span>
</div>
<div>
<TopMenuDropdown TMPMenuTestCategory={basicMenuArray} />
</div>
</div>
);
}
}
So I ended up realizing something this morning, as I'm learning ReactJS still, and my brain did not process the things properly.
I changed the initial
<div>
to
<div style={{display:'flex'}}>
and added a style={{display:'flex', float:'left'}} to the <Menu> which generates the button.
the final code snippet looks like this for anyone still learning like I am :)
return (
<div style={{display:'flex'}}>
{this.props.TMPMenuTestCategory.map (({name,items},i) =>
{
return <Menu
style={{display:'flex', float:'left'}}
key={i}
menuButton={<MenuButton>{name}</MenuButton>}
>
{items.map((item,j) =>
{
console.log(item,j);
return <MenuItem key={j}>{item}</MenuItem>
}
)}
</Menu>
} )}
</div>
)
I am using a json with several values, one of them is "iframe" which can be "si" (yes) or "no" depending on whether it is an iframe or not.
With that value (yes / no) I need (this.props.tabsiframe === 'yes') to show an <iframe> or a <div>.
My code works, because if (this.props.tabsiframe === 'yes') is an iframe it paints an iframe, but if the next element is (this.props.tabsiframe === 'no' ) change all the elements of <iframe> to <div>.
The re-rendering changes the already created divs and converts them to iframe if iframe = yes and if it is = it does not change the iframe to div. This is an example of what happens
https://drive.google.com/file/d/1cY9eHq_aBuQb8b_HtYUXjC6lqrYCJe3p/view
Maybe it happens because every time I click on the element the menu is updated: divIframe: {tabsDivIframe: [... new Set (this.state.divIframe.tabsDivIframe), url] .filter (function (el) { return the;})},. A new url is added to the array, so I guess that's what it renders and it changes the divs to iframes or iframes to divs.
class App extends Component {
constructor(props, context){
super(props, context);
["openTabs", "removeTab"].forEach((method) => {
this[method] = this[method].bind(this);
});
this.state = {
tabs:{
tabsLi: [],
},
divIframe:{
tabsDivIframe: [],
},
tabsiframe : '',
showtabs: true,
}
}
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
this.setState({
showtabs: false,
})
if (this.state.tabs.tabsLi.includes(trdtitle) === false){
this.setState({
tabs: { tabsLi:[...new Set(this.state.tabs.tabsLi),trdtitle].filter(function(el) { return el; })},
divIframe: { tabsDivIframe:[...new Set(this.state.divIframe.tabsDivIframe),url].filter(function(el) { return el; })},
tabsiframe: iframe,
}, () => {
console.log(this.state.tabs.tabsLi);console.log(this.state.divIframe.tabsDivIframe);console.log(this.state.tabsiframe)
})
}
}
render(){
return (
<><Tabs
showtabs={this.state.showtabs}
tabs={this.state.tabs}
tabsLi={this.state.tabs.tabsLi}
divIframe={this.state.divIframe}
tabsDivIframe={this.state.divIframe.tabsDivIframe}
tabsiframe={this.state.tabsiframe}
openTabs={this.openTabs}
removeTab={this.removeTab}
/>
</>
)
}
}
class Tabs extends Component {
render(){
return(
<div id="content-tabs" className="tabs">
{( this.props.showtabs)
? (
<>
<div className="waiting-leads">
<p>Parece que todavía no hay ningún lead...</p>
<h3>¡Ánimo, ya llega!</h3>
<img src={imgDinosaurio} alt="Dinosaurio"></img>
</div>
</>
) : (
<>
<DivAndIframe
tabsDivIframe={this.props.divIframe.tabsDivIframe}
tabsiframe={this.props.tabsiframe}
/>
</>
)}
</div>
);
}
}
class DivAndIframe extends Component{
render(){
return(
<>
{this.props.tabsDivIframe.map((url, index) =>
<div key={url.toString() id={"myTab" + index}>
{( this.props.tabsiframe === 'si')
? (
<iframe title={"iframe"+index} className="iframeTab" src={url}></iframe>
) : (
<div>{url}</div>
)}
</div>
)}
</>
);
}
}
The problem you are having here is within your map.
Here is a state map to try to explain:
After First tab Click (div):
this.props.tabsiframe = 'no'
this.props.tabsDivIframe = ['www.sampleurl1.com']
After Second tab Click (iframe):
this.props.tabsiframe = 'yes'
this.props.tabsDivIframe = ['www.iframe2.com', 'www.sampleurl1.com']
so on the virtual DOM after the second click, the map will again iterate through this.props.tabDivIframe and evaluate this.props.tabsiframe as 'si' or 'no' for everything.
{this.props.tabsDivIframe.map((url, index) =>
<div key={url.toString() id={"myTab" + index}>
{( this.props.tabsiframe === 'si') //this line here; we share this same value for all tabs
? (
<iframe title={"iframe"+index} className="iframeTab" src={url}></iframe>
) : (
<div>{url}</div>
)}
</div>
)}
I wrote a small interactive sample of your problem here: https://jsfiddle.net/eojrx6my/6/
You will need to store individual 'tabsiframe' for each url, or somehow identify them individually
I am working on a project and i want to display a hidden <div> below another <div> element using an event handler but when i click the icon that is meant to display the div, the whole page becomes blank
This is image I want:
This is what i get
I have tried to check through the internet for some places where i could get the solution. Well i found something similar to what i had done but the error still happens for me.
class PostItTeaser extends Component {
state = {
postIt: false,
moreIt: false,
}
togglePostIt = e => {
e ? e.preventDefault() : null
this.setState({ postIt: !this.state.postIt })
}
_toggle = e => {
e ? e.preventDefault() : null
this.setState({
moreIt: !this.state.moreIt,
})
}
Child = () => <div className="modal">Hello, World!</div>
render() {
let { postIt } = this.state
let { moreIt } = this.state
let {
type,
group,
disabled,
session: { id, username },
} = this.props
return (
<div>
<div
className="post_it inst"
style={{ marginBottom: type == 'group' && 10 }}
>
<img src={`/users/${id}/avatar.jpg`} alt="Your avatar" />
<div className="post_teaser">
<span
className="p_whats_new"
onClick={disabled ? null : this.togglePostIt}
>
What's new with you, #{username}? #cool
</span>
<span className="m_m_exp" data-tip="More" onClick={this._toggle}>
<MaterialIcon icon="expand_more" />
</span>
</div>
</div>
{moreIt && <Child />}
{postIt && (
<PostIt back={this.togglePostIt} type={type} group={group} />
)}
</div>
)
}
}
From skimming through the code I believe you need to bind the scope, since the function you're calling is using this.setState, it needs this to be the react component, not the event you're listening to:
onClick={this._toggle.bind(this)}
You can also bind the functions scope in the constructor. Or, a less memory performant & ugly way:
onClick={() => { this._toggle(); } }
I have an accordion in React formed of a row component which is looped inside a body parent component. In the row I'm toggling the state showDetails to show/hide the details for each row, effectively opening the accordion item. But, since the state is for each row, how do I close one accordion item when I open another one?
Body:
export default class Body extends React.Component {
render() {
const {modelProps, showInfo, linkedRow} = this.props;
return (
<div className="c-table__body">
{this.props.model.map(
(subModel, i) =>
linkedRow ?
<LinkedRow
key={`${i}`}
model={subModel}
modelProps={modelProps}
/>
:
<Row
key={`${i}_${subModel.username}`}
model={subModel}
modelProps={modelProps}
showInfo={showInfo}
handleStatusChanged={this.props.handleStatusChanged}
/>
)}
</div>
);
}
}
Row:
class Row extends React.Component {
constructor(props) {
super(props);
this.state = {
userId: '',
showDetails: false,
showModal: false,
status: '',
value: '',
showInfo: false
};
render() {
const { model, modelProps, showInfo } = this.props;
return (
<div className="c-table__row">
<div className="c-table__row-wrapper">
{modelProps.map((p, i) => (
<div className={`c-table__item ${this.isStatusCell(model[p]) ? model[p] : p}`} key={i}>{this.isStatusCell(model[p]) ? this.toTitleCase(model[p]) : model[p]}</div>
))}
{showInfo ? (
<div className="c-table__item c-table__item-sm">
<a
name="view-user"
onClick={this.showDetailsPanel}
className={this.state.showDetails ? 'info showing' : 'info'}
>
<Icon yicon="Expand_Cross_30_by_30" />
</a>
</div>
) : (
''
)}
</div>
{this.state.showDetails ? (<ConnectedDetails user={model} statusToggle={this.handleStatusChange}/>) : null}
</div>
);
}
}
export default Row;
Not really sure how to approach this, maybe something in the body that check is there's any row open according to the showDetails state in the rows?
Thanks in advance
The approach is to lift the state of which <Row /> is open to the <Body /> component.
Also the method that switch between opened <Row /> is on the <Body /> component.
toggleOpen = (idx) => {
this.setState({ openRowIndex: idx });
}
then when you rendered your <Row />s you can pass a prop isOpen:
<Row
key={`${i}_${subModel.username}`}
model={subModel}
modelProps={modelProps}
showInfo={showInfo}
handleStatusChanged={this.props.handleStatusChanged}
isOpen={this.state.openRowIndex === i}
onToggle={_ => this.toggleOpen(i)}
/>