VUE watch triggered infinite loop - javascript

I'm new to using VUE.Js, and i created a very simple app to try out how it works.
The problem happens immediately where when i run the app, the watch for a variable is triggered in an infinite loop. I cannot figure out why. There is a v-for loop but that is on an array that only has two elements.
Initially the SubTotal should be 0. But as soon as the app is run, it triggers the Buy method, even though i haven't clicked the buy button and the sub total ends up being 442.37999999999965.
Thanks for any help.
Here is the jsfiddle Beer shopping cart
HTML :
<div id = "growler">
<table>
<tr>
<th style='width:150px'>Beer</th>
<th style='width:50px'>Price</th>
<th style='width:30px'></th>
</tr>
<tr v-for = "beer in beers">
<td>{{ beer.name }}</td>
<td>{{ beer.price }}</td>
<td>
<button :click="buy(beer)">buy</button>
</td>
</tr>
<tr>
<td>SubTotal</td>
<td>{{subTotal}}</td>
<td></td>
</tr>
</table>
</div>
JS:
new Vue({
el: "#growler",
data: {
beers: [
{name: 'Ahool Ale', price: 2.00},
{name: 'Agogwe Ale', price: 2.38}
],
shoppingCart: [],
subTotal: 0.00
},
watch: {
shoppingCart: function() {
console.log('shopping cart watch triggered');
this.updateSubTotal();
}
},
methods: {
updateSubTotal: function () {
var s=this.shoppingCart.length;
var t=0;
for (var i=0;i<s; i++){
t += this.shoppingCart[i].price;
}
this.subTotal = t;
},
buy: function (beer) {
console.log('beer pushed on array');
this.shoppingCart.push(beer);
}
},
beforeCreate: function() {
console.log('beforeCreate');
},
created: function() {
console.log('created');
},
beforeMount: function() {
console.log('beforeMount');
},
mounted: function() {
console.log('mounted');
},
beforeUpdate: function() {
console.log('beforeUpdate');
},
updated: function() {
console.log('updated');
},
beforeDestroy: function() {
console.log('beforeDestroy');
},
destroyed: function() {
console.log('afterDestroy');
}
});

