TL;DR: I need to set a default value for display on a select2 field, bound via knockout, but the select2 binding keeps overriding my viewmodel value to "" instead of accepting the value.
The Ingredients
I am utilizing the following:
- KnockoutJS
- Select2 on input fields
- Custom knockout binding to select2
- An ajax call to load an object (an invoice) as part of a Start() method on my viewmodel.
The Goal
- Load my values as part of the initial viewmodel's
Start()
function - Bind the select2 default values to the values of my VM at the time it loads the invoice.
- Allow users to select other options as they choose
- the default values wold be included in the select2 options as well due to the way we're bringing down the select2 values, so no need to
The Problem
- Select2 would be working entirely fine if I was starting from a blank form. It loads my values from an Ajax call upon dropdown, etc.
- However, when I load the invoice for display, the viewmodel values aren't set on the select2 controls.
- It appears the select2 control actually loads the data and overwrites my viewmodel's value with
""
when it loads, as a value hasn't been selected yet -- rather than letting me show a default item based on my bound value..
- It appears the select2 control actually loads the data and overwrites my viewmodel's value with
Thoughts so far on Trying to Solve it
I'll be investigating all of these:
- I might not be properly using the knockout binding to allow for a default element choice that isn't a part of its values.
- If there is a way I could verify that the select2 boxes are loaded and then trigger an element update, that would be fine, too.
The Code
Document Load
$(document).ready(function () {
'use strict';
console.log("creating viewmodel");
vm = new invoiceDetailsPage.ViewModel();
vm.Start();
console.log("applying bindings");
ko.applyBindings(vm);
});
The invoiceDetailsPage NameSpace(some irrelevant parts removed)
var invoiceDetailsPage = invoiceDetailsPage || {
InvoiceDetailItem: function () {
'use strict';
var self = this;
self.DatePayable = new Date();
self.Fees = 0.00;
self.Costs = 0.00;
self.Adjustments = ko.observable();
self.AdjustmentNote = ko.observable();
self.Total = ko.computed(function () {
});
self.hasAdjustments = ko.computed(function () {
});
},
Invoice: function (invoiceID, documentTypeID, firmID, invoiceNumber, invoicePeriod, datePayable, privateComment, statusID, vendorFirmID) {
'use strict';
var self = this;
self.TypeID = ko.observable(documentTypeID);
self.PrivateComment = ko.observable(privateComment);
self.Status = ko.observable(statusID);
self.FirmID = ko.observable(firmID);
self.VendorFirmID = ko.observable(vendorFirmID);
self.InvoicePeriod = ko.observable(invoicePeriod);
self.DatePayable = ko.observable(datePayable);
self.InvoiceNumbers = ko.observable(invoiceNumber);
self.DetailItems = ko.observableArray([]);
self.isFinalized = ko.computed(function () {
//finalized if it has the appropriate status (anything except)
});
self.hasPrivateComments = ko.computed(function () {
// if self.privatecomment isn't null or empty, true
});
self.TotalFees = ko.computed(function () {
//foreach item in detailitems, sum of fees.
});
self.TotalCosts = ko.computed(function () {
//foreach item in detailitems, sum of Costs.
});
self.TotalAdjustments = ko.computed(function () {
//foreach item in detailitems, sum of adjustments.
});
self.GrandTotal = ko.computed(function () {
//foreach item in detailitems, sum of totals.
});
},
LoadInvoice: function (clientSiteID, invoiceID, callbackFunction, errorFunction) {
'use strict';
var self = this;
self.clientSiteID = clientSiteID;
self.invoiceID = invoiceID;
$.ajax({
url: '/api/DefenseInvoice/GetDefenseInvoice?ClientSiteID=' + self.clientSiteID + "&InvoiceID=" + invoiceID,
type: 'GET',
processData: false,
contentType: 'application/json; charset=utf-8',
dataType: "json",
data: null,
success: function (data) {
console.log(data);
callbackFunction(data);
},
error: function (jqXHR, textStatus, errorThrown) {
errorFunction(jqXHR, textStatus, errorThrown);
}
});
},
ViewModel: function () {
'use strict';
var self = this;
self.InvoiceLoaded = ko.observable();
self.Invoice = ko.observable(new invoiceDetailsPage.Invoice()); // load blank invoice first
self.clientSiteID = -1;
self.invoiceID = -1;
self.SaveInvoiceDetails = function () {
// can only save the details prior to approval / rejection
// should update only general invoice fields, not private comments or adjustments
};
self.LoadInvoice = function() {
self.InvoiceLoaded(false);
invoiceDetailsPage.LoadInvoice(self.clientSiteID, self.invoiceID, function(result) {
//success
vm.Invoice(new invoiceDetailsPage.Invoice(
result.InvoiceInfo.DefenseInvoiceID,
result.InvoiceDocumentTypeID,
result.InvoiceInfo.FirmID,
result.InvoiceInfo.InvoiceNumber,
result.InvoiceInfo.InvoicePeriod,
result.InvoiceInfo.DatePayable,
result.InvoiceInfo.PrivateComment,
result.InvoiceInfo.StatusID,
result.InvoiceInfo.VendorFirmID
));
self.InvoiceLoaded(true);
}, function() {
//error
toastr.error("We're sorry, but an error occurred while trying to load the invoice. Please contact support or refresh the page to try again.", "Invoice Approval");
console.log("LoadInvoice -- ERROR");
console.log(" error: " + errorThrown);
toastr.clear(notifier);
});
};
self.Start = function () {
self.LoadInvoice();
};
},
utils: {
GetSelect2Options: function (placeholder, url) {
'use strict';
var options = {
allowClear: false,
placeholder: placeholder,
query: function (query) {
var dto = {
query: query.term,
filters: {
ClientSiteID: Number(vm.clientSiteID)
}
};
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(dto),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (msg) {
query.callback(msg);
}
});
}
};
return options;
}
}
};
The Knockout Binding we're using
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindingsAccessor) {
var obj = valueAccessor(),
allBindings = allBindingsAccessor(),
lookupKey = allBindings.lookupKey;
$(element).select2(obj);
if (lookupKey) {
var value = ko.utils.unwrapObservable(allBindings.value);
$(element).select2('data', ko.utils.arrayFirst(obj.data.results, function (item) {
return item[lookupKey] === value;
}));
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).select2('destroy');
});
},
update: function (element) {
$(element).trigger('change');
}
};
The HTML Element and its bindings
<input type="text" id="ddlInvoiceType" placeholder="Invoice Type" class="select2-container" data-bind="select2: invoiceDetailsPage.utils.GetSelect2Options('Invoice Type', '/api/DefenseInvoiceType/Post'), value: Invoice().TypeID"/>
""
instead of loading its value from the ViewModel'sInvoice().TypeID
property. – Priapic