Knockout JS setting optionsValue destroys my code - javascript

The code below is simplified, see the fiddle: http://jsfiddle.net/QTUqD/7/
Basically I'm setting the device name under the data-bind, but I also need to specify the optionsValue for sending off to the database, but when I set it, the display data-bind is blank.
<script id="extItems" type="text/html">
<tr>
<td data-bind="text: device() && device().name"></td>
</tr>
</script>
<script id="editExts" type="text/html">
<tr>
<td>
<select data-bind="options: $root.devicesForItem($data), optionsText: 'name', value: device, optionsValue: 'id'"></select>
</td>
</tr>
</script>
window.ExtListViewModel = new function () {
var self = this;
window.viewModel = self;
self.list = ko.observableArray();
self.pageSize = ko.observable(10);
self.pageIndex = ko.observable(0);
self.selectedItem = ko.observable();
self.extQty = ko.observable();
self.devices = ko.observableArray();
self.addressList = ko.observableArray(['addressList']);
self.availableDevices = ko.computed(function() {
var usedQuantities = {}; // for each device id, store the used quantity
self.list().forEach(function(item) {
var device = item.device();
if (device) {
usedQuantities[device.id] = 1 + (usedQuantities[device.id] || 0);
}
});
return self.devices().filter(function(device) {
var usedQuantity = usedQuantities[device.id] || 0;
return device.qty > usedQuantity;
});
});
// need this to add back item's selected device to its device-options,
// and to maintain original order of devices
self.devicesForItem = function(item) {
var availableDevices = self.availableDevices();
return self.devices().filter(function(device) {
return device === item.device() || availableDevices.indexOf(device) !== -1;
});
}
self.edit = function (item) {
if($('#extMngForm').valid()) {
self.selectedItem(item);
}
};
self.cancel = function () {
self.selectedItem(null);
};
self.add = function () {
if($('#extMngForm').valid()) {
var newItem = new Extension();
self.list.push(newItem);
self.selectedItem(newItem);
self.moveToPage(self.maxPageIndex());
}
};
self.remove = function (item) {
if (confirm('Are you sure you wish to delete this item?')) {
self.list.remove(item);
if (self.pageIndex() > self.maxPageIndex()) {
self.moveToPage(self.maxPageIndex());
}
}
$('.error').hide();
};
self.save = function () {
if($('#extMngForm').valid()) {
self.selectedItem(null);
};
};
self.templateToUse = function (item) {
return self.selectedItem() === item ? 'editExts' : 'extItems';
};
self.pagedList = ko.dependentObservable(function () {
var size = self.pageSize();
var start = self.pageIndex() * size;
return self.list.slice(start, start + size);
});
self.maxPageIndex = ko.dependentObservable(function () {
return Math.ceil(self.list().length / self.pageSize()) - 1;
});
self.previousPage = function () {
if (self.pageIndex() > 0) {
self.pageIndex(self.pageIndex() - 1);
}
};
self.nextPage = function () {
if (self.pageIndex() < self.maxPageIndex()) {
self.pageIndex(self.pageIndex() + 1);
}
};
self.allPages = ko.dependentObservable(function () {
var pages = [];
for (i = 0; i <= self.maxPageIndex() ; i++) {
pages.push({ pageNumber: (i + 1) });
}
return pages;
});
self.moveToPage = function (index) {
self.pageIndex(index);
};
};
ko.applyBindings(ExtListViewModel, document.getElementById('extMngForm'));
function Extension(extension, name, email, vmpin, device, macAddress, shipTo){
this.extension = ko.observable(extension);
this.name = ko.observable(name);
this.email = ko.observable(email);
this.vmpin = ko.observable(vmpin);
this.device = ko.observable(device);
this.macAddress = ko.observable(macAddress);
this.shipTo = ko.observable(shipTo);
}