I found your mistake:
<button :click="buy(beer)">buy</button>
You used :(v-bind) instead of #(v-on:) on the click handler.
When you first bind it, the function is called once and updates the shoppingCart. This will update the subTotal data, which will force a re-render of the DOM, which will trigger the buy function again because of the :bind.
Fix:
<button #click="buy(beer)">buy</button>
<!-- or -->
<button v-on:click="buy(beer)">buy</button>
Suggested changes for your code:
Use computed properties instead of a method to update a property that represents a sum of other values:
new Vue({
el: "#growler",
data: {
beers: [{
name: 'Ahool Ale',
price: 2.00
},
{
name: 'Agogwe Ale',
price: 2.38
}
],
shoppingCart: []
},
watch: {
shoppingCart: function() {
console.log('shopping cart watch triggered');
}
},
computed: {
subTotal: function() {
return this.shoppingCart.reduce(function(total, beer) {
return total + beer.price;
}, 0);
}
}
},
methods: {
buy: function(beer) {
this.shoppingCart.push(beer);
}
},
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
<div id="growler">
<button>buy</button>
<table>
<tr>
<th style='width:150px'>Beer</th>
<th style='width:50px'>Price</th>
<th style='width:30px'></th>
</tr>
<tr v-for="beer in beers">
<td>{{ beer.name }}</td>
<td>{{ beer.price }}</td>
<td>
<button #click="buy(beer)">buy</button>
</td>
</tr>
<tr>
<td>SubTotal</td>
<td>{{subTotal}}</td>
<td></td>
</tr>
</table>
</div>

Related

ASP.NET MVC Knockout button click call POST API

I have a table (list) on my front end in html, and foreach row, I have a button:
...
<td>
<button data-bind="click: sendDataToApi">Test button</button>
</td>
...
and in the .js file I have something like this:
define(['viewmodels/shell', 'durandal/services/logger', 'plugins/dialog', 'viewmodels/shell', 'toastr', 'knockout', 'kovalidationconfig', 'plugins/router', 'typeahead.bundle'],
function (shell, logger, dialog, shell, toastr, ko, kvc, router, typeahead) {
var vm = {
activate: activate,
shell: shell,
data: ko.observableArray([]),
close: function () {
$(window).off('popstate', vm.goBack);
$(window).off('resize', adjustModalPosition);
dialog.close(vm, 'cancel');
},
goBack: function () {
$(window).off('popstate', vm.goBack);
$(window).off('resize', adjustModalPosition);
dialog.close(vm, 'back');
},
editPreregisteredChildren: function () {
router.navigate("#/function/" + this.id);
},
currentPage: ko.observable(1),
itemsPerPage: ko.observable(10),
hasNextPage: ko.observable(false),
previousPage: previousPage,
nextPage: nextPage,
sendDataToApi: function () {console.log("sdsdsds")},
searchCriteria: ko.observable(''),
applySearch: applySearch,
locations: ko.observableArray([]),
locationId: ko.observable(),
LocationName: ko.observable(),
exportHref: ko.observable("/spa/ExportSchedulings"),
bindingComplete: function (view) {
bindFindLocationEvent(view);
}
};
function sendDataToApi() {
console.log("hello.")
};
});
so, firstly, I want to get console.log("something") to work.
for now Im getting error in my console in chrome:
Uncaught ReferenceError: Unable to process binding "click: function(){return sendDataToApi }"
Message: sendDataToApi is not defined
I dont get it why?
after that I need to do an ajax call to my controller, and the end to call some api in that controller, and return the information if the api call was successfull or not.
I am going to assume that you are trying to display information in a table given the
<td>
<button data-bind="click: sendDataToApi">Test button</button>
</td>
I am also going to assume that that there is a ko:foreach at the table or table-body level. if that is the case then sendDataToApi is associated with the parent vm object and not the object that is currently being used to create the table rows.
If that is the case then you would need to use the $parent.sendDataToApi or $root.sendDataToApi
<td>
<button data-bind="click: $parent.sendDataToApi">Test button</button>
</td>
or
<td>
<button data-bind="click: $root.sendDataToApi">Test button</button>
</td>
EDIT
you just need to add a parameter to the receiving function because the knockout passes the current object.
var serverData = [{
id: 1,
name: 'Test 1'
},
{
id: 2,
name: 'Test 2'
},
{
id: 3,
name: 'Test 3'
},
];
function ViewModel() {
var self = this;
self.data = ko.observableArray([]);
self.checkServer = function checkServer(row) {
console.log(ko.toJS(row));
}
self.fillTable = function fillTable() {
var mappedData = serverData.map(r => new RowViewModel(r));
self.data(mappedData);
}
}
function RowViewModel(data) {
var self = this;
self.id = ko.observable(data.id || 0);
self.name = ko.observable(data.name || '');
}
ko.applyBindings(new ViewModel());
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button class="button" data-bind="click: fillTable">Fill Table</button>
<table class="table">
<tbody data-bind="foreach: data">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: name"></td>
<td>
<button data-bind="click: $parent.checkServer">Check Server</button>
</td>
</tr>
</tbody>
</table>

Highlight newly inserted row in Vue.js

I am fetching the JSON data(Orders) from REST API and displaying in a dynamic HTML table using Vue js. I have a "Print" button for each row in the table. The purpose of the button is printing the data of the row in a structure, basically a bill.
For that, I want to highlight the newly added row until the Print button is clicked by the user. How do I achieve this?
I'm refreshing the table every minute.
This is my code.
<tr v-for="orders, index in orders">
<th scope="row">{{ index + 1 }}</th>
<td>{{ orders.id }}</td>
<td>{{ orders.billing.first_name + " " +orders.billing.last_name }}</td>
<td>{{ orders.date_created }}</td>
<td>{{ orders.billing.phone}}</td>
<td>{{ orders.billing.address_1 + ", " + orders.billing.address_2 + ", " + orders.billing.city + orders.billing.postcode }}</td>
<td>{{ orders.line_items.name}}</td>
<td>{{ orders.total}}</td>
<td><button class="btn btn-primary" (click)="printBill(data)">Print</button></td>
</tr>
</tbody>
</table>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
orders: []
},
mounted: function() {
axios.get('https://localhost/Site/wp-json/wc/v3/orders?consumer_key=KEY&consumer_secret=KEY1')
.then(response => {
this.orders = response.data;
console.log(response);
})
.catch(error => {
console.log(error);
});
},
})
</script>
I wrote a small example, have a look:
<template>
<div id="app">*
<tr
v-for="(order, index) in orders"
:key="index"
:class="{highlight: orders[index].isPrinted === undefined}"
>
<th scope="row">{{ index + 1 }}</th>
<td>{{ order.name }}</td>
<td>{{ order.something}}</td>
<td>
<button class="btn btn-primary" #click="printBill(index)">Print</button>
</td>
</tr>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
orders: []
};
},
methods: {
printBill(index) {
//code to print the bill
//change flag
this.$set(this.orders[index], "isPrinted", true);
}
},
mounted() {
//axios request - data sample
this.orders = [
{
name: "first",
something: "whatever"
},
{
name: "second",
something: "whatever"
},
{
name: "third",
something: "whatever"
},
{
name: "fourth",
something: "whatever"
},
{
name: "fifth",
something: "whatever"
}
];
}
};
</script>
<style>
.highlight {
background-color: blue;
color: white;
}
th {
width: 20%;
}
td {
width: 20%;
}
</style>
You can run it here.
As you can see that I am adding a flag to elements in orders array whenever printBill method runs.
By tracking newly added property we can conditionally display highlight class.
Add an isPrinted flag to each row of data, making sure you retain this if rows had been previously flagged. Also, call the API every minute.
mounted: function() {
// Call the API the first time
this.refreshData()
// Then call the API every minute
this.setIntervalId = setInterval(this.refreshData, 60000)
},
beforeDestroy: function() {
// Stop refreshing data after the component is destroyed!
clearInterval(this.setIntervalId)
}
methods: {
// Extract refresh logic into a method
refreshData () {
axios.get('https://localhost/Site/wp-json/wc/v3/orders?consumer_key=KEY&consumer_secret=KEY1')
.then(response => {
// Note the orders we previously flagged as printed, so we can reapply the flag after refreshing
const previouslyFlaggedIds = this.orders.filter(x => x.is_printed).map(x => x.id);
this.orders = response.data.map(x => ({...x, is_printed: previouslyFlaggedIds.find(y => y === x.id) != null}));
})
.catch(error => {
console.log(error);
});
}
}
Use this to style the rows
<tr
v-for="(order, index) in orders"
:key="order.id"
:class="{highlight: !order.is_printed}"
>
Set is_printed when rows are printed.
<td><button class="btn btn-primary" #click="printBill(order)">Print</button></td>
methods: {
printBill(order) {
order.is_printed = true
}
}

