Vue.js -- How to prevent data modification between components - javascript

I must say I'm still pretty new to Vue.js.
I build a Point of Sale system with the help of this tutorial.
I have made some modifications to it, for example the items are
fetched with an api call and you now can edit the price of an item in the transaction. It also required some fixing after updating to latest vue version, for example v-repeat to v-for etc.
Now I have a problem that is if you edit the price of an item in the transaction
it also changes the price of the item in the item list where you click to add the items.
Due to reasons I can't provide a whole image of the app or large parts of code.
Here's a screencap trying to explain what's happening
In the item list template the items are rendered from items array with v-for.
In the transaction template the items are rendered from itemList array with v-for.To clarify: itemList array contains the items added to the transaction.
I can't understand why this is happening or better yet how to prevent it. I can consider providing parts of the code if you need it.
EDIT:
In the comments Hector guessed that I'm referencing the same array in both of the templates, but that is not the case.
In the item list component I reference the items array by passing a prop.
<item-list :items="items" :add="onItemClick"></item-list>
In the transaction component I reference the lineItems array by passing a prop.
<transaction :items="lineItems" :edit-number-of-items="editNumberOfItems" :edit-price-each="editPriceEach" :edit-total-qty="editTotalQty" :edit-taxed-price="editTaxedPrice" :editing="editing" :remove-item="removeItem"></transaction>
The first three cells of the transaction table:
<tbody>
<tr v-for="item in items">
<!-- Code -->
<td>{{ item.item.id }}</td>
<!-- Name -->
<td>
<span>{{ item.item.name }}</span>
</td>
<!-- Price Each -->
<td>
<span class="editable-item" v-if="!item.editingPriceEach" #click="editPriceEach(item)">{{ item.item.priceEach }}</span>
<input v-if="item.editingPriceEach" #keyup.enter="editPriceEach(item)" type="text" v-model="item.item.priceEach" autofocus>
<button v-if="item.editingPriceEach" #click="editPriceEach(item)">OK</button>
</td>
In <tr v-for="item in items"> items is referring to the prop name items not the array called items in the data option.
Also for example the function editPriceEach passed to the transaction component as a prop looks like this:
editPriceEach: function (lineItem) {
lineItem.editingPriceEach = !lineItem.editingPriceEach;
}
As you can see it triggers the editingPriceEach property which is then used in the onItemClick function. It's used to add the items from the item list to the transaction:
onItemClick: function (item) {
var found = false;
for (var i = 0; i < this.lineItems.length; i++) {
if (this.lineItems[i].item === item) {
this.lineItems[i].numberOfItems++;
found = true;
break;
}
}
if (!found) {
this.lineItems.push({
item: item,
numberOfItems: 1,
editingNumberOfItems: false,
editingName: false,
editingPriceEach: false,
editingTaxedPrice: false,
editingTotalQty: false
});
}
},
Thank you for your patience with the small amount of code.

Related

Vue how to send index from v-for to method

I'm trying to pass the index of a v-for so i can change some values, i'm adding the doctor team members dynamically and i have this file input to add their image, although when i add doctors it only changes the first's one image because it always sends index of the first doctor, the file input is inside the v-for loop if that helps.
<v-file-input
hide-input
class="d-none"
id="doctorImage"
truncate-length="1"
#change="doctorImage($event, index, doctor)"
></v-file-input>
Method:
doctorImage(e, index, doctor) {
console.log(index)
this.doctors[index] = {
image: URL.createObjectURL(e),
imageData: e,
name: doctor.name,
specialty: doctor.specialty
}
this.doctorChange += 1
}
The index is always 0, although it displays the index number when i create it dynamically on the doctor's card, why is that happening?
Refer List Rendering using v-for.
<div ... v-for="(doctor, index) in doctors" ... >
{{ index }} // you can use index
...
</div>
Ok so i fixed the issue, that happened because i had a label for doctorImage and the file input id was doctorImage also inside the v-for statement, that made multiple same id's and created a conflict.
I iterate with a limit. Example
If the data were:
{
limit: 3,
currentIndex: 2,
listData:[{name:'Data1'},{name:'Data2'},{name:'Data3'},{name:'Data4'},{name:'Data5'}]
}
In the .vue file it would be the following
<span v-for="index in limit" v-bind:key="index">
{{listData[index+currentIndex].name}}
</span>
I make sure that you control that the sum index + currentIndex does not exceed the limit of your data

Vue list items not re-rendered on state change

