React JS Custom Sidebar Navigation - javascript

I have Created a toggled navigation in React APP. Everything working fine except the toggle. Below is the code of toggle. I am using the check of parentId if parentId exist then include the children id's and it will open the toggle. If not exist then add the main Id which one is parentId in children.
Problem is I parentId already exist and if I click the toggle it is not closing the old one and displaying the new one along with old.
const handleArrowClick = ( data: CoursesNav ) => {
const { id, parentId } = data;
let newtoggledMenus = [...toggledMenus];
if( parentId && newtoggledMenus.includes(parentId)){
if (newtoggledMenus.includes(id)) {
var index = newtoggledMenus.indexOf(id);
if (index > -1) {
newtoggledMenus.splice(index, 1);
}
} else {
newtoggledMenus.push(id);
}
settoggledMenus(newtoggledMenus);
}else{
settoggledMenus([id]);
}
};
I have created the nav structure given below in which under the main item children are added.
export const CourseMenu = ( book: Book ): CoursesNav[] => {
if( course ){
// Chapters from book
return book.chapters.map( c => {
return {
id: c.id,
name: c.name,
link: `/my-book/${course.id}/chapter/${c.id}/edit`,
parentId: c.id,
// get topics and put under children
children: c.topics.map( m => {
return {
id: m.id,
name: m.name,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/edit`,
parentId: c.id,
//get blocks and put under blocks
children: m.blocks.map( b => {
return {
id: b.id,
name: b.title,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/block/${b.id}/edit`,
parentId: m.id
}
})
}
})
}
});
}
}
Nav logic is added just to show you that what I am trying to create.
Every parent has button for toggle to show the sub items. If any sub item has children again the toggle will displayed.
Only I have the problem in opening and closing the correct toggle. Right now under a main children if I open its all child previous one are not closing because they are getting the parentId in toggledMenu state which is a numeric array.

yow broh, why don't you use aria. don't search for the parent of no body.
The user clicks your button/link whatever the user clicks, but you need to set an aria attribute call aria-controls in which you need to put the value of which eva you want to hide/show.
start by closing any open menu or so, then wait 150 milliseconds or so. just to prevent that your loop closes the one that needs to be opened
function App () {
/**
* togging menus
*/
function onClickHandler (e) {
e.preventDefault();
const btnPressed = e.target;
// first you need to check if there is a item toggled and stuff.
const openMenus = document.querySelectorAll("[aria-expanded]");
if (openMenus.length) {
Object.keys(openMenus).forEach(button => {
const toggler = openMenus[button];
const menuId = toggler.getAttribute("aria-controls");
const menuToClose = document.getElementById(menuId);
if (menuToClose && menuToClose !== null) {
menuToClose.classList.remove("show");
menuToClose.setAttribute("aria-hidden", "true");
menuToClose.classList.add("hidden");
// now the button
toggler.setAttribute("aria-expanded", "false");
}
})
}
//wait a minute to prevent conflicts
setTimeout(()=>{
const toOpen = document.getElementById(btnPressed.getAttribute("aria-controls"));
if (!toOpen) {return;}
btnPressed.setAttribute("aria-expanded", "true");
toOpen.classList.add("show");
toOpen.setAttribute("aria-hidden", "false");
toOpen.classList.remove("hidden");
},[150])
}
return (
<div className="accordion-example">
<ul aria-label="Accordion Control Group Buttons" className="accordion-controls">
<li>
<button onClick={onClickHandler} aria-controls="content-1" aria-expanded="false" id="accordion-control-1">Apples</button>
<div className="menu hidden" aria-hidden="true" id="content-1">
<p>Apples are a fine fruit often associated with good health, and fewer doctor's appointments.</p>
<p>Example. An apple a day keeps the doctor away.</p>
</div>
</li>
<li>
<button onClick={onClickHandler} aria-controls="content-2" aria-expanded="false" id="accordion-control-2">Lemons</button>
<div className="menu hidden" aria-hidden="true" id="content-2">
<p>Lemons are good with almost anything, yet are often have a negative connotation when used in conversation.</p>
<p>Example. The bread from the french bakery is normally very good, but the one we bought today was a lemon.</p>
</div>
</li>
<li>
<button onClick={onClickHandler} aria-controls="content-3" aria-expanded="false" id="accordion-control-3">Kiwis</button>
<div className="menu hidden" aria-hidden="true" id="content-3">
<p>Kiwis are a fun, under-appreciated fruit.</p>
</div>
</li>
</ul>
</div>)
}
ReactDOM.render( < App / > , document.getElementById("page"));
.hidden {
display: none;
}
.show {
display: blocK
}
<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="page"></div>

