React - only one "active" child component at any one time - javascript

I have a parent component that holds a number of children components. I want to add an active className to a a child component when it is clicked on.
This is working, but the issue is that each child component can have an active classname. Only one of the components should have be active each time.
Does anyone have any idea how to solve this issue?
Please see my code below.
class MyComponent extends Component {
constructor(props) {
super(props);
this.addActiveClass= this.addActiveClass.bind(this);
this.state = {
active: false,
};
}
addActiveClass() {
const currentState = this.state.active;
this.setState({ active: !currentState });
};
render() {
return (
<div
className={this.state.active ? 'active': null}
onclick={this.addActiveClass}
>
<p>{this.props.text}</p>
</div>
)
}
}
class Test extends Component {
render() {
return (
<div>
<MyComponent text={'1'} />
<MyComponent text={'2'} />
<MyComponent text={'3'} />
<MyComponent text={'4'} />
</div>
);
}
}

Add the active functionality to your Test Component.
There you can check whether there is already active Component or not.
class MyComponent extends React.Component {
render() {
return <div
id={this.props.id}
className={this.props.active ? 'active': null}
onClick={this.props.handleClick} >
{this.props.text}
</div>
}
}
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
components: [
{id: 1, text: 1},
{id: 2, text: 2},
{id: 3, text: 3},
{id: 4, text: 4}
],
activeID: null
};
}
handleClick(e) {
// If there is already active component ID, don't set another one!
// We support only one active ID.
if (this.state.activeID !== null) return;
const id = parseInt(e.target.id);
this.setState({
activeID: id
});
}
renderComponents() {
return (this.state.components.map( c =>
<MyComponent
id={c.id}
active={c.id === this.state.activeID}
text={c.text}
handleClick={this.handleClick.bind(this)} /> ));
}
renderActiveIDText() {
return (this.state.activeID ? <p>{"Active Component ID: " + this.state.activeID}</p> : null );
}
render() {
return <div>
{this.renderActiveIDText()}
{this.renderComponents()}
</div>
}
}
ReactDOM.render(<Test />, document.getElementById('container'));
<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="container">
<!-- This element's contents will be replaced with your component. -->
</div>

Related

Access data from two parent containers - ReactJS