Rendering a table with dynamic headers

I have to render a table with dynamic headers, I mean, I don't want to do something like this in the HTML
<table>
<tr>
// THIS TABLE ROW IS WHAT I SAY
<th>here info</th>
<th>something here</th>
<th>another header</th>
</tr>
<tr ng-repeat="thing in things">
<td>{{thing.asfs}}</td>
<td>{{thing.asx}}</td>
<td>{{person.dsf}}</td>
</tr>
</table>
I want something like this
<table>
<tr ng-repeat="head in heads">
{{head}}
</tr>
<tr ng-repeat="bar in bars">
<td ng-repeat="foo in foos"></td>
</tr>
</table>
that is only an example, I need to do it with this data:
{
"55f6de98f0a50c25f7be4db0":{
"clicks":{
"total":144,
"real":1
},
"conversions":{
"total":4,
"amount":229
},
"cost":{
"cpc":0.1999999999999995,
"ecpc":1145.0000000000027,
"total":28.79999999999993
},
"revenue":{
"total":4,
"epc":0.027777777777777776
},
"net":{
"roi":-1.1612903225806457,
"total":4
},
"name":"Traffic Source #2",
},
"55f6de98f0a50c25f7be4dbOTHER":{
"clicks":{
"total":144,
"real":1
},
"conversions":{
"total":4,
"amount":229
},
"cost":{
"cpc":0.1999999999999995,
"ecpc":1145.0000000000027,
"total":28.79999999999993
},
"revenue":{
"total":4,
"epc":0.027777777777777776
},
"net":{
"roi":-1.1612903225806457,
"total":4
}
"name":"Traffic Source #3"
},
}
every key, like clicks, conversions, cost, etc, should be a td, it is just that I don't want static HTML.
Any suggestions?
EDIT
And also, sometimes that object will grow, could come up with some more keys like this one 55f6de98f0a50c25f7be4db0
I did this fiddle with the exact same data I am receiving
http://jsfiddle.net/wLkz45qj/
UPDATE:
What you need to do is first convert you inconvenient object to array of objects with simple structure, and then use my code , i.e.
{
a: {
b:{
c: 'x'
}
}
}
will turn into
[[ a, { 'b.c' : 'x' }], ...]
or just
[{ _id : a , 'b.c' :'x'}, ...]
easiest way to do that is to use lodash or underscore ( check map, flatMap, pairs etc)
#jperezov showed you core idea, little bit detailed example:
$scope.peopleKeys = Object.keys(people[0])
and
<table>
<tr>
<th></th>
<th ng-repeat="personKey in peopleKeys">
{{ personKey }}
</th>
</tr>
<tr ng-repeat='p in people'>
<th>{{ $index }}</th>
<td ng-repeat="personKey in peopleKeys">
{{ p[personKey] }}
</td>
</tr>
</table>
You may also have some dictionary with display names:
$scope.displayNames = {
id: 'ID',
firstName: 'First Name'
...
}
and then your header going to be:
<tr>
<th></th>
<th ng-repeat="personKey in peopleKeys">
{{ displayNames[personKey] }}
</th>
</tr>
PS: OR you can just use ui-grid
var app = angular.module('myApp', []);
function PeopleCtrl($scope, $http) {
$scope.headers=[];
$scope.data = [];
$scope.LoadMyJson = function() {
for (var s in myJson){
$scope.data.push(s);
if ($scope.headers.length < 1)
for (var prop in myJson[s]){
prop.data = [];
$scope.headers.push({th:prop, td: []});
}
}
for (var s in $scope.data){
for (var prop in $scope.headers){
var header = $scope.headers[prop].th;
var data = myJson[$scope.data[s]][header];
$scope.headers[prop].td.push(data);
}
}
};
}
What you're looking for is something like this, I think:
http://jsfiddle.net/wLkz45qj/8/
Maybe iterate another time over "inner" for formatting.

