Why is my setSelected useState not accepting the data from the .map function? I have the following react js code:
const [ selected, setSelected ] = useState(null)
const sectionItems = [
{ id: "1", title: "title1", description: "description1" },
{ id: "2", title: "title2", description: "description2" },
{ id: "3", title: "title3", description: "description3" },
]
I am mapping through the sectionItems and rendering a modal, based on if selected has an item or not:
{sectionItems.map((section, index) => {
return (
<div key={section.id} className="processSection1" onClick={setSelected(section) >
<div className="processTitle" >{section.title}</div>
</div>
)
})}
{selected ? <Modal title={selected.title} description={selected.description} /> : " "}
Problem: Why cant I pass the data into setSelected? Or the more precise question is, how can I render the modal with each sectionItem?
Also am getting this error: Too many re-renders. React limits the number of renders to
prevent an infinite loop.
you have to use onClick like this
onClick={()=>setSelected(section)}
If you want to add a value to a function you should use an inline function inside the onClick. Right now you are triggering the function for each rendering at render time.
Change:
onClick={setSelected(section)}
to:
onClick={() => setSelected(section)}
So I am making this project in ReactJs, which has a sidebar, where I am trying to implement dropdown menu.
Required Behavior
If I click in any of the option of the sidebar, if it has a submenu, it will show. And close upon again clicking.
Current Behavior
If I click any of the options, all the submenus are showing at once.
For example if I click publications option, it shows me all the options, such as featured publications, journal publications.
How do I fix that?
My sidebarItems array
const sidebarItems = [
{
title: "Publications",
url: "#",
subMenu: [
{
title: "Journal Publications",
url: "#",
},
{
title: "Featured Publications",
url: "#",
},
],
},
{
title: "Team Members",
url: "#",
subMenu: [
{
title: "Current Members",
url: "#",
},
{
title: "Lab Alumni",
url: "#",
},
],
},
{
title: "Projects",
url: "#",
subMenu: [
{
title: "Project 1",
url: "#",
},
{
title: "Project 2",
url: "#",
},
{
title: "Project 3",
url: "#",
},
],
},
{
title: "News",
url: "#",
},
{
title: "Contact Us",
url: "#",
},
];
export default sidebarItems;
The Sidebar Component
import { useState } from "react";
import { Box, Text } from "#chakra-ui/react";
import sidebarItems from "./sidebarItems";
export default function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Box>
{sidebarItems.map((items) => {
return (
<Box
width='200px'
height='40px'
textAlign='center'
cursor='pointer'
onClick={() => {
setIsOpen(!isOpen);
}}
>
<Text>{items.title}</Text>
{isOpen
? items.subMenu?.map((item) => {
return <Text>{item.title}</Text>;
})
: ""}
</Box>
);
})}
</Box>
</div>
);
}
You have to use an array state variable. Your single state variable isOpen is dictating all the subMenus here:
{isOpen
? items.subMenu?.map((item) => {
return <Text>{item.title}</Text>;
})
: ""}
You need to have an array state variable here, so each sidebar item has a corresponding boolean to dictate opening/closing of value.
const [isOpen, setIsOpen] = useState(Array(sidebarItems.length).fill(false));
Now you have to ensure that you are setting it correctly and manipulating the right array element.
onClick={() => {
let newIsOpen = [...isOpen];
newIsOpen[index] = !isOpen[index];
setIsOpen(newIsOpen);
}}
I hope this helps you reach your solution
This is happening because you are using wrong logic and you don't specify which submenu should be shown.
First, delete the current state and dont use it.
Then, you should define another state like:
const [selectedMenu, setSelectedMenu] = useState("");
then, define this function:
const handleClick = (title) => {
setSelectedMenu(title);
}
after that, once you click on Box, you should invoke function like this:
onClick={() => handleClick(item.title)}
consequently, you should write your logic like this:
<Text>{items.title}</Text>
{item.title === selectedMenu
? items.subMenu?.map((item) => {
return <Text>{item.title}</Text>;
})
: ""}
I think the problem is occurring because you have only 1 state variable set for every sidebar option. Every sidebar option should have its own variable keeping track of whether its submenu should open or not.
In your code, when the isOpen state variable is set to true then when the function maps over all the options the variable's value will always be true.
Try setting a variable for each of the menu options which contains whether the submenu should open or not.
I'm reading from a JS object (from JSX) and trying to pass value of a component but it's rendered as string.
I tried placing components (in icon key of data see below) in {} but that doesn't help as data gives an error.
Here's the simplified version of the files.
data.js as below:
const data = [
{
title: "some title",
desc: "some desc",
},
[
{
icon: "<TwitterIcon />",
title: "title 1",
desc: "desc 1",
},
{
icon: "<FacebookIcon />",
title: "title 2",
desc: "desc 2",
},
],
]
export { data }
index.js that reads data object and passes as props to AnotherComponent:
import { data } from "../path/to/data"
import AnotherComponent from "../path/to/AnotherComponent"
const Homepage = () => {
return (
<AnotherComponent {...data} />
)
}
AnotherComponent.jsx as below:
import {TwitterIcon, FacebookIcon} from "../path/to/CustomIcons"
const AnotherComponent = ({ ...data}) => {
return (
{data[1].map(item => (
<div>{item.icon}</div> // this prints string as opposed to rendering the component
<div>{item.title}</div>
<div>{item.desc}</div>
))}
)
}
index.js returns:
<div><TwitterIcon /></div>
<div>title 1</div>
<div>desc 1</div>
<div><FacebookIcon /></div>
<div>title 2</div>
<div>desc 2</div>
In the object you are defining as:
{
icon: "<TwitterIcon />",
title: "title 1",
desc: "desc 1",
}
Don't use "<TwitterIcon />" It will always return a string, instead use TwitterIcon:
{
icon: TwitterIcon,
title: "title 1",
desc: "desc 1",
}
And finally, call it where you need it, in this way:
const AnotherComponent = ({ ...data}) => {
return (
{data[1].map(item => (
<div><item.icon /></div> // HERE: check I'm calling item.icon as React Component
<div>{item.title}</div>
<div>{item.desc}</div>
))}
)
}
In this way you are passing the icon to anywhere you want and not just passing a string. So, you can call it as a Component when you need it to render. I do it a lot in my work.
I think you should pass directly the icon component in the object, like this:
const data = [
{
title: "some title",
desc: "some desc",
},
[
{
icon: <TwitterIcon />,
title: "title 1",
desc: "desc 1",
} ...
Then in index.js you can do (it is more clear to pass down props like this):
const Homepage = () => {
return (
<AnotherComponent data={data} />
)
}
In AnotherComponent.jsx now you can do:
const AnotherComponent = ({data}) => {
return (
{data[1].map(item => (
<div>{item.icon}</div>
<div>{item.title}</div>
<div>{item.desc}</div>
))}
)
}
Currently I use Vuetify for base components and would like to create reusable extensions. For example a list containing checkboxes, a datatable column with some functionality etc.
For this question I will take the list containing checkboxes example. I created the following component called CheckboxGroup.vue
<template>
<v-container>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
<script>
export default {
props: {
items: Array,
required: true
}
};
</script>
This component takes an array of objects as a property and creates a checkbox for each entry.
Important parts are v-model="item.state" and :label="item.title". Most of the time the state attribute will have a different name, same for the title attribute.
For testing purposes I created a view file called Home.vue holding an array of documents.
<template>
<v-container>
<CheckboxGroup :items="documents"/>
<v-btn #click="saveSettings">Save</v-btn>
</v-container>
</template>
<script>
import CheckboxGroup from "../components/CheckboxGroup";
export default {
components: {
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
};
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
}
};
</script>
This time title is called name and state is called deleted. Obviously CheckboxGroup is not able to manage the documents because the attribute names are wrong.
How would you solve this problem? Would you create a computed property and rename these attributes? Would be a bad idea I think...
And by the way, is using v-model a good idea? A different solution would be to listen to the changed event of a checkbox and emit an event with the item index. Then you would have to listen for the change in the parent component.
I don't think there is a way to create something like
<CheckboxGroup :items="documents" titleAttribute="name" stateAttribute="deleted"/>
because it would be bad design anyway. I hope that this is a very trivial problem and every Vue developer has been confronted with it, since the primary goal should always be to develop abstract components that can be reused multiple times.
Please keep in mind that this checkbox problem is just an example. A solution for this problem would also solve same or similar problems :)
If I understood what you wanted, it`s not so trivial. Using props is a good idea. You dont need to manage the documents attribute names, just set the attribute name to your component.
Note
Renaming the attributes or using proxies is more resource-intensive like this solution, because you need to run loop to rename the attribute names or apply aliases to data array objects.
Example
CheckboxGroup.vue
<template>
<v-container fluid>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item[itemModel]"
:label="item[itemValue]"
></v-checkbox>
<hr>
{{items}}
</v-container>
</template>
<script>
export default {
name: "CheckboxGroup",
props: {
items: {
type: Array,
required:true
},
itemValue:{
type:String,
default: 'title',
// validate props if you need
//validator: function (value) {
// return ['title', 'name'].indexOf(value) !== -1
// }
// or make required
},
itemModel:{
type:String,
default: 'state',
// validate props if you need
//validator: function (value) {
// validate props if you need
// return ['state', 'deleted'].indexOf(value) !== -1
// }
// or make required
}
}
};
</script>
Home.vue
<template>
<div id="app">
<checkbox-group :items="documents"
item-value="name"
item-model="deleted"
>
</checkbox-group>
</div>
</template>
<script>
import CheckboxGroup from "./CheckboxGroup.vue";
export default {
name: "App",
components: {
// HelloWorld,
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
}
}
};
</script>
Based on your example I`v tried to show how to create component to managing object attributes in child component. If you need more information, please let me know.
Some good answers here that definitely solve your issue - you are essentially wanting to pass data down to a child (which isn't bad design - you were on the right track!)..
I am kind of shocked that slots or scoped-slots haven't been mentioned yet... so I figured I would chime in..
Scoped-slots allow you to take advantage of data you are passing to a child - but within the parent. The child essentially "reflects" data back to the parent, which allows you to style the child component/slot however you wish, from the parent.
This is different than just passing data via a prop attribute, because you would have to rely on styling within the child - you couldn't change the styles on a 'per-use' basis. The styles you set in the child would be "hard coded"..
In this example I am riding on top of the already provided label slot that Vuetify provides - just passing my own custom scoped-slot to it.. How to find documentation on v-checkbox slots
I made some minor changes to help spice some things up, and to show how you have greater control over styles this way (and you can use any object prop for the label you want .name, .whatever, .label, etc..)
Lastly, it is important to note that Vuetify already provides a "grouped checkbox" component - v-radio-group - I know it's called "radio-group" but it supports checkboxes...
Edit: fixed the state "issue"...
Scoped Slots With Render Function - Original Answer Moved To Bottom
Thanks to #Estradiaz for collaborating on this with me!
Vue.component('checkboxgroup', {
props: {
items: { type: Array, required: true }
},
render (h) {
return h('v-container', this.items.map((item) => {
return this.$scopedSlots.checkbox({ item });
}));
},
})
new Vue({
el: "#app",
data: {
documents: [{
id: 1,
name: "Doc 1 - delete",
deleted: false,
icon: "anchor",
},
{
id: 12,
title: "Doc 1 - state",
state: false,
icon: "anchor",
},
{
id: 2,
name: "Doc 2 - delete",
deleted: false,
icon: "mouse"
},
{
id: 3,
name: "Doc 3 - delete",
deleted: true,
icon: "watch"
}
]
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet" type="text/css">
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet" type="text/css"></link>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.0.8/css/all.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/2.1.99/css/materialdesignicons.min.css" rel="stylesheet" />
<div id="app">
<v-app>
<v-container>
<CheckboxGroup :items="documents">
<template #checkbox={item}>
<v-checkbox
v-model="item[(item.name && 'deleted') || (item.title && 'state') ]" color="red">
<template #label>
<v-icon>mdi-{{item.icon}}</v-icon>
{{ item.name || item.title }}
{{ item }}
</template>
</v-checkbox>
</template>
</CheckboxGroup>
</v-container>
</v-app>
</div>
My attempt of a json to component parser
usefull names are welcome
so basically you can target element tagnames as slot #[slotname] or put slot names and target entries to overwrite the default component.
omiting tag property in the component will append children to the parent vnode
Consider:
[
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
title: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]
Example :
Vue.component('Liste', {
props:["tag", "items"],
render(h){
console.log(this.items)
let tag = this.tag || (this.$parent.$vnode && this.$parent.$vnode.tag)
if(tag === undefined) throw Error(`tag property ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
return (
this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item})
)
|| h(ElementTag, {
attrs: attrs,
scopedSlots: this.$scopedSlots
})
}))
}
})
new Vue({
data(){
return {
items: [
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
text: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<Liste tag="p" :items="items">
<template #input="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.date"/>
</template>
<template #slotvariant="{item}">
slotvariant - {{item.text}}<br>
</template>
<template #deleted="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.deleted"/>
</template>
</Liste>
</div>
Typescript:
import {Vue, Component, Prop} from 'vue-property-decorator'
export type AbstractElement = {
[key: string]: any // passed as $attrs | useable for assigned $props
ElementTag: string
slotName?: string
}
#Component<List>({
render(h){
let tag = this.tag
|| (this.$parent.$vnode && this.$parent.$vnode.tag)
|| (this.$parent.$el && this.$parent.$el.tagName)
if(tag === undefined) throw Error(`tag prperty: ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
console.log("slotName", slotName)
return (this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item}))
|| h(ElementTag, {
attrs: attrs,
slot: slotName || ElementTag,
scopedSlots: this.$scopedSlots
})
}))
}
})
export default class List extends Vue{
#Prop(String) readonly tag?: string
#Prop(Array) readonly items!: Array<AbstractElement>
}
will raise this here
You can use a Proxy to map the document property names during access.
Note
In my original answer, I used Proxy handlers for get and set, which is sufficient for plain javascript objects, but fails when used with Vue data properties because of the observer wrappers that Vue applies.
By also trapping has in the Proxy, this can be overcome. I left the original answer below for anyone interested in this problem.
Here is a demo of how to use Proxy to 'alias' Vue reactive properties to different names
without affecting the original data structure
without having to copy the data
console.clear()
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.component('checkboxgroup', {
template: '#checkboxGroup',
props: { items: Array, required: true },
});
const aliasProps = (obj, aliasMap) => {
const handler = {
has(target, key) {
if (key in aliasMap) {
return true; // prevent Vue adding aliased props
}
return key in target;
},
get(target, prop, receiver) {
const propToGet = aliasMap[prop] || prop;
return Reflect.get(target, propToGet);
},
set(target, prop, value, receiver) {
const propToSet = aliasMap[prop] || prop;
return Reflect.set(target, propToSet, value)
}
};
return new Proxy(obj, handler);
}
new Vue({
el: '#app',
data: {
documents: [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
},
computed: {
checkBoxItems() {
const aliases = {
title: 'name',
state: 'deleted'
}
return this.documents.map(doc => aliasProps(doc, aliases));
}
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
},
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet"/>
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet"/>
<div id="app">
<v-app id="theapp">
<v-container>
<checkboxgroup :items="checkBoxItems"></checkboxgroup>
<v-btn color="info"
#click="saveSettings">Save</v-btn>
</v-container>
</v-app>
</div>
<template id="checkboxGroup">
<v-container style="display: flex">
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
Original answer
You can use a Proxy to map the document property names during access.
<template>
...
<CheckboxGroup :items="checkBoxItems"/>
...
</template>
<script>
export default {
...
computed: {
checkBoxItems() {
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
return documents.map(doc => new Proxy(doc, handler))
},
},
...
}
</script>
Demo
const documents = [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
const checkItems = documents.map(doc => new Proxy(doc, handler))
console.log('Accessing new property names via checkItems')
checkItems.forEach(ci => console.log(ci.id, ci.title, ci.state))
console.log('After update, values of documents')
checkItems.forEach(ci => ci.state = !ci.state )
documents.forEach(doc => console.log(doc.id, doc.name, doc.deleted))
Ok, so I am very confused about the way of making a LeftNav menu with material-ui.
I am new on the project and I updated reactjs and material-ui.
So, a lot of stuff have been deprecated about LeftNav from material-ui and I am trying to fix it.
Here is the menu as it was when I opened the project (with all the console warning):
<LeftNav ref="leftNav"
docked={false}
style={{opacity: '0.9'}}
menuItems={menuItems}
onChange={this.leftNavOnChange} />
From this array:
var menuItems = [
{ route: '/', text: 'Home' },
{ type: 'SUBHEADER', text: 'Connect' },
{ route: '/categories', text: 'Categories' },
{ route: '/icons', text: 'Icons'},
{ route: '/Tmp', text: 'Tmp', disabled: !Permissions['connect_v2_list_tmp']['isPermitted'] },
{ route: '/wizard', text: 'Wizard', disabled: !Permissions['connect_v2_analyze_spreadsheet']['isPermitted'] },
{ route: '/linkshortener', text: 'Link shortener'},
{ type: 'SUBHEADER', text: 'Visual search' },
{ route: '/whitelist', text: 'Whitelist', disabled: !Permissions['connect_v2_list_whitelist']['isPermitted'] },
{ route: '/blacklist', text: 'Blacklist', disabled: !Permissions['connect_v2_list_blacklist']['isPermitted'] },
{ type: 'SUBHEADER', text: 'Tmp-wise' },
{ route: '/viewer', text: 'Viewer', disabled: !Permissions['connect_v2_view_bw_entity']['isPermitted']},
];
And here is what I did from what I understood about the way of doing it:
<LeftNav ref="leftNav"
docked={false}
style={{opacity: '0.9'}}
//menuItems={menuItems}
//onChange={this.leftNavOnChange}
>
{menuItems.map(function(items, i) {
if (items.route) {
return <MenuItem linkButton={true} href={items.route} key={i}>{items.text}</MenuItem>;
} else {
return <MenuItem data={items.type} key={i}>{items.text}</MenuItem>;
}
})}
</LeftNav>
So, less warning except one : using methods on left nav has been deprecated. Please refer to documentations.
but not such a big deal.
My problem here, is that my links are not working. I am staying on the same page.
And my other main problem: all the style it had is gone.
So, my question is:
am I doing it right?
Or am I missing something owned by reactjs and / or material-ui?
Thanks a lot in advance for the time spent on my request.
This is what I do (I am using react-router):
import { browserHistory } from 'react-router';
handleLeftNav: function (route) {
browserHistory.push(route);
this.setState({
leftNavOpen: false
});
},
<MenuItem onTouchTap={() => { return this.handleLeftNav('/route/'); }}>Route</MenuItem>
If you move your map outside of the LeftNav then you should no longer receive this warning. When I compose my LeftNav I follow this pattern and I don't get the error you're reporting. Hope this helps.
let menuItems = menuItems.map(function(items, i) {
if (items.route) {
return <MenuItem linkButton={true} href={items.route} key={i}>{items.text}</MenuItem>;
} else {
return <MenuItem data={items.type} key={i}>{items.text}</MenuItem>;
}
});
<LeftNav ref="leftNav"
docked={false}
style={{opacity: '0.9'}}
//menuItems={menuItems}
//onChange={this.leftNavOnChange}
>
{ menuItems }
</LeftNav>