When you use optionsValue, KO writes the property value to whatever you have bound against value. So, it would now populate value with the id rather than the object.
There are a couple of ways to tackle this scenario where you want both the value (for sending to the DB) and the object (for binding other parts of the UI against).
A pretty typical solution is to create a computed observable on your object that takes the currently selected object and returns the id.
So, in your Extension you would do something like:
this.device = ko.computed({
read: function() {
var device = this.device.asObject();
return device && device.id;
},
deferEvaluation: true, //deferring evaluation, as device.asObject has not been created yet
}, this);
//create this as a sub-observable, so it just disappears when we turn this into JSON and we are just left with the id to send to the DB
this.device.asObject = ko.observable(device);
Then remove the optionsValue and bind value against device.asObject
In this case, I added the asObject sub-observable, so it will just drop off when you turn this into JSON (ko.toJSON) to send to the server. The only tricky part about this technique is that if you are loading existing data from the server, then you would need to populate asObject with the appropriate choice from your options.
Here is a sample: http://jsfiddle.net/rniemeyer/Q3PEv/
Another option that I have used is to continue to use optionsValue, but then to create a custom binding that tracks the object in a separate observable. Here is a custom binding that creates an asObject sub-observable for whatever is bound against value. This way you really don't need to mess with it at all in your view model.
//when using optionsValue, still track the select object in a different observable
ko.bindingHandlers.valueAsObject = {
init: function(element, valueAccessor, allBindingsAccessor) {
var value = allBindingsAccessor().value,
prop = valueAccessor() || 'asObject';
//add an "asObject" sub-observable to the observable bound against "value"
if (ko.isObservable(value) && !value[prop]) {
value[prop] = ko.observable();
}
},
//whenever the value or options are updated, populated the "asObject" observable
update: function(element, valueAccessor, allBindingsAccessor) {
var prop = valueAccessor(),
all = allBindingsAccessor(),
options = ko.utils.unwrapObservable(all.options),
value = all.value,
key = ko.utils.unwrapObservable(value),
keyProp = all.optionsValue,
//loop through the options, find a match based on the current "value"
match = ko.utils.arrayFirst(options, function(option) {
return option[keyProp] === key;
});
//set the "asObject" observable to our match
value[prop](match);
}
};
Sample here: http://jsfiddle.net/rniemeyer/E2kvM/

Related

Modify payment lines pos odoo 8