How to target the last column in the table body to replace its value with a fixed value for every row?

Using Vue, I have displayed table with dynamic data pulled from external JSON.
I want to target the last column in the table body to replace its value with a fixed value for every row.
How would I do this?
Note that my script uses the initial value from the JSON data for that column to determine which class to put on that td.
Here is my code:
var dataURL = 'inc/data.json.php'
Vue.component('demo-grid', {
template: '#grid-template',
replace: true,
props: ['data', 'columns', 'filter-key'],
data: function() {
return {
data: null,
columns: null,
sortKey: '',
filterKey: '',
reversed: {}
}
},
compiled: function() {
// initialize reverse state
var self = this
this.columns.forEach(function(key) {
self.reversed.$add(key, false)
})
},
methods: {
sortBy: function(key) {
this.sortKey = key
this.reversed[key] = !this.reversed[key]
}
}
})
var demo = new Vue({
el: '#app',
data: {
searchQuery: '',
gridColumns: [...],
gridData: []
},
ready: function() {
this.fetchData()
},
methods: {
fetchData: function() {
var xhr = new XMLHttpRequest(),
self = this
xhr.open('GET', programsURL)
xhr.onload = function() {
self.gridData = JSON.parse(xhr.responseText)
}
xhr.send()
}
}
})
<table>
<thead>
<tr>
<th v-repeat="key: columns" v-on="click:sortBy(key)" v-class="active: sortKey == key">
{{key | capitalize}}
<span class="arrow" v-class="reversed[key] ? 'dsc' : 'asc'">
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-repeat="
entry: data
| filterBy filterKey
| orderBy sortKey reversed[sortKey]">
<!-- here is where I wish to target the 5th in this row to change its value -->
<td v-repeat="key: columns" v-class="lvl-1 : entry[key] === '1', lvl-2 : entry[key] === '2', lvl-3 : entry[key] === '3'>
{{entry[key]}}
</td>
</tr>
</tbody>
</table>
Compare the special $index property with the length of the array (or computed property), and then use a template fragment so you can switch out the <td>
<template v-repeat="column in columns">
<td v-show="$index < columns.length-1">All other columns...</td>
<td v-show="$index === columns.length-1">Last Column</td>
</template>
Solved it with:
<div v-if="$index === 4">
...

