React - How to pass an object inside a map array - javascript

I am creating a sidebar in my React application, before I used an array with objects for routing
SideBarItem.js
const SidebarItems = [
{
names: "General",
},
{
name: "E-commerce",
route: '/Maps',
},
{
name: "Maps",
route: '/Maps',
},
{
names: "About",
},
{
name: "Calendar",
route: '/Calendar'
},
];
export default SidebarItems;
Lesson.js
<ul>
{SidebarItems.map((item, index) => {
return (
<>
{item.names && (
<li className="header-menu">
<span>{item.names}</span>
</li>
)}
{item.name && (
<li>
<i className="fa fa-folder"></i>
<Link to={`${path}` + item.route}
className="link">{item.name}</Link>
</li>
)}
</>
);
})}
</ul>
And I got this side bar with links and paragraphs
The names property from the SidebarItems object is a simple <span> tag.
But now I use a different approach instead of creating an object with properties i use map it makes my work easier
const SidebarItems = ['Calendar', 'Messages', 'Notifications'].map((itemName) => ({
name: itemName,
route: `/${itemName}`
}));
This code performs the same logic as the old code. But I can't add the names property, I don't know how to do it, I tried to create an object inside an array
const SidebarItems = ['Calendar', 'Messages', {names: "General"}, 'Notifications'].map((itemName) => ({
name: itemName,
names: itemName.names,
route: `/${itemName}`
}));
But I get an error - Error: Objects are not valid as a React child (found: object with keys {names}). If you meant to render a collection of children, use an array instead.
How can I solve this problem?

Based on the array you're looping through, Here is what you need to do
const SidebarItems = [
"Calendar",
"Messages",
{ names: "General" },
"Notifications",
].map((itemName) => {
if (typeof itemName === "string") {
return {
name: itemName,
names: itemName.names || "",
route: `/${itemName}`,
};
} else {
return {
name: itemName.names,
names: itemName.names,
route: `/${itemName.names}`,
};
}
});

Related

Pass array as prop in React Typescript

I've Navbar component inside which I'm creating a NavDropdown component to use inside the Navbar. I want to pass array as prop to the NavDropdown component to display its dropdown items.
The array will be of structure:
DropDownItems: [
{
title: "Keywords",
to: "/app/keywords",
},
{
title: "Followers",
to: "/app/followers",
},
{
title: "Following",
to: "/app/following",
},
]
However, I'm getting the following 2 errors:
Property 'map' does not exist on type 'SubItemsProps'.
Parameter 'subItem' implicitly has an 'any' type.
Navbar.tsx
type SubItemsProps = {
subItems: DropdownItem[]
}
interface DropdownItem {
subTitle: string,
subLink: string
}
const NavDropdown = ({
title,
subItems
}: {
title: string;
subItems: SubItemsProps;
}) => {
return (
<div>
<div>{title}</div>
<div>
<ul>
{subItems.map(subItem => (
<Link href={subItem.subLink}>
<li>
<span>
{subItem.subTitle}
</span>
</li>
</Link>
))}
</ul>
</div>
</div>
);
};
/* Navbar component */
just edit:
type SubItemsProps = DropdownItem[]
You might be able to solve the problem the following way:
{subItems.subItems.map(subItem => (
<Link href={subItem.subLink}>
<li>
<span>
{subItem.subTitle}
</span>
</li>
</Link>
))}
The type which you have defined has the array as attribute. That is why you have to call to the attribute to get the array.
type SubItemsProps = {
subItems: DropdownItem[]
}

How do I pass different values depending on the imported data in React?