someone have any idea how i should modify the payment-lines in the POS,I want to add a type of credit card(like a many2one, I did it) but every time I add a line my option change to the first and also when the order is finished not save the value in pos.order -> statement_id.
enter image description here
here is my code:
function POS_CashRegister (instance, local) {
var pos = instance.point_of_sale;
var _t = instance.web._t;
var QWeb = instance.web.qweb;
var round_pr = instance.web.round_precision
const ParentOrder = pos.Order;
pos.PosModel.prototype.models.push({ //loaded model
model: 'pos.credit.card',
fields: ['id', 'name'],
domain: [['pos_active','=',true]],
loaded: function(self,credit_cards){ //pass parameters
self.credit_cards = credit_cards;
},
});
pos.PaymentScreenWidget = pos.PaymentScreenWidget.extend({
validate_order: function(options) {
var self = this;
var currentOrder = self.pos.get('selectedOrder');
var plines = currentOrder.get('paymentLines').models;
for (var i = 0; i < plines.length; i++) {
if(plines[i].cashregister.journal_id[1] === 'Tarjeta de Credito (PEN)')
{
var value = plines[i].node.firstElementChild.nextElementSibling.nextElementSibling.firstElementChild.value;
plines[i].set_credit_card(parseInt(value));
//console.log(plines[i].node.firstElementChild.nextElementSibling.nextElementSibling.firstElementChild.value);
//plines[i].node
}
}
console.log(currentOrder);
self._super(options);
},
render_paymentline: function (line) {
var self = this;
if(line.cashregister.journal_id[1] !== 'Tarjeta de Credito (PEN)'){
if (line.cashregister.currency[1] !== 'USD') {
return this._super(line);
} else {
var el_html = openerp.qweb.render('Paymentline', {widget: this, line: line});
el_html = _.str.trim(el_html);
var el_node = document.createElement('tbody');
el_node.innerHTML = el_html;
el_node = el_node.childNodes[0];
el_node.line = line;
el_node.querySelector('.paymentline-delete')
.addEventListener('click', this.line_delete_handler);
el_node.addEventListener('click', this.line_click_handler);
var sourceInput = el_node.querySelector('.source-input');
var convertedInput = el_node.querySelector('.converted-input');
sourceInput.addEventListener('keyup', function (event) {
el_node.line.set_usd_amount(event.target.value);
convertedInput.value = el_node.line.get_amount_str();
});
line.node = el_node;
return el_node;
}
}else {
return this._super(line);
}
},
});
pos.Paymentline = pos.Paymentline.extend({
initialize: function(attributes, options) {
this.amount = 0;
this.cashregister = options.cashregister;
this.name = this.cashregister.journal_id[1];
this.selected = false;
this.credit_card = false;
this.pos = options.pos;
},
set_credit_card: function(value){
this.credit_card = value;
this.trigger('change:credit_card',this);
},
get_credit_card: function(){
return this.credit_card;
},
export_as_JSON: function(){
return {
name: instance.web.datetime_to_str(new Date()),
statement_id: this.cashregister.id,
account_id: this.cashregister.account_id[0],
journal_id: this.cashregister.journal_id[0],
amount: this.get_amount(),
credit_card_id: this.get_credit_card(),
};
},
});
}
any suggestions?
You can create 2 journals here too. One for visa and another for master If you don't want that drop down there. Another way is you have to store selected option in a variable and then print that variable in front.
To store selected option initially assigned ids to each values of option and after then while validating order you can get that id of that field and from that id you can get your value. By this way also you can do that.

Applying filter to a observableArray?

I'm working on a project where I have a search field that filters an observableArray based on what is typed in the search box.
Here is my html code:
<input class="form-control" placeholder="Search for a burger joint around downtown Indianapolis, IN" data-bind="value: searchInput, valueUpdate: 'afterkeydown', event: { keyup: filterResults }">
Here is my js code:
var BurgerJoint = function(data) {
this.id = ko.observable(data.id);
this.name = ko.observable(data.name);
this.long = ko.observable(data.long);
this.lat = ko.observable(data.lat);
this.comments = ko.observable(data.comments);
}
var ViewModel = function() {
var self = this;
self.searchInput = ko.observable('');
this.burgerList = ko.observableArray([]);
initialBurgerJoints.forEach(function(burgerItem){
self.burgerList.push( new BurgerJoint(burgerItem) );
addmarker(burgerItem.lat, burgerItem.long, burgerItem.id, burgerItem.name, burgerItem.comments);
});
self.burgerList.sort(function (l, r) { return l.name() > r.name() ? 1 : -1 });
self.currentFilter = ko.observable();
self.filterResults = function(){
var value = self.searchInput().toLowerCase();
if(value != ''){
self.burgerList(self.burgerList.filter(function(data){
var startsWith = data.name.toLowerCase().startsWith(value);
return burgerList;
}));
}
return true;
};
self.showmap = function(data) {
viewmarker(data.id());
};
}
ko.applyBindings(new ViewModel());
I'm having issues with the filterResults function. Any suggestions?
Maybe you shouldn't set your original burgerList in filter function. Make a copy/clone of the original first. Try something like
self.filterResults = function(){
var value = self.searchInput().toLowerCase();
if(value != ''){
self.burgerList(self.burgerList().filter(function(data){
return data.name.toLowerCase().startsWith(value);
}));
}else{
// reset with the original burgerList contents here
self.burgerList(self.burgerListClone());
}
};

How to iterate anonymous function inside each function in Knockout viewmodel