I have some array of object, when user click button I fetch new array and display display some results.
It works fine until I fetch second array. When I fetch first array with one element and then fetch array with two elements it change (add or remove) only second element.
How I change array value:
fetchAsync(result){
this.issue = result.body;
}
How issues looks like?
const issues = [
{
"id":100,
"key":"DEMO-123",
"summary":"Demo issue description",
"devices":[
{
"id":100,
"name":"iPhone6S",
"browsers":[
{
"id":100,
"name":"Safari",
"displayVariants":[
{
"id":100,
"issueKey":"DEMO-123",
"state":1,
"browserName":"safari",
"user":"user-1",
"landScope":false
}
]
}
]
}
]
}
]
and the value which was changed is issues[].devices[].browsers[].displayVariants[].state
How to force Vue to rerender this component when nested change appear?
[ EDIT ]
I render issues like this:
<tr v-for="issue in issues">
<td>
<div>{{ issue.key }}</div>
<div class="issue-description">[ {{ issue.summary }} ]</div>
</td>
<template v-for="d in issue.devices">
<td v-for="browser in d.browsers">
<!--{{ d }}-->
<device v-for="variant in browser.displayVariants"
:variant="variant"
:browserId="browser.id"
:issueKey="issue.key"
:issueId="issue.id"
:deviceId="d.id"></device>
</td>
</template>
</tr>
and device template
<template>
<svg viewBox="0 0 30 30" class="mobileSVG" #click="changeState" :class="[state, {landscape: variant.landScope}]">
<use xlink:href="#mobile"/>
</svg>
</template>
I think adding keys to your list will solve the problem:
https://v2.vuejs.org/v2/guide/list.html#key
Vue tries to make minimum changes to the DOM, and think that the first item has not changed, so it is not re-rendered. In your case you already have the id, using that as key should solve the issue.
Vue cannot detect the following changes made to the array.
Here is the documentation.
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
vm refers to component instance.
To overcome the limitation 1 do:
Vue.set(items, indexOfItem, newValue)
For limitation 2:
items.splice(newLength)
So in your case you could do
this.$set(this.issues[0].devices[whateverIndex].browsers[anyIndex].displayVariants, indexOfVariant, valueOfVariant)

How do I pass the current item in a for loop to a method in vue.js 2?

I am building (as an exercise) a shopping cart in vue.js 2. I have my shop items and order items stored in my vue data array and a button rendered in a for loop for each shop item to add the item to the order (ex. push).
Here is the section of code that houses my list of items from my shop array in my vue data:
<fieldset>
<legend>Shop</legend>
<section v-if="shop">
<article v-for="(item, index) in shop">
<header><h1>{{ item.title }}</h1></header>
<p>{{ item.description }}</p>
<footer>
<ul>
<li>${{item.price}}</li>
<!-- find a way to set input name -->
<li><label>Quantity <input type="number" name=""></label></li>
<li><button v-on:click="addItemToOrder($event)">Add to Order</button></li>
</ul>
</footer>
</article>
</section>
<p v-else>No Items to Display</p>
</fieldset>
here is my vue element:
new Vue({
el: '#my-order',
data:{
'shop':[
{
'title':'Website Content',
'description':"Order your Website content by the page. Our search-engine-optimized web content puts you ahead of the competition. 250 words.",
'price':25,
'sku':'web001'
},
{
'title':'Blog Post',
'description':"We write blog posts that position your website as a go-to resource for expert knowlegde.",
'price':50,
'sku':'blog001'
},
{
'title':'Twitter Post'
},
{
'title':'Product Description'
}
],
'customizations':null,
'order':{
'items':null,
'total':null
},
'customer':null,
'payment':null
},
methods:{
addItemToOrder: function(){
/* Here is where I need to append the item to the cart */
}
}
})
How do I pass the item in the for loop to the order (eg: append it to order.items)?
You just need to pass the item in as a parameter to the function.
v-on:click="addItemToOrder(item)"
Then you can use it your Vue component
addItemToOrder: function(item){
this.order.items.push(item);
}
Make sure you initialize order.items to an empty array inside your components data so that you can push to it.
'order':{
'items': [],
'total': 0
},
In general, it is a good idea to initialize your data to the correct data type if you know what it will be.
I realise this is a bit late however in case anyone else happens across this thread...
You need to pass in the event as well as the item
in your vue code
someMethod : function(e, item){}
in your html
<a v-on:click="someMethod($event, $data)"></a>

Update unrelated field when clicking Angular checkbox