I want to take data from js files classified as categories such as 'Delivery' and 'Cafe' and deliver different data to different pages.
I thought about how to import it using map(), but I keep getting errors such as 'products' is not defined.'
It must be done, but it is not implemented well with javascript and react weak. If you know how to do it, I'd appreciate it if you could let me know.
Products.js
export const Product = [
{
Delivery: [
{
id: '101',
productName: '허니랩',
summary: '밀랍으로 만든 친환경 식품포장랩 허니랩.',
description:
'~~',
images: ['3k7sH9F'],
companyName: '허니랩',
contact: '02-6082-2720',
email: 'lesslabs#naver.com',
url: 'https://honeywrap.co.kr/',
},
{
id: '102',
productName: '허니포켓',
summary: '밀랍으로 만든 친환경 식품포장랩 허니랩. 주머니형태.',
description:
"~~",
images: ['4zJEqwN'],
companyName: '허니랩',
contact: "02-6082-2720",
email: "lesslabs#naver.com",
url: "https://honeywrap.co.kr/",
},
],
},
{
HouseholdGoods: [
{
id: '201',
productName: '순둥이',
summary: '아기용 친환경 순한 물티슈',
description:
'~',
images: ['4QXJJaz'],
companyName: '수오미',
contact: '080-000-3706',
email: 'help#sumomi.co.kr',
url: 'https://www.suomi.co.kr/main/index.php',
},
{
id: '202',
category: ['HouseholdGoods'],
productName: '순둥이 데일리',
summary: '친환경 순한 물티슈',
description: '품질은 그대로이나 가격을 낮춘 경제적인 생활 물티슈',
images: ['OMplkd2'],
companyName: '수오미',
contact: '080-000-3706',
email: 'help#sumomi.co.kr',
url: 'https://www.suomi.co.kr/main/index.php',
},
],
},
];
Delivery.js
(The file was named temporarily because I did not know how to classify and deliver data without creating a js file separately.)
import React from "react";
function Delivery(
productName,
companyName,
contact,
email,
url,
summary,
description
) {
return (
<div className="Product">
<div className="Product__data">
<h3 className="Product__name">{productName}</h3>
<h4>{companyName}</h4>
<h5>Contact: {contact}</h5>
<h5>Email: {email}</h5>
<h5>URL: {url}</h5>
<p className="Product__summary">{summary}</p>
<p className="Proudct__descriptions">{description}</p>
</div>
</div>
);
}
export default Delivery;
Category.js
import React from "react";
import Delivery from "./Delivery";
import { Product } from "./Products";
class Category extends React.Component {
render() {
state = {
products: [],
};
this.setState(_renderProduct());
return <div>{products ? this._renderProduct() : "nothing"}</div>;
}
_renderProduct = () => {
const { products } = this.state;
const renderProducts = products.map((product, id) => {
return (
<Delivery
productName={Product.productName}
companyName={Product.companyName}
contact={Product.contact}
email={Product.email}
url={Product.url}
summary={Product.summary}
description={Product.description}
/>
);
});
};
}
export default Category;
Sorry and thank you for the long question.
There are quite a few different problems I've found.
First is that you call setState inside render in the Category component, this causes an infinite loop. Instead call setState inside a lifecycle method like componentDidMount or use the useEffect hook if using functional components.
Another problem is that state in Category is also defined inside render. In class components you would normally put this in a class constructor outside of render.
In your setState call you refer to _renderProduct(), this should be this._renderProduct() instead.
Now the main problem here is the structure of your data / how you render this structure.
Products is an array of objects where each object either has a Delivery or HouseholdGoods property which is an array of products. I would advise you to change this structure to something more like this:
export const Product = {
Delivery: [
{
id: "101",
},
{
id: "102",
},
],
HouseholdGoods: [
{
id: "201",
},
{
id: "202",
},
],
};
or this:
export const Product = [
{ id: "101", productType: "Delivery" },
{ id: "102", productType: "Delivery" },
{ id: "201", productType: "HouseholdGoods" },
{ id: "202", productType: "HouseholdGoods" },
];
I personally prefer the second structure, but I've implemented the first as this seems to be what you were going for:
class Category extends React.Component {
constructor(props) {
super(props);
this.state = {
products: null,
};
}
componentDidMount() {
this.setState({ products: Product });
}
render() {
const { products } = this.state;
return (
<div>
{products
? Object.keys(products).map((productKey) => {
return (
<div key={productKey}>
{products[productKey].map((product) => {
return (
<Delivery
key={product.id}
productName={product.productName}
companyName={product.companyName}
contact={product.contact}
email={product.email}
url={product.url}
summary={product.summary}
description={product.description}
/>
);
})}
</div>
);
})
: "no products"}
</div>
);
}
}
We need a nested loop here, because we need to map over each property key and over the array of objects inside each property. If you use the other structure for Product I've shown, you can simply map over Product without needing two loops.
Now the last important problem was that you weren't destructuring the props inside your Delivery component, instead you should do something like this:
function Delivery({
productName,
companyName,
contact,
email,
url,
summary,
description,
}) {
return (
<div className="Product">
<div className="Product__data">
<h3 className="Product__name">{productName}</h3>
<h4>{companyName}</h4>
<h5>Contact: {contact}</h5>
<h5>Email: {email}</h5>
<h5>URL: {url}</h5>
<p className="Product__summary">{summary}</p>
<p className="Proudct__descriptions">{description}</p>
</div>
</div>
);
}
Example Sandbox

Struggling in construction the tree hierarchy in react

I wanted to use tree view but I am struggling in construction the tree (hierarchical view of information) in react.
same as https://www.w3schools.com/howto/howto_js_treeview.asp
What I am trying CodeSandBox - https://codesandbox.io/s/unruffled-babbage-9knrz?file=/index.js
JSON - (this is not fixed can be any format)
const data = [
{
title: "Node 1",
childNodes: [
{ title: "Childnode 1.1" },
{
title: "Childnode 1.2",
childNodes: [
{
title: "Childnode 1.2.1",
childNodes: [{ title: "Childnode 1.2.1.1" }]
},
{ title: "Childnode 1.2.2" }
]
}
]
}
];
I have toggle function which expand and vice versa the tree node.
I am struggling in construction the tree in react.
Please guide me.
The first of all you don't need use <Tree/> as the recursive component, instead of it use <Node/>
The second thing in React you don't need to use the querySelector and classList for changing state of a component.
I've implemented my idea in code here
const Tree = () => {
return (
<ul>
{data.map(({ title, childNodes }) => (
<Node key={title} title={title} childNodes={childNodes} />
))}
</ul>
);
};
class Node extends React.Component {
state = {
isOpen: false
};
toggle = () => {
this.setState({
isOpen: !this.state.isOpen
});
};
render() {
const { title, childNodes } = this.props;
const { isOpen } = this.state;
return (
<li>
<span className="caret" onClick={this.toggle}>
{title}
</span>
{childNodes && isOpen && (
<ul>
{childNodes.map(({ title, childNodes }) => (
<Node key={title} title={title} childNodes={childNodes} />
))}
</ul>
)}
</li>
);
}
}
export default Tree