I am building a Knockout viewmodel. The model has some fields like dateFrom, DateTo, Status and so forth. In addition, there is a list of invoices.
The invoices have some pricing information, which is a price object. My main object also have a price object, which should iterate all the invoice objects and find the total price.
My problem is the following:
The code runs smooth, until I add the following in my view:
<label data-bind="text:totalPrice().price().priceExVat"></label>
Here I get an:
TypeError: $(...).price is not a function
Which refers to my:
exVat += $(ele).price().priceExVat;
I don't understand it, because in my each function, I should have the element. The element have a price() function, so why would it not work? Is it some scope issue?
My viewmodel:
function invoice(invoiceDate, customerName, pdfLink, status) {
var self = this;
self.pdfLink = pdfLink;
self.print = ko.observable(0);
self.customerName = customerName;
self.status = status;
self.pdfPagesCount = function () {
return 1;
};
self.invoiceDate = invoiceDate;
self.price = function () {
return new price(1.8, 2.1);
};
}
function price(exVat, total) {
var self = this;
self.currency = '€';
self.total = total;
self.priceExVat = exVat;
self.vatPercentage = 0.25;
self.vatAmount = self.exVat - self.total;
self.priceExVatText = function() {
return self.priceExVat + ' ' + self.currency;
};
}
var EconomicsViewModel = function (formSelector, data) {
var self = this;
self.dateFrom = data.dateFrom;
self.dateTo = data.dateTo;
self.invoices = ko.observableArray([
new invoice('05-05-2014', 'LetterAmazer IvS', "http://www.google.com","not printed"),
new invoice('05-05-2014', 'LetterAmazer IvS', "http://www.google.com", "not printed")
]);
self.totalPrice = function () {
var exVat = 0.0;
$(self.invoices).each(function (index, ele) {
console.log(ele);
exVat += $(ele).price().priceExVat;
});
return price(exVat, 0);
};
};
From what I read, totalPrice is actually a price object, you don't need to put a .price():
<label data-bind="text:totalPrice().priceExVat"></label>
EDIT:
Sorry, there were also problems on your javascript:
self.totalPrice = function () {
var exVat = 0.0;
$(self.invoices()).each(function (index, ele) { //<-- add () to self.invoices to get the array
console.log(ele);
exVat += ele.price().priceExVat; //<-- remove useless jQuery
});
return new price(exVat, 0); //<-- add 'new'
};
Check this fiddle
EDIT2:
To answer robert.westerlund's comment, you could remove $().each and replace with ko.utils.arrayForEach or even simpler use a for loop:
var arr = self.invoices();
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
exVat += arr[i].price().priceExVat;
}
Updated fiddle

Computed observable calculated from async data

In my view model, I have an observable array that needs to be populated from a $.getJSON call. I would like to have a computed observable to represent the total of a "price" column contained in the JSON returned.
I've managed to populate the observable array...
(function($){
function Coupon(expiration, value) {
var self = this;
self.expiration = expiration;
self.value = value;
}
$(function() {
$.when($.getJSON(coupons_url, null)).done(function(couponsJson) {
ko.applyBindings({
coupons: ko.utils.arrayMap(couponsJson[0].objects,
function(coupon) {
return new Coupon(coupon.expiration, coupon.value);
})
savingsAvailable: ko.computed(function() {
var total = 0;
for (var i = 0; i < this.coupons().length; i++) {
total += parseFloat(this.coupons()[i].value / 100);
}
return total.toFixed(2);
})
});
});
});
})(jQuery);
...but I'm not sure how to then access the value of coupons when I try to populate the computed observable. this.coupons() errors out: "this.coupons() is not a function". What do I need to do to accomplish this, and/or what am I doing wrong?
ko.computed() takes a second parameter that defines the value of "this" when evaluating the computed observable. So what ever object hold "coupons" you would want to pass that in as the second parameter.
Or you could try something like the following create a view model instead of defining the object on the fly and passing it as a parameter to applyBindings.
var Coupon = function(expiration, value) {
var self = this;
self.expiration = expiration;
self.value = value;
}
var viewModel = function(couponsJson){
var self = this;
self.coupons = ko.utils.arrayMap(couponsJson[0].objects, function(coupon) {
return new Coupon(coupon.expiration, coupon.value);
})
self.savingsAvailable = ko.computed(function() {
var total = 0;
for (var i = 0; i < self.coupons().length; i++) {
total += parseFloat(self.coupons()[i].value / 100);
}
return total.toFixed(2);
})
}
(function($){
$(function() {
$.when($.getJSON(coupons_url, null)).done(function(couponsJson) {
var vm = new viewModel(couponsJson)
ko.applyBindings(viewModel);
});
});
})(jQuery);