I am guessing you have a 3 level hierarchy inside a Book -> chapter, topic & block. On click of (let's say) right button, you want that specific item's children to be toggled. This is evidently at chapter and topic level.
One solution is maintain the entire hierarchy which you shared (book.chapters) as a state, and have a showChildren flag at each level.
return book.chapters.map( c => {
return {
id: c.id,
name: c.name,
link: `/my-book/${course.id}/chapter/${c.id}/edit`,
parentId: c.id,
showChildren: false,
// get topics and put under children
children: c.topics.map( m => {
return {
id: m.id,
name: m.name,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/edit`,
parentId: c.id,
showChildren: false,
//get blocks and put under blocks
children: m.blocks.map( b => {
return {
id: b.id,
name: b.title,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/block/${b.id}/edit`,
parentId: m.id
}
})
}
})
}
});
}
Based on the click on the right arrow, toggle that particular state.
With the above approach, you can keep multiple levels in the nav hierarchy open to toggle.

Related

how to select All checkbox based group in reactjs

I did initial state for select single checkbox .Here is my intial state
this.state = {
fruites: [
{ id: 1 , value: "banana", isChecked: false },
{ id: 2, value: "apple", isChecked: false },
{ id: 3,value: "mango", isChecked: false },
{ id: 4, value: "grap", isChecked: false }
]
};
}
Method: I just this for selected all checkbox
handleAllChecked = id => event => {
let fruites = this.state.fruites;
fruites.forEach(fruite => {
data.filter(item =>
fruite.isChecked = event.target.checked;
});
});
this.setState({ fruites: fruites });
};
I just this method for individual checkbox .
handleCheckChieldElement = event => {
let fruites = this.state.fruites;
fruites.forEach(fruite => {
if (fruite.value === event.target.value)
fruite.isChecked = event.target.checked;
});
this.setState({ fruites: fruites });
};
Render:Here is my UI, I want to select All checkbox based on group . For example , I have got two group of value - such as Group , Topgroup. The problem is that , When I click on the group , it will select All checkbox including Topgroup and also I click banana , it will select all banana , I don't want to get all banana when click on one item. I don't to want to get topgroup checkbox when I select on the group.
{[{ id: 1, name: "group" }, { id: 2, name: "topGropup" }].map(item => (
<div>
<input
type="checkbox"
onChange={this.handleAllChecked(item.id)}
value="checkedall"
/>{" "}
{item.name}
<ul>
{this.state.fruites.map((fruite, index) => {
return (
<CheckBox
key={index}
handleCheckChieldElement={this.handleCheckChieldElement}
{...fruite}
/>
);
})}
</ul>
</div>
))}
</div>
How can I resolve this problem . Here is my codesanbox : https://codesandbox.io/s/react-multi-select-checkbox-or6ko
Here, I edited your codesandbox: https://codesandbox.io/s/react-multi-select-checkbox-bbuky
Basically you have 8 checkboxes, even though its 4 items displayed, duplicated for each group.
I added the 4 missing items in your state, but you'd actually want some kind of factory function that lets you create your state given the groups you have.
I had to edit some of your values since you were relying on stuff that now is not unique anymore, like value and use the group's id for example to create a unique identifier groupId-itemId.
Memory pointer to the same list
The groups in the app have the same pointer to memory list of fruits.
because of that the updates will affect on both groups.
See how I fixed it:
https://codesandbox.io/s/react-multi-select-checkbox-r29d1
I found some things in the app that can be improve so I improve them for example:
label to input checkbox to be able to click also on the text
I am here if you have any problem, I suggest you to learn Hooks.

How to disable other divs coming from loop except the clicked one