I have a list of checkboxes for people, and I need to trigger an event that will display information about each person selected in another area of the view. I am getting the event to run in my controller and updating the array of staff information. However, the view is not updated with this information. I think this is probably some kind of scope issue, but cannot find anything that works. I have tried adding a $watch, my code seems to think that is already running. I have also tried adding a directive, but nothing in there seems to make this work any better. I am very, very new to Angular and do not know where to look for help on this.
My view includes the following:
<div data-ng-controller="staffController as staffCtrl" id="providerList" class="scrollDiv">
<fieldset>
<p data-ng-repeat="person in staffCtrl.persons">
<input type="checkbox" name="selectedPersons" value="{{ physician.StaffNumber }}" data-ng-model="person.isSelected"
data-ng-checked="isSelected(person.StaffNumber)" data-ng-change="staffCtrl.toggleSelection(person.StaffNumber)" />
{{ person.LastName }}, {{ person.FirstName }}<br />
</p>
</fieldset>
</div>
<div data-ng-controller="staffController as staffCtrl">
# of items: <span data-ng-bind="staffCtrl.infoList.length"></span>
<ul>
<li data-ng-repeat="info in staffCtrl.infoList">
<span data-ng-bind="info.staffInfoItem1"></span>
</li>
</ul>
</div>
My controller includes the following:
function getStaffInfo(staffId, date) {
staffService.getStaffInfoById(staffId)
.then(success)
.catch(failed);
function success(data) {
if (!self.infoList.length > 0) {
self.infoList = [];
}
var staffItems = { staffId: staffNumber, info: data };
self.infoList.push(staffItems);
}
function failed(err) {
self.errorMessage = err;
}
}
self.toggleSelection = function toggleSelection(staffId) {
var idx = self.selectedStaff.indexOf(staffId);
// is currently selected
if (idx >= 0) {
self.selectedStaff.splice(idx, 1);
removeInfoForStaff(staffId);
} else {
self.selectedStaff.push(staffId);
getStaffInfo(staffId);
}
};
Thanks in advance!!
In the code you posted, there are two main problems. One in the template, and one in the controller logic.
Your template is the following :
<div data-ng-controller="staffController as staffCtrl" id="providerList" class="scrollDiv">
<!-- ngRepeat where you select the persons -->
</div>
<div data-ng-controller="staffController as staffCtrl">
<!-- ngRepeat where you show persons info -->
</div>
Here, you declared twice the controller, therefore, you have two instances of it. When you select the persons, you are storing the info in the data structures of the first instance. But the part of the view that displays the infos is working with other instances of the data structures, that are undefined or empty. The controller should be declared on a parent element of the two divs.
The second mistake is the following :
if (!self.infoList.length > 0) {
self.infoList = [];
}
You probably meant :
if (!self.infoList) {
self.infoList = [];
}
which could be rewrited as :
self.infoList = self.infoList || [];

Conditionally bind data in AngularJS

I have an array of tasks. They have titles and and labels.
function Task(taskTitle, taskType) {
this.title = taskTitle;
this.type = taskType;
}
$scope.tasks = [];
I end up declaring a bunch of tasks with different types and adding them to the array
In my html, I show a column of cards, filtered by type of task:
<div ng-model="tasks">
<div class="card" ng-repeat="abc in tasks track by $index" ng-show="abc.type==0">
<p> {{ abc.title }} </p>
</div>
</div>
I want to bind the first card displayed in this filtered view to some other div. I'll be processing an inbox, so I'll whittle this list of cards down to zero. Each time I 'process' a card and remove it from the list, I need the data to refresh.
<div ng-model="firstCardInFilteredArray">
<h4>Title of first card:</h4>
<p> This should be the title of the first card! </p>
</div>
My intuition was to do something like this (in javascript):
// pseudo-code!
$scope.inboxTasks = [];
for (i=0; i<tasks.length(); i++) {
if (tasks[i].type == 0) {
inboxTasks.append(tasks[i]);
}
}
and somehow run that function again any time the page changes. But that seems ridiculous, and not within the spirit of Angular.
Is there a simple way in pure javascript or with Angular that I can accomplish this conditional binding?
You can filter your ng-repeat: https://docs.angularjs.org/api/ng/filter/filter
<div ng-model="tasks">
<div class="card" ng-repeat="abc in filteredData = (tasks | filter: {type==0}) track by $index">
<p> {{ abc.title }} </p>
</div>
</div>
Additionally, by saving the filtered data in a separate list you can display the next task like this:
<div>
<h4>Title of first card:</h4>
<p> filteredData[0].title </p>
</div>
Your data will automatically update as you "process" tasks.
The other answers helped point me in the right direction, but here's how I got it to work:
HTML
<input ng-model="inboxEditTitle" />
JS
$scope.filteredArray = [];
$scope.$watch('tasks',function(){
$scope.filteredArray = filterFilter($scope.tasks, {type:0});
$scope.inboxEditTitle = $scope.filteredArray[0].title;
},true); // the 'true' keyword is the kicker
Setting the third argument of $watch to true means that any changes to any data in my tasks array triggers the watch function. This is what's known as an equality watch, which is apparently more computationally intensive, but is what I need.
This SO question and answer has helpful commentary on a similar problem, and a great fiddle as well.
More on different $watch functionality in Angular
To update inboxTasks, you could use $watchCollection:
$scope.inboxTasks = [];
$scope.$watchCollection('tasks', function(newTasks, oldTasks)
{
for (i=0; i<newTasks.length(); i++)
{
if(newTasks[i].type == 0)
{
$scope.inboxTasks.append(tasks[i]);
}
}
});

Categories