I have an component that renders different types of fields called Item. Item may render a select box with a list of Users or a list of Inventory. I have two containers: one for Users and another for Inventory. I originally thought to nest my containers but that appears to freeze my react app. Inventories and Users containers are identical except that one container holds inventory items and the other holds users.
Here is the Users container:
import React, { Component } from 'react';
class UsersContainer extends Component{
constructor(props){
super(props);
this.state = {
users: []
}
}
componentDidMount(){
//put api call here
this.setState({users: [{id: 1, name: "Test Name", email: "test#yahoo.com"}, {id: 2, name: "John Doe", email: "johndoe#gmail.com"}, {id: 3, name: "Jane Doe", email: "janedoe#yahoo.com"}]})
}
render(){
return(
<div className="users-container">
{React.Children.map(this.props.children, child => (
React.cloneElement(child, {...this.props, users: this.state.users })
))}
</div>
)
}
}
export default UsersContainer;
I originally tried to nest the containers but this causes React to freeze:
<UsersContainer>
<InventoriesContainer>
{this.props.items.map(i => (
<Item name={i.name} />
))}
</InventoriesContainer>
</UsersContainer>
Item looks something like this:
function elementUsesInvetory(inventories){
//returns selectbox with list of inventory
}
function elementUsesUsers(users){
//returns selectbox with list of users
}
function Item(props){
render(){
return(
<>
{elementUsesUsers(props.inventories)}
{elementUsesInventory(props.users)}
</>
);
}
}
How can I provide the data from UsersContainer and InventoriesContainer to the Item component?
Merging them into one component would avoid a lot of confusion. If you still want to nest them, you might want to pass the props by prop-drilling or by using the context API. React.cloneElement isn't preferred for nested child components. More on that here
You can pass down the data with the help of React's context API. The UsersContainer component holds the Provider and passes users down to Inventories
The Inventories will then pass on the users and inventories as props to the Items component. I'm not sure if you need separate functions for the select boxes but I've added them in the demo anyway.
const MyContext = React.createContext();
class UsersContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
users: []
};
}
componentDidMount() {
//put api call here
this.setState({
users: [
{ id: 1, name: "Test Name", email: "test#yahoo.com" },
{ id: 2, name: "John Doe", email: "johndoe#gmail.com" },
{ id: 3, name: "Jane Doe", email: "janedoe#yahoo.com" }
]
});
}
render() {
return (
<div className="users-container">
<MyContext.Provider value={this.state.users}>
{this.props.children}
</MyContext.Provider>
</div>
);
}
}
class Inventories extends React.Component {
static contextType = MyContext;
constructor(props) {
super(props);
this.state = {
inventories: []
};
}
componentDidMount() {
//put api call here
this.setState({
inventories: [
{ id: 1, name: "Test Name", email: "test#yahoo.com" },
{ id: 2, name: "John Doe", email: "johndoe#gmail.com" },
{ id: 3, name: "Jane Doe", email: "janedoe#yahoo.com" }
]
});
}
render() {
return (
<div className="inventory-container">
{React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {
...this.props,
users: this.context,
inventories: this.state.inventories
});
})}
</div>
);
}
}
function Items(props) {
function usersSelect(items) {
return (
<select>
{items.map((item) => (
<option key={"user"+item.id} value="{item.id}">
{item.name}
</option>
))}
</select>
);
}
function inventoriesSelect(items) {
return (
<select>
{items.map((item) => (
<option key={item.id} value="{item.id}">
{item.name}
</option>
))}
</select>
);
}
return (
<div>
<h2>users</h2>
{usersSelect(props.users)}
<h2>inventories</h2>
{inventoriesSelect(props.inventories)}
</div>
);
}
function App() {
return (
<div>
<UsersContainer>
<Inventories>
<Items />
</Inventories>
</UsersContainer>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I good approach would be to put the state in common between those components in a level up in the tree component.
So what are you trying to do:
<UsersContainer>
<InventoriesContainer>
{this.props.items.map(i => (
<Item name={i.name} />
))}
</InventoriesContainer>
</UsersContainer>
Would be:
RealFatherComponent extends Component {
// state that Item will need will be set here
render() {
return (
< UsersContainer **propsShared** >
<Item **propsShared** />
</UsersContainer>
< InventoriesContainer **propsShared** >
<Item **propsShared** /> );
</InventoriesContainer>
}
}

Display Content from Array to React Component

I'm trying to display my AboutPageContent.js to AboutPage.js. I would assume I would have to map it out somehow, but I'm not sure how.
Relevant Code
AboutPage.js
import React from 'react';
// CSS import statements
import '../css/AboutPage.css';
import '../css/App.css';
// Content import Statements
import AboutPageContent from '../content/AboutPageContent.js';
class About extends React.Component {
constructor(props) {
super(props);
this.state = AboutPageContent;
}
render() {
return(
<div className='about-page'>
<div className='page-header'>{this.state.about.name}</div>
<div>{this.state.about.desc.map(paragraph => <p>{paragraph}</p>)}</div>
</div>
)
}
}
export default About;
AboutPageContent.js
let AboutPageContent = {
about: [{
name: 'About Me',
desc: [
'p1',
'p2',
'p3',
'p4',
'p5',
'p6'
],
id: 1
}]};
export default AboutPageContent;
You have to do 2 maps, 1 for state.about and another one for state.about[i].desc.
class About extends React.Component {
constructor(props) {
super(props);
this.state = AboutPageContent;
}
render() {
return (
<div className="about-page">
<div className="page-header">{this.state.about.name}</div>
<div>
{this.state.about.map((currAbout) =>
currAbout.desc.map((paragraph, i) => <p key={i}>{paragraph}</p>)
)}
</div>
</div>
);
}
}
Or if you want to display the current about.name, move the <div className='page-header'>... inside this.state.about loop.
class About extends React.Component {
constructor(props) {
super(props);
this.state = AboutPageContent;
}
render() {
return (
<div className="about-page">
<div>
{this.state.about.map((currAbout, i) => (
<React.Fragment key={i}>
<div className="page-header">{currAbout.name}</div>
{currAbout.desc.map((paragraph, i) => (
<p key={i}>{paragraph}</p>
))}
</React.Fragment>
))}
</div>
</div>
);
}
}
Your this.state.about is an array, so you can't reference the name and desc properties with dot notation. Currently, this.state.about.name would be undefined. this.state.about[0].name would be equal to 'About Me'.
I would simply remove the [ ]s around the this.state.about property. That would make it an object (as opposed to an array) so the rest of your code should work fine. Leave this.state.about.desc as an array so you can use the map method.
You can access the object in about this way state.about[0]
const AboutPageContent = {
about: [
{
name: "About Me",
desc: ["p1", "p2", "p3", "p4", "p5", "p6"],
id: 1,
},
],
};
class About extends React.Component {
constructor(props) {
super(props);
this.state = AboutPageContent;
}
render() {
return (
<div className="about-page">
<div className="page-header">{this.state.about[0].name}</div>
<div>
{this.state.about[0].desc.map((paragraph) => (
<p>{paragraph}</p>
))}
</div>
</div>
);
}
}
ReactDOM.render(
<About />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
However if you have more than one property in the object and you want to display description by name you could try this
const AboutPageContent = {
about: [
{
name: "About Me",
desc: ["p1", "p2", "p3", "p4", "p5", "p6"],
id: 1,
},
{
name: " Section 2",
desc: ["p12", "p10"],
id: 1,
},
],
};
class About extends React.Component {
state = AboutPageContent;
render() {
return (
<div className="about-page">
<div className="page-header">
{Object.values(this.state)
.flat()
.map((o, i) => (
<React.Fragment key={i}>
<p>{o.name}</p>
{o.desc.map((paragraph) => (
<p>{paragraph}</p>
))}
</React.Fragment>
))}
</div>
</div>
);
}
}
ReactDOM.render(
<About />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

ReactJS: Reset-Render Child Component on Parent state change

I have a strange - at least for me :-) - issue with my Components.
I have a map() function generating a list of Child elements. When I change the filter and the list repopulates the state of the children remain as it was. For example, if the second Alumni Child Component has state {height:auto} the new second Allumni Child has again {height: auto}.
I find it really odd because this is not the same element, it's a new element corresponding to another person, with new content passed through props.
Long story short how do I force my new Child elements to have initial state {height: 0}?
There is a lot of details hidden but the core of my Parent App is as follows:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
filterName: "",
filterInitial: "Α",
....
filteredGraduates:[],
loaderVisible:true
};
this.updateInitial = this.updateInitial.bind(this)
...
}
componentDidMount(){
...
}
updateInitial(letter) {
if (letter==='*'){
this.setState({
filterInitial: "*",
filteredGraduates : [...graduates]
})
}else{
this.setState({
filterInitial: letter,
filteredGraduates : graduates.filter( (graduate) => graduate.Surname.charAt(0)===letter )
})
}
console.log('this is the letter: ',letter);
}
render() {
return (
<div className="app">
<div className="filters">
<div className="name-pagination">
<span
onClick={ () => this.updateInitial('*')}
className={'*'==this.state.filterInitial? 'active': ''}
>Α - Ω
</span>
<hr/>
{letters.map( (letter) => {
return(
<span
onClick={ () => this.updateInitial(letter)}
className={letter==this.state.filterInitial? 'active': ''}
>{letter}</span>
)
}
)}
</div>
</div>
</div>
{this.state.loaderVisible && <Loader /> }
{!this.state.loaderVisible && <div className="graduates-wrapper">
{this.state.filteredGraduates
.sort( (x,y) => x.Surname.localeCompare(y.Surname) )
.map( (graduate) => {
return(
<div>
<Allumni
key={graduate.id}
.... a lot of props ...
/>
</div>
)
})
}
</div>}
</div>
);
}
}
and my Child is:
class Allumni extends React.Component {
constructor(props) {
super(props);
this.state = {
height:0
};
this.updateHeight = this.updateHeight.bind(this)
}
updateHeight() {
this.setState({
height: this.state.height === 0 ? 'auto' : 0,
});
};
render() {
const {
name,
surName,
.......
} = this.props
return (
<div className="allumni-wrapper">
<div className="allumni-main-info">
<span className="allumni-surname">{surName}</span><br/><span className="allumni-name">{name}</span>
</div>
<div className="allumni-extra">
<span className="allumni-year">{yearOf}</span><br/>
<span className="allumni-job">{job}</span> / <span className="allumni-home-city">{homeCity}</span>
</div>
<div className="allumni-details">
<AnimateHeight
duration={ 500 }
height={ this.state.height }
>
<p><span className="attribute-name">... {fatherName}</p>
<p><span className="attribute-name">...{studies}</p>
....
</AnimateHeight>
</div>
<div className="allumni-actions" onClick={this.updateHeight}>
<AccountCardDetailsOutlineIcon size={30}/>
<span className={'actions-toggle ' + (this.state.height >= 0 ? '' : 'rotated') }><ChevronDownIcon/></span>
</div>
</div>
);
}
}
export default Allumni
thank you

