I've using this code over and over again. Wonder if there's a better way to make this shortcut
Here's the example
// items.js
export default [
{ title: 'ABC', href: 'javascript:void(0)' },
{ title: 'DEF', href: 'javascript:void(0)' }
]
// index.vue
<script>
import items from './items.js'
export default: {
computed: {
links() {
let results = []
items.forEach((item,index) => {
item.id = index
results.push(item)
})
return results
}
}
}
</script>
//returned result = { title: 'ABC', href: 'javascript:void(0)', id: 0 }
I just want to add id to each of the object in the computed property, so that I not worry about using v-for="(item, index) in items" :key="index". instead just using v-for="item in links" :key="item.id" in the template
The question:
From the code, you see I declare empty array of results let results = [] then I populate the result from forEach data before I return the result. Is that any better way to do to return each of the loop data without declare empty array and populate it before return the array of that generated data?
Sometimes, I not just add id, maybe add another like item.internal = true. Or level if it's multilevel.
You could use map method and spread operator to add id to the item :
computed: {
links() {
return items.map((item,index)=>({id:index,...item}))
}
}
Related
Problem Statement:
I am trying to work on a design that involves replicating the Parties Being Served form fields on each Add button click. On clicking the Add Button, the corresponding PartyInfo object gets added to the PartyInfoList array
In the BaseButtonList.vue file, I use the PartyInfoList array to setup the v-for loop
<base-card
v-for="(data, index) of partiesInfoList" :key="index"
ref="childComponent"
#add-parties="updatePartiesInfoList"
#delete-party="deleteParty(index)"
:lastElement="index === partiesInfoListLength - 1"
>
However, it is important to note that initially the array has no elements. But, since I have no other way to enter the data to the array, I'm forced to load the form despite the partiesInfoList array having zero elements. I achieve this by setting the partiesInfoList with dummy data. This allows the initial form to be displayed. And when the Add Another button gets hit, this first real entry will be pushed to the 2nd index of the partiesInfoList.
However, this causes serious issues when deleting the entries from the array. The splice does function properly when I console log the output. But, Vue ends up deleting the wrong element from the DOM.
I have used a unique index key and also tried other possible keys, but all yield the same pernicious bug. Any help would be greatly appreciated. I think this is a really tricky design pattern, I can think of easier alternatives, but I want to get this working as a challenge. Maybe there's a better data flow I can set up or something.
Attached is the code
BaseCard.vue
<template>
//contains the template for the input form element
</template>
<script>
import { random } from '#amcharts/amcharts4/.internal/core/utils/String';
import { EventBus } from './bus.js';
export default {
emits:['add-parties','delete-party'],
props:['lastElement'],
data() {
return {
partyInfo:
{
id: '',
fullName: '',
preAuthorize: '',
serviceAddress: '',
},
validation: {
fullNameIsValid: true,
serviceAddressIsValid: true
},
hideAddButton: false,
formIsValid: true,
addServiceButtonText: '+ Add Service Notes (Optional)',
serviceNotes: [],
showServiceNotes: false,
showDeleteButton: true,
enteredServiceNote: '', //service notes addendum
}
},
computed : {
showServiceNotex(){
if(!this.showServiceNotes){
return '+Add Service Notes (Optional)'
}else{
return '- Remove Service Notes';
}
}
},
methods: {
setServiceNotes(){
this.showServiceNotes = !this.showServiceNotes;
},
addAnotherParty(){
this.validateForm();
if(this.counter === 0){
this.counter++;
this.lastElement = false;
}
if(!this.formIsValid){
return;
}
let emitObj = JSON.parse(JSON.stringify(this.partyInfo));
this.$emit('add-parties', emitObj); //event
},
deleteParty(){
this.$emit('delete-party');
},
validateForm(){
this.formIsValid = true;
if(this.partyInfo.fullName === ''){
this.validation.fullNameIsValid = false;
this.formIsValid = false;
}
if(this.partyInfo.serviceAddress === ''){
this.validation.serviceAddressIsValid = false;
this.formIsValid = false;
}
},
clearValidity(input){
this.validation[input] = true;
},
clearForm(){
this.partyInfo.fullName = '';
this.partyInfo.serviceAddress = '';
this.partyInfo.preAuthorize = false;
}
},
created(){
console.log('created');
}
}
</script>
Attached is the <BaseCardList> which renders the form elements in a v-for loop
BaseCardList.vue
<template>
<ul>
<base-card
v-for="(data, index) of partiesInfoList" :key="index"
ref="childComponent"
#add-parties="updatePartiesInfoList"
#delete-party="deleteParty(index)"
:lastElement="index === partiesInfoListLength - 1"
>
<!-- Wrapper for the `Parties Being Served` component-->
<template v-slot:title>
<slot></slot>
</template>
</base-card>
</ul>
</template>
<script>
import BaseCard from './BaseCard.vue';
export default {
components: { BaseCard },
data() {
return {
selectedComponent: 'base-card',
partiesInfoList : [
{id: 0,
fullName: 'dummy',
serviceAddress: 'dummy',
preAuthorize: ''
}
],
clearForm: false,
counter: 1
}
},
computed : {
hasParty(){
return this.partiesInfoList.length > 0;
},
partiesInfoListLength(){
return this.partiesInfoList.length;
}
},
methods: {
updatePartiesInfoList(additionalInfo){
// if(this.counter == 0){
// this.partiesInfoList.splice(0,1);
// }
this.partiesInfoList.push(additionalInfo);
this.counter++;
console.log(this.partiesInfoList);
console.log('The length of list is '+this.partiesInfoList.length);
},
deleteParty(resId){
// const resIndex = this.partiesInfoList.findIndex(
// res => res.id === resId
// );
// this.partiesInfoList.splice(resIndex, 1);
if(this.counter == 1){
return;
}
this.partiesInfoList.splice(resId, 1);
console.log('Index is '+resId);
console.log('after del');
console.log(this.partiesInfoList);
}
}
}
</script>
Actual Output Bug on screen : The adjacent element gets removed from the DOM. Say,I click on delete for the 'Second' but 'Third' gets removed. But, if there's an empty form element at the end of the v-for, then this one gets deleted.
The keys in your v-for should be an id, not an index. Vue ties each component to its key.
With keys, it will reorder elements based on the order change of keys, and elements with keys that are no longer present will always be removed/destroyed.
vue documentation
When you splice the partyInfoList item, Vue reregisters the indexes. The indexes cascade down, and since the there are now 2 items in partyInfoList instead of 3, the Vue component with an index of 2, or the last component, gets removed.
Here is an example:
// partyInfoList
[
// index: 0
{
id: 0,
fullName: 'first',
serviceAddress: '1',
preAuthorize: ''
},
// index: 1
{
id: 1,
fullName: 'second',
serviceAddress: '2',
preAuthorize: ''
},
// index: 2
{
id: 2,
fullName: 'third',
serviceAddress: '3',
preAuthorize: ''
},
]
Splice
// resId = 1
this.partiesInfoList.splice(resId, 1);
Resulting array
// partyInfoList
[
// index: 0
{
id: 0,
fullName: 'first',
serviceAddress: '1',
preAuthorize: ''
},
// index: 1
{
id: 2,
fullName: 'third',
serviceAddress: '3',
preAuthorize: ''
},
]
The array looks fine, but templates on the screen do not. This is because Vue saw that the partyInfoList item with an index of 2 does not exist anymore, so the component with :key="2" and all of its data was removed the DOM.
Your solution is actually very simple. All you need to do is change :key="index" to :key="data.id". This will tie your component to the data, not a dynamic value like an index.
I also don't know how you are setting the id on each partyInfoList item, but those must be unique for the keys to work.
This is what your new code should look like:
<base-card
v-for="(data, index) of partiesInfoList" :key="data.id"
ref="childComponent"
#add-parties="updatePartiesInfoList"
#delete-party="deleteParty(index)"
:lastElement="index === partiesInfoListLength - 1"
>
In updatePartiesInfoList just before pushing new object to partiesInfoList, check if this.partiesInfoList[0].fullName === 'dummy' returns true, then if the condition is truthy, execute this.partiesInfoList.splice(0, 1) to remove the dummy object from your array!
I'm (obviously) very new to React and Javascript, so apologies in advance if this is a stupid question. Basically I have an array of objects in this.state, each with its own nested array, like so:
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
}, ...
I've already written a function that maps through the state objects and runs .includes() on each object.recipe to see if it contains a string.
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
The main issue is that .map() returns a value for each item in the array, and I don't want that. I need to have a function that checks each object.recipe, returns a match if it finds one (like above), but also returns a "No match found" message if NONE of the nested arrays contain the value it's searching for. Right now this function returns "{this.state.findMe} not found in {thing.type}" for each object in the array.
I do know .map() is supposed to return a value. I have tried using forEach() and .filter() instead, but I could not make the syntax work. (Also I can't figure out how to make this function a stateless functional component -- I can only make it work if I put it in the render() method -- but that's not my real issue here. )
class App extends React.Component {
state = {
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
},
{
type: "chili",
recipe: ["beans", "beef", "tomato"]
},
{
type: "padthai",
recipe: ["noodles", "peanuts", "chicken"]
},
],
findMe: "bread",
}
render() {
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
return (
<div>
<div>
<h3>Results:</h3>
{newArray}
</div>
</div>
)
}
};
You could use Array.reduce for this:
const result = foods.reduce((acc,curr)=> curr.recipe.includes(findMe) ? `${curr.type} contains ${findMe}`:acc ,'nothing found')
console.log(result)
Though if the ingredient is found in more than one recipe it will only return the last one.
Alternatively, you could use a map and a filter:
const result = foods.map((thing, i) => {
if (thing.recipe.includes(findMe)) {
return `${thing.type} contains ${findMe}`;
}}).filter(val=>!!val)
console.log(result.length?result[0]:'nothing found')
I have a dynamic component being injected in the slot of another one and I pass the props object to it. But when I update the data (an array) which has been assosiated with the prop (dataT: this.tableData), that prop isn't being updated inside the component.
It seems like I have a deal with two different objects but the array was passed by the reference, wasn't it?
This is the main component
<template>
<Button #click="addWindows"></Button>
<Window v-for="window in windows" :key="window.id">
<component :is="window.name" v-bind="window.props" #onDeleteRow="handleDeleteRow"></component>
</Window>
</template>
<script>
export default{
data(){
return{
windows:[],
tableData:[
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
methods:{
addWindows(){
this.windows = [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
},
{
id: 1,
name: 'Component2',
props: {}
}];
},
handleDeleteRow(id){
this.tableData = this.tableData.filter(r => r.id != id);
}
}
}
</script>
I expect updating dataT prop in Component1 when I modify this.tableData in the main component.
Original answer based on an earlier version of the question
If you make windows a computed property it can depend on tableData:
export default {
data() {
return {
tableData: [
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
computed: {
windows () {
return [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
}, {
id: 1,
name: 'Component2',
props: {}
}
]
}
}
}
If you can't make all of it a computed property, e.g. because you need to be able to modify it, then keep it as data and just use the computed property to create the array needed in your template. In that case the computed property would just be merging together different parts of the data into the correct form.
In your original code, the line dataT: this.tableData won't work because this.tableData doesn't exist yet, it'll just be undefined. There's no lazy evaluation here, it needs to resolve to the correct object at the point it hits that line.
Even if it was able to get access to the correct object it wouldn't help because in handleDeleteRow you're reassigning tableData to point to a different object. Passing 'by reference' has nothing to do with the name you use to identify the object, it refers to a reference in memory.
Incidentally, v-on also supports an object syntax, just like v-bind, so you could make onDeleteRow optional in a similar fashion.
Update based on the edited question
When you write this in addWindows:
props: {
dataT: this.tableData
}
This will assign the current value of this.tableData to dataT. That current value will be an array and as arrays are reference types any modifications made to that array will apply no matter what identifier is used to reference it.
However, this line...
this.tableData = this.tableData.filter(r => r.id != id);
... does not modify that array. Instead it assigns a totally new array to this.tableData. This will have no effect on the array referenced by dataT, which is unchanged.
There are several ways you could approach solving this, including using a computed property. However, a property getter might provide a convenient sleight-of-hand:
addWindows () {
const vm = this;
this.windows = [
{
id: 0,
name: 'Component1',
props: {
get dataT () {
return vm.tableData
}
}
},
{
id: 1,
name: 'Component2',
props: {}
}
];
}
This will always evaluate to the current value of tableData. Vue's reactivity should be fine with this extra indirection, it just sees it as equivalent to accessing tableData directly.
TL;DR
The issue is with your binding. Use the following:
<component
:is="window.name"
:dataT="window.props.dataT"
#onDeleteRow="handleDeleteRow">
</component>
Explanation
the v-bind attribute specifies what prop is bound to what value (or reference). In your case, you didn't specify what values you're binding to what props, thus the component props weren't bound as expected.
I'm new to vue.js and struggling with the following scenario.
I send an array filled with objects via props to my router-view.
Inside one of my router-view components I use this array in multiple functions, reference it with 'this.data' and safe it inside the functions in a new variable so I don't overwrite the actual prop data.
However the functions overwrite the original prop data and manipulate the data of the prop.
Here is an abstract example of my question:
App.vue
<template>
<div>
<router-view :data='data'></router-view>
</div>
</template>
<script>
export default {
data: function() {
return {
data: [],
};
},
created: function() {
this.getData();
},
methods: {
getData: function() {
this.data = // array of objects
},
}
route component:
<script>
export default {
props: {
data: Array,
},
data: function() {
return {
newData1 = [],
newData2 = [],
}
}
created: function() {
this.useData1();
this.useData2();
},
methods: {
useData1: function() {
let localData = this.data;
// do something with 'localData'
this.newData1 = localData;
}
useData2: function() {
let localData = this.data;
// do something with 'localData'
this.newData2 = localData;
}
}
}
</script>
The 'localData' in useData2 is manipulated from changes in useData1, whereby I don't overwrite the data prop.
Why do I overwrite the prop and how can i prevent it?
The problem you're experiencing a side effect of copying this.data by reference, rather than value.
The solution is to use a technique commonly referred to as cloning. Arrays can typically be cloned using spread syntax or Array.from().
See below for a practical example.
// Methods.
methods: {
// Use Data 1.
useData1: function() {
this.newData1 = [...this.data]
},
// Use Data 2.
useData2: function() {
this.newData2 = Array.from(this.data)
}
}
#Arman Charan is right on his answer. Object and arrays are not primitive types but reference.
There is an awesome video explanation here => JavaScript - Reference vs Primitive Values/ Types
So for reference types you first have to clone it on another variable and later modify this variable without the changes affecting the original data.
However for nested arrays and objects in high level the spead and Array.from will not work.
If you are using Lodash you can use _.cloneDeep() to clone an array or an object safely.
I like functional programming and I use Lodash which I strongly recommend.
So you can do:
let original_reference_type = [{ id:1 }, { id: 2 }]
let clone_original = _.cloneDeep(original_reference_type)
clone_original[0].id = "updated"
console.log(original_reference_type) //[{ id:1 }, { id: 2 }] => will not change
console.log(clone_original) // [{ id: "updated" }, { id: 2 }]
Suggestion: For simple arrays and objects use:
Objects:
let clone_original_data = {...original_data} or
let clone_original_data = Object.assign({}, original_data)
Arrays:
let clone_original_data = [...original_data] or
let clonse_original_data = original_data.slice()
For complex and high nested arrays or Objects go with Lodash's _.cloneDeep()
I think this is most readable, "declarative" way:
First, install lodash npm i lodash. Then import desired function, not the whole library, and initialize your data with array from props.
<script>
import cloneDeep from 'lodash/cloneDeep'
export default {
props: {
data: Array
},
data () {
return {
// initialize once / non reactive
newData1: cloneDeep(this.data),
newData2: cloneDeep(this.data)
}
}
}
</script>
I often find my self struggling with manipulating a specific item in an array, in a React component state. For example:
state={
menus:[
{
id:1,
title: 'something',
'subtitle': 'another something',
switchOn: false
},
{
id:2,
title: 'something else',
'subtitle': 'another something else',
switchOn: false
},
]
}
This array is filled with objects, that have various properties. One of those properties is of course a unique ID. This is what i have done recentely to edit a "switchOn" property on an item, according to its ID:
handleSwitchChange = (id) => {
const newMenusArray = this.state.menus.map((menu) => {
if (menu.id === id) {
return {
...menu,
switchOn: !menu.switchOn
};
} else {
return menu;
};
})
this.setState(()=>{
return{
menus: newMenusArray
}
})
}
As you can see, alot of trouble, just to change one value. In AngularJS(1), i would just use the fact that objects are passed by reference, and would directly mutate it, without any ES6 hustle.
Is it possible i'm missing something, and there is a much more straightforward approach for dealing with this? Any example would be greatly appreciated.
A good way is to make yourself a indexed map. Like you might know it from databases, they do not iterate over all entries, but are using indexes. Indexes are just a way of saying ID A points to Object Where ID is A
So what I am doing is, building a indexed map with e.g. a reducer
const map = data.reduce((map, item) => {
map[item.id] = item;
return map;
}, {})
now you can access your item by ID simply by saying
map[myId]
If you want to change it, you can use than object assign, or the ... syntax
return {
// copy all map if you want it to be immutable
...map
// override your object
[id]: {
// copy it first
...map[id],
// override what you want
switchOn: !map[id].switchOn
}
}
As an helper library, I could suggest you use Immutable.js, where you just change the value as it were a reference
I usually use findIndex
handleSwitchChange = (id) => {
var index = this.state.menu.findIndex((item) => {
return item.id === id;
});
if (index === -1) {
return;
}
let newMenu = this.state.menu.slice();
newMenu[index].switchOn = !this.state.menu[index].switchOn;
this.setState({
menu: newMenu
});
}