Ember.js how to observe array keys changed by input

I have an object with a couple dozens of settings, some settings depend on other settings, so, I need to observe if some setting changed.
import Ember from 'ember';
export default Ember.Controller.extend({
allPermissionChanged: function () {
alert('!');
}.observes('hash.types.[].permissions'),
permissionsHash: {
orders:{
types: [
{
label: 'All',
permissions: {
view: true,
edit: false,
assign: false,
"delete": false,
create: true
}
},
}
],
permissions:[
{
label:'Просмотр',
code:'view'
},
{
label:'Редактирование',
code:'edit'
},
{
label:'Распределение',
code:'assign'
},
{
label:'Удаление',
code:'delete'
},
{
label:'Создание',
code:'create'
}
]
}
}
});
Next I try to bind each setting to input
<table class="table table-bordered">
<thead>
<tr>
{{#each hash.types as |type|}}
<th colspan="2">{{type.label}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each hash.permissions as |perm|}}
<tr>
{{#each hash.types as |type|}}
{{#if (eq (mut (get type.permissions perm.code)) null)}}
<td> </td>
<td> </td>
{{else}}
<td>{{perm.label}}</td>
<td>{{input type="checkbox" checked=(mut (get type.permissions perm.code)) }}</td>
{{/if}}
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
But observer doesn't work.
Also I prepared Jsbin example - http://emberjs.jsbin.com/havaji/1/edit?html,js,output
You are using the wrong syntax for that. hash.types.[] should only be used if you want to observe an actual array, when something is added or removed from it. To observe a property in an array you you hash.types.#each.permissions.
allPermissionChanged: function () {
alert('!');
}.observes('hash.types.#each.permissions')
You can read more about it in the Ember Guides.
You could change booleans to objects with boolean property so you could properly observe value of checkbox.
Controller:
App.IndexController = Ember.Controller.extend({
testData: Ember.ArrayProxy.create({
content: [
{ value: true },
{ value: false },
{ value: true }
]
}),
//...
Template:
{{input type='checkbox' checked=data.value}}
Observer:
arrayChanged: Ember.observer('testData.#each.value', function () {
console.log('changed');
})
Working demo.

Categories

Resources