ReactJS: how to map JSON elements sequentially and show the hidden div on click

I'm trying to load items from JSON and toggle a dropdown div with description on click. While I can display elements sequentially (ex: loc1 & desc1, loc2 & desc2) on static divs I'm having trouble finding out how to render it properly when the second part (desc) is hidden and only shows when the loc div is clicked.
What would be the best way to map the result so it doesn't show as loc1 & loc2, desc1 & desc2 but as loc1 & desc1, loc2 & desc2?
Code:
var places = {
library: {
location: [
{
loc_name: "library1",
"desc": "desc1 : Modern and spacious building"
},
{
loc_name: "library2",
"desc": "desc2 : A cosy small building"
}
]
}
};
function contentClass(isShow) {
if (isShow) {
return "content";
}
return "content invisible";
}
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = { isShow: false };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(function (prevState) {
return { isShow: !prevState.isShow };
});
}
render() {
const libraries_desc = places.library.location.map((libr) =>
<div>
<p>{libr.desc}</p>
</div>
);
const lib_names = places.library.location.map((libr) =>
<div>
<p>{libr.loc_name}</p>
</div>
);
return (
<div>
<div className='control' onClick={this.handleClick}>
<h4>{lib_names}</h4>
<div className={contentClass(this.state.isShow)}>{libraries_desc}</div>
</div>
</div>
);
}
}
render((
<Toggle />
), document.getElementById('root'));
Current result:
library1
library2
desc1 : Modern and spacious building
desc 2 : A cosy small building
Desired Result:
library1
desc1 : Modern and spacious building (hidden but shown when clicked)
library2
desc 2 : A cosy small building (hidden but shown when clicked)
Codesandbox
I might try extracting a location into a separate component. By extracting it, each location is responsible for knowing its state. In your case, that means its visibility (controlled by this.state.isShow).
Here's how you could do it:
import React from 'react';
import { render } from 'react-dom';
var places = {
library: {
location: [
{
loc_name: "library1",
"desc": "Modern and spacious building"
},
{
loc_name: "library2",
"desc": "A cosy small building"
}
]
}
};
class Location extends React.Component {
constructor(props) {
super(props);
this.state = { isShow: false };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(function (prevState) {
return { isShow: !prevState.isShow };
});
}
contentClass(isShow) {
if (isShow) {
return "content";
}
return "content invisible";
}
render() {
return (
<div className='control' onClick={this.handleClick}>
<h4>{this.props.desc}</h4>
<div className={this.contentClass(this.state.isShow)}>{this.props.loc_name}</div>
</div>
)
}
}
class Toggle extends React.Component {
constructor(props) {
super(props);
}
render() {
const locations = places.library.location.map(location => {
return <Location {...location} />
})
return (
<div>
{locations}
</div>
);
}
}
render((
<Toggle />
), document.getElementById('root'));
Your Toggle Component should be like this.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {
isShow: false,
id: -1, // initial value
};
}
handleClick = (id) => {
this.setState({
isShow: !this.state.isShow,
id: id
});
}
render() {
const { location } = places.library;
const { isShow, id } = this.state;
return (
<div className="control">
{location.map((libr, index) => (
<div key={index} onClick={() => { this.handleClick(index) }}>
<p>{libr.loc_name}</p>
{(isShow && (id === index)) && <p>{libr.desc}</p>}
</div>
))}
</div>
);
}
}
So when you click on the div element. A click event will be triggered called handleClick which will pass the index as a param to the function. which will set isShow to false or truth and vice versa along with the current element you want to show which will be selected through this.state.id. So everytime isShow is true and this.state.id matched index element of the array. Your description will show otherwise it will be hidden as you want.
So your desired result will be something like this.
library1
desc1 : Modern and spacious building (hidden but shown when clicked)
library2
desc 2 : A cosy small building (hidden but shown when clicked)

Nested React list not working

I am trying to recursively render JSON data to nested list using React. Right now I am using simple data object like this:
[{"id": "1",
"name": "Luke"
},
{"id": "2",
"name": "Jim",
"childNodes":[{
"id": "3",
"name": "Lola"
}]
}]
using this class:
export default class NestedList extends Component {
constructor(props) {
super(props);
this.state = {
visible: true
};
}
toggle = () => {
this.setState({ visible: !this.state.visible });
};
renderChild = (child) => {
if (child.childNodes) {
return (
<ul>
{child.myData.map(item => {
return this.renderChild(item);
})}
</ul>
);
}
else if (child.name) {
return <input type="checkbox"><Child name={child.name}/></input>;
}
return null;
}
render() {
return (
<aside>
<div>
<h4>Data Sets</h4>
<ul>
{this.renderChild(this.props.myData)}
</ul>
</div>
</aside>
);
}
}
which calls a Child class that creates list element:
export default class Child extends Component {
render() {
let {name}=this.props;
return (
<li>{name}</li>
);
}
}
but it doesn't print anything. I have tried removing attribute childNodes altogether and tried to print the list but it doesn't work still. I don't understand where I am doing wrong. I would appreciate some help regarding how to fix this.
You need to map through myData first so the rendering process begins:
<ul>
{this.props.myData.map(data => this.renderChild(data))}
</ul>
Also, on childNodes you need to loop through child.childNodes:
if (child.childNodes) {
return (
<ul>
{child.childNodes.map(node => this.renderChild(node))}
</ul>
);
}
there were couple of issues here:
You passed myData to renderChild which doesn't hold childNodes
property nor name property. Hence none of the conditions were met
(null was returned).
So maybe you should loop through myData and
pass each member of the array to renderChild.
Even if we will pass a valid "child" to the renderChild method,
inside this condition:
if (child.childNodes) {
Again you are using a wrong property:
<ul>
{child.myData.map(item => {
return this.renderChild(item);
})}
</ul>
this should be:
{child.childNodes.map(item => {...
Last thing, You can't nest child elements inside an input element.
so change the layout, maybe like this? :
<input type="checkbox"/>
<Child name={child.name} />
Here is a running example with your code:
const data = [
{
id: "1",
name: "Luke"
},
{
id: "2",
name: "Jim",
childNodes: [
{
id: "3",
name: "Lola"
}
]
}
];
class NestedList extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: true
};
}
toggle = () => {
this.setState({ visible: !this.state.visible });
};
renderChild = child => {
if (child.childNodes) {
return (
<ul>
{child.childNodes.map(item => {
return this.renderChild(item);
})}
</ul>
);
} else if (child.name) {
return (
<div>
<input type="checkbox"/>
<Child name={child.name} />
</div>
);
}
return null;
};
render() {
return (
<aside>
<div>
<h4>Data Sets</h4>
<ul>{this.props.myData.map(item => this.renderChild(item))}</ul>
</div>
</aside>
);
}
}
class Child extends React.Component {
render() {
let { name } = this.props;
return <li>{name}</li>;
}
}
ReactDOM.render(<NestedList myData={data} />, document.getElementById("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>

Categories