I have some li tags whose data is coming from a loop. When I click any li tag it will become active by changing its image and will store into localstorage so that on refresh the clicked one is still active. Here when we click, an active object is adding to json the clicked li tag and stores it in localstorage. Now the problem is when I click again on the clicked li tag or outside it, it's toggling the earlier image, which should not be happening. Again, other li tags should be disabled except the clicked one. Here is the code below https://stackblitz.com/edit/angular-skzgno?file=src%2Fapp%2Fapp.component.ts
app.component.html
<hello name="{{ name }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<div>
<pre>
</pre>
<ul>
<li *ngFor="let item of statusdata" (click)="toggleActive(item, !item.active)">
<span>{{item.id}}</span>
<span>{{item.name}}</span>
<span>
<img *ngIf="!item?.active || item?.active === false" src ="https://dummyimage.com/qvga" />
<img *ngIf="item?.active === true" src ="https://dummyimage.com/300.png/09f/fff" />
</span>
</li>
</ul>
</div>
app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
statusdata: any;
ngOnInit() {
this.statusdata = [
{ id: 1, name: "Angular 2" },
{ id: 2, name: "Angular 4" },
{ id: 3, name: "Angular 5" },
{ id: 4, name: "Angular 6" },
{ id: 5, name: "Angular 7" }
];
this.statusdata.forEach(item => {
this.getCacheItemStatus(item);
});
}
toggleActive(item, activeStatus = true) {
item.active = activeStatus;
localStorage.setItem(`item:${item.id}`, JSON.stringify(item));
}
getCacheItemStatus(item) {
const cachedItem = localStorage.getItem(`item:${item.id}`);
if (cachedItem) {
const parse = JSON.parse(cachedItem); // Parse cached version
item.active = parse.active; // If the cached storage item is active
}
}
}
It looks to me like you just need to add some logic to your toggleActive method. You could check if any items are active before deciding whether or not to do anything about the click. Does something like this solve your problem?
toggleActive(item, activeStatus = true) {
if(item.active || !this.statusdata.some(d => d.active)){
item.active = activeStatus;
localStorage.setItem(`item:${item.id}`, JSON.stringify(item));
}
}

show element in v-for list: VueJS [duplicate]

This question already has an answer here:
Vue.js - Add class to clicked button
(1 answer)
Closed 3 years ago.
I have a v-for which display all my items and I have a panel for each items (to modify and delete) but when I click on this button to display my panel, it appears on all of my items. How can I avoid that ? This is the same thing when I click on modify button, the input to modify my item appears on each element.
There is my code :
<div v-for="(comment, index) in comments" :list="index" :key="comment">
<div v-on:click="show = !show">
<div v-if="show">
<button #click="edit(comment), active = !active, inactive = !inactive">
Modify
</button>
<button #click="deleteComment(comment)">
Delete
</button>
</div>
</div>
<div>
<p :class="{ active: active }">
{{ comment.content }}
</p>
<input :class="{ inactive: inactive }" type="text" v-model="comment.content" #keyup.enter="doneEdit">
</div>
</div>
And the methods & data :
data() {
return {
show: false,
editing: null,
active: true,
inactive: true
}
},
methods: {
edit(comment) {
this.editing = comment
this.oldComment = comment.content
},
doneEdit() {
this.editing = null
this.active = true
this.inactive = true
}
}
You have the same show, editing, active, inactive state for all items. So if you change some data property for one item it changed for all.
There are a lot of ways to achieve what you want.
The easiest is to manage your data by index.
For example:
<div v-on:click="showIndex = index">
<div v-if="showIndex === index">
...
data () {
return {
showIndex: null
...
The main problem with this approach - you can show/edit only one item at the time.
If you need more complicated logic and whant to manage more then one item at the time I suggest to create a separate component for your items and each will have own state (show, editing etc.)
#NaN's approach works if you want to only have one open at a time. If you want to have the possibility of having multiple open at the same time you would need to keep track of each individual element. Right now you are only basing it on show. Which can only be true/false for all elements at the same time.
So this is what you need to do:
Change show from a boolean to an array
data() {
return {
show: [],
editing: null,
active: true,
inactive: true,
}
},
Then you can keep track of which element should have the panel or not:
<div v-on:click="toggleActive(index)">
And the method:
methods: {
toggleActive(index) {
if (this.show.includes(index)) {
this.show = this.show.filter(entry => entry !== index);
return;
}
this.show.push(index);
}
}
and finally your v-if becomes:
<div v-if="show.includes(index)">

Displaying array data onclick in React

I would like to iterate through an array and display each element in the array every time I click a button.
so far I have this:
{this.state.users.map(function(user, index){
return <p key={ index }>{user.name} </p>;
}, this)}
This displays each users name on screen, one after each other.
How can I do it so it just shows users[0] and then moves to users[1] or even better, click removes the user at position users[0] and then a new person is at position users[0] and then if the array is empty, it displays the text 'no more users'
I know how to remove elements from arrays etc, it's just the displaying one at a time in React land which I cant do
Based on my understanding to your question may be you are trying to achieve this -
let users = [{
name: "abcd"
}, {
name: "xyz"
}, {
name: "temp"
}];
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
activeIndex: 0
};
}
tick =() => {
let activeIndex = this.state.activeIndex;
if (activeIndex == this.props.users.length -1){
activeIndex = 0;
} else {
activeIndex++;
}
this.setState({
activeIndex
});
}
render() {
return (
< ul className = "list-group" >
< li className = "list-group-item" >
{this.props.users[this.state.activeIndex].name}
< button className = "btn btn-default" onClick={this.tick} >
show Next
< /button>
</li >
< /ul>
);
}
}
ReactDOM.render(
< Example users = {users}/ > ,
document.getElementById('test')
);
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<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="test">
</div>
Keep the index of activeUser in state, then render just this one user: { this.state.users[ this.state.activeUser ].name}. Increment/decrement the activeUser on click.
demo on jsfiddle
var Users = React.createClass({
getInitialState(){
return {
users : [{ name : 'John'}, { name : 'Jane'}, { name : 'Robert' }],
activeUser : 0
};
},
next(){
/* here you should add checking if there are more users before setting the state */
this.setState({ activeUser : this.state.activeUser + 1 });
},
render: function() {
return (<div>
<p>{ this.state.users[ this.state.activeUser ].name} </p>
<span onClick={ this.next }>next</span>
</div>);
}
});
Seems like you have the pseudo code there in your question which like Rajesh asked seems to describe some kind of paginated list of users/carousel.
You'll need to define the maximum number of users that are visible in the list and then store the current topmost element index. I'd suggest storing these in state.
Then store the list of names as an array of object each containing the name details as well as a parameter that will represent whether it is visible or not.
Your initial state could look like this:
{
currentTopElement: 0,
maxNamesToShow: 1,
names: [
{name: 'John Smith', isVisible: false},
{name: 'Jane Smith', isVisible: false},
...etc.
]
}
Add a click handler on your button would be required that would increment the currentTopElement by one.
iterate through the names array setting isVisible to false unless the index matches that of currentTopElement, if it does set isVisible to true
To finish off, in the component that needs to render your names list can do so if you conditionally render a name only if its corresponding isVisible value is true:
const elementsToShow = this.state.filter( t => t.isVisible);
You can the iterate over elementsToShow like so:
elementsToShow.map((user, index) => <p key={ index }>{user.name} </p>);