Losing object reference on lookup in Javascript

I'm working on an extension for Google Chrome and I ran into the following situation:
I'm trying to get all the existing tabs from all the opened windows in the same instance of Google Chrome. I manage to get them and construct an array of objects that contain the relevant data for me.
When I look at the constructed array using console.log (which is saved for future use also) I can see the collection of objects, but I can't reference them (when I try I get undefined).
I tried to save the array outside my object in a container, but nothing changes.
Any idea why the reference to the objects go away when I try to look them up? Thanks.
Here is the code:
(function(window){
//defining a namespace
var example = {
bmarksmaster: (function() {
var bmarksmaster = function() {
return new bmarksmaster.fn.init();
}
bmarksmaster.fn = bmarksmaster.prototype = {
debug: false,
tabs: [],
constructor: bmarksmaster,
init: function() {
return this;
},
windowParser: function(ctx, filter) {
var local = ctx;
var filter = filter;
return function(wObj) {
if((wObj !== null) && (wObj !== undefined)) {
for(var idx in wObj) {
var cw = wObj[idx];
if((cw.tabs !== null) && (cw.tabs !== undefined)) {
var cwtabs = cw.tabs;
for(var tabIdx in cwtabs) {
local.tabs.push(filter(tabIdx, cwtabs[tabIdx]));
}
}
}
}
};
},
getTabs: function() {
var returnData = [];
chrome.windows.getAll(
{
"populate": true
}, this.windowParser(this, function(i, e) {
var data = {};
if(!e.incognito) {
data["title"] = e.title;
data['url'] = e.url;
data['favicon'] = e.favIconUrl || "";
}
return data;
}));
return this.tabs;
},
getTab: function(callback) {
this.getTabs();
for (var tabIdx in this.tabs) {
if(callback(tabIdx, this.tabs[tabIdx])) {
return this.tabs[tabIdx];
}
}
},
getTabsData: function(callback) {
var data = [];
var tabs = [];
tabs = this.getTabs();
console.log(this.tabs[0]);
for (var tabIdx in tabs) {
console.log(tabs[tabIdx]);
var tabData = callback(tabIdx, tabs[tabIdx]);
if(tabData) {
data.push(tabData);
}
}
return data;
},
setDebug: function() {
this.debug = true;
},
resetDebug: function() {
this.debug = false;
}
};
bmarksmaster.fn.init.prototype = bmarksmaster.fn;
return bmarksmaster;
})()
};
window.example = example;
})(window);
//end of bmarksmaster.js file
console.log(example.bmarksmaster().getTabs()); //this works, I can see the array
console.log(example.bmarksmaster().getTabs()[0]); //this doesn't work, I get undefined, never mind the shortcut
I think the logic in your code is wrong. It is a bit convoluted and hard to follow. I would recommend rewriting it a bit to be simpler. Something like this might help get you started. It collects all the windows, putts all the tabs into the tabs var.
var tabs = [];
chrome.windows.getAll({ populate: true}, function(windows) {
var localTabs = windows.reduce(function(a, b){
return a.tabs.concat(b.tabs);
});
tabs = localTabs.filter(function(element){
return !element.incognito;
});
})

Categories

Resources