Changing state of object nested in array of objects

As stated in the title I'm trying to change the state of an object nested in an array of objects. I just can not get this to work. I've added a sample of what I'm trying to do in a codesandbox.
I'm using a Material-UI component call ToggleButton (link to demo ToggleButton). I want to change the state to toggle the buttons in the group. I was able to get this working for a create function but can not get it working for my update function.
Trying to change the values of the object in the array is just not working for me. Below are some things I've tried to no success. I want to change the IsTrue: so I can toggle the button to display the users selection.
setAppetizer(true);
setRecipeObjectState({
...recipeObjectState,
Category: [
...recipeObjectState.Category,
{
IsTrue: appetizer,
category: "Appetizer",
},
],
});
This just adds more buttons to my button group.
setAppetizer(true);
setRecipeObjectState((prevState) => ({
...prevState,
Category: [
{
IsTrue: appetizer,
category: "Appetizer",
},
],
}));
I'm just lost now at this point. I just want to also state that this is my first React project that is not just a sandbox for learning the framework and I'm also a jr. developer trying to learn. I have search stackoverflow and nothing has helped me out. I hope I have included enough information for someone to help out.
Your state and app appear to be very convoluted, but the general idea when updating nested array state is to shallowly copy the state at each level where an update is being made. Use array::map to map the Category property to a new array object reference and when the category matches toggle the IsTrue "selected" property.
setRecipeObjectState((prevState) => ({
...prevState,
Category: prevState.Category.map((category) =>
category.category === newCategory // <-- newCategory value from toggled button
? {
...category,
IsTrue: !category.IsTrue
}
: category
)
}));
Since your "selected" calculation is selected={Boolean(item.IsTrue)} you'll want to ensure your IsTrue element values are actually togglable, i.e. just store the boolean value right in the array.
const recipeObject = {
AuthorId: authorId,
BookAuthor: bookAuthor,
BookTitle: bookTitle,
Calories: parseInt(calories),
Category: [
{
IsTrue: false,
category: "Appetizer"
},
{
IsTrue: false,
category: "Breakfast"
},
{
IsTrue: false,
category: "Soup / Salad"
},
{
IsTrue: false,
category: "Vegetarian"
},
{
IsTrue: true,
category: "Meat (Beef, Pork, Chicken)"
},
{
IsTrue: false,
category: "Fish"
},
{
IsTrue: false,
category: "Dessert"
}
],
Description: description,
DurationInMinCook: parseInt(durationInMinCook),
DurationInMinPrep: parseInt(durationInMinPrep),
ImageUrl: imageUrl,
Ingredients: addedIngredients, // array
Instructions: addedInstructions, // array
IsRecipe: true,
Likes: 0,
RecipeId: selectedRecipeId,
ServingSize: parseInt(servingSize),
Title: title,
YouTubeUrl: youTubeUrl
};
You mutating the same reference, you need to render a copy or the component won't render (shallow comparison):
Try updating state like this.
const oldState = recipeObjectState;
oldState.Category = [
{
IsTrue: appetizer,
category: "Appetizer"
}
];
setRecipeObjectState(oldState);
I didn't try your component because it's huge.
Updating a single object property in an array of objects seems to be a very common use-case. Here is generally how you do that. Suppose id is a unique identifier of each object and that we want to toggle selected:
import React, { useState } from "react";
import "./styles.css";
import faker from "faker";
const array = [];
for (let id = 1; id < 10; id++) {
array.push({
id,
name: faker.name.firstName(),
age: faker.random.number(100),
selected: false
});
}
export default function App() {
const [list, setList] = useState(array);
const onClick = (id) => (event) => {
setList((list) =>
list.map((item) =>
item.id === id ? { ...item, selected: !item.selected } : item
)
);
};
return (
<div className="App">
Click on a item:
<ul>
{list.map(({ id, name, age, selected }) => (
<li key={id} onClick={onClick(id)} className="item">
{name} {age}
{selected ? " ✅" : null}
</li>
))}
</ul>
</div>
);
}
https://codesandbox.io/s/xenodochial-river-9sq6g?file=/src/App.js:0-825

creating abstract components that can manage external data

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))

Categories

Resources