Calling a function from a conditional variable - ReactJS

I have this Button A, with content "All categories". When I click this button, I want a div with content "show more" above Button A to display 6 other buttons, each consisting of "category 1", "category 2" up to 6. (and "show more" to disappear). Clicking any of these 6 buttons would hide the 6 buttons, and go back to being the "show more" div.
CODE
var React = require('react');
module.exports = React.createClass({
getInitialState: function() {
return ({
category: false,
selected: 'Total',
categories: [
{
name: 'Button 1',
id: 1,
},
{
name: 'Button 2',
id: 2,
},
{
name: 'Button 3',
id: 3,
},
{
name: 'Button 4',
id: 4,
},
{
name: 'Button 5',
id: 5,
},
{
name: 'Button 6',
id: 6,
}
]
})
},
selectCategory: function(event) {
(this.state.category) ? this.setState({category: false}) : this.setState({category: true})
},
render: function() {
if(this.state.category == true) {
var category = this.state.categories.map(function(category){
return (
<div
className="category-container"
onClick={this.selectCategory}>
<span> {category['name']} </span>
</div>
)
})
} else {
var category = (
<span id="showplease" onClick={this.selectCategory}> Show more </span>
)
}
console.log(this.state.selected)
return (
<div id="landing">
<div id="search-container">
<div id="category-selector-container">
{category}
<div id="selector" onClick={this.selectCategory}> Button A </div>
</div>
</div>
</div>
)
}
})
Simple enough. HERE IS MY ISSUE: Clicking any of the 6 buttons does not change the state, as if after being mapped, the individual components lost the 'onClick={this.selectCategory}' part. However, clicking the "Show more" div will run the selectCategory function, and will show the 6 buttons.
Any help? I can simply avoid the mapping and individually repeat the buttons, but I would like to know why this is not working.
Thank you in advance!
Your problem involves the this variable. Whenever you create a new function, a new context for this is created and the old one is lost.
There are a few ways around this. A common one is to assign this to a different variable, e.g. var self = this;. Another way is to use bind to ensure this is passed from the outer scope. i.e.
var category = this.state.categories.map(function (category) {
return (
<div
className="category-container"
onClick={this.selectCategory}
>
<span>{category['name']}</span>
</div>
);
}.bind(this));
If you're using ES6, the best way is to use arrow functions, which automatically bind this.
var category = this.state.categories.map((category) => (
<div
className="category-container"
onClick={this.selectCategory}
>
<span>{category['name']}</span>
</div>
));

Categories

Resources