Populate knockout view model from javascript - javascript

I'm in the process of replacing one hell of a lot of javascript/jquery code with knockoutjs and I'm trying to figure out the best way forward. I have no time to replace everything at the same time so I will have to integrate the knockout logic with the existing javascript...
Is there a way to populate a knockout view model from javascript which is not called from a data-bind attribute? Any help would be nice since I've not been able to find this anywhere else (at least not anything that worked).
I know what I'm mentioning here isn't the "correct" way of doing things, but I'm trying to migrate parts of the javascript code... Doing it all in one go isn't an option at the moment.
(using knockout 3.2)
Edit:
Typically the existing javascript does something like:
$('#productlist').append(productItemHtmlCode);
And I would rather have it do something like:
ViewModel.productList.push(productItemObject);

If I understand correctly, currently you have something like this:
<div id='myDiv'>
current status is: <span id='statusSpan'>Active</span>
</div>
with some corresponding javascript that might be something like:
function toggleStatus() {
var s= document.getElementById('statusSpan');
s.innerHTML = s.innerHTML == 'Active' ? 'Inactive' : 'Active';
}
And you want to change it so that the javascript is updating the viewmodel rather than manipulating the DOM?
var app = (function() {
var vm = {
statusText: ko.observable('Active'),
toggleStatus: toggleStatus
}
return vm
function toggleStatus() {
vm.statusText = vm.statusText == 'Active' ? 'Inactive' : 'Active';
}
}) ();
ko.applyBindings(app,document.getElementById('myDiv'));
And then the html would be
<div id='myDiv'>
current status is: <span id='statusSpan' data-bind="text: statusText"></span>
</div>
If that's what you're talking about, that's what Knockout is designed for. The javascript updates the viewmodel, knockout manipulates the DOM.
The example you give is easy to represent in Knockout.
the HTML:
<div>
<table data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: name"></td>
<td data-bind="text: category"></td>
</tr>
</table>
</div>
and in the viewmodel:
vm = {
products: ko.observableArray(), // empty array to start
addProduct: addProduct
}
return vm;
function addProduct(id, name, category) {
products.push({id: id, name: name, category:category});
}
etc.

Related

Knockout TypeScript table not rendering data

I have a table, which shows invoices, then a nested table that shows the individual checks made for those invoices. I'm using knockout and typescript to render these tables. I am able to get the invoices to show, however the checks table doesn't show the data. Here's the code so far:
<tbody class="nohighlight" data-bind="foreach: parent.bankDrafts">
<tr>
<td><span data-bind="text: CheckID"></span></td>
<td><span data-bind="text: CheckRunID"></span></td>
<td><span data-bind="text: VendorName"></span></td>
<td><span data-bind="text: CheckDate"></span></td>
<td><span data-bind="text: FormatCurrency(CheckAmount)"></span></td>
<td><span data-bind="text: Globalize.formatCheckRunApproveStatus(ApprovalStatusID)"></span></td>
</tr>
</tbody>
Here's the typescript:
namespace CheckRunApproval {
declare let searchParameter: string;
class SearchCheckRunModel {
public searchParameter = ko.observable<string>(searchParameter || null);
public checkRuns = ko.observableArray<CheckRunModel>(null);
public bankDrafts = ko.observableArray<BankDraftInfoModel>();
}
var model = new SearchCheckRunModel();
export function GetBankDrafts(data: CheckRunModel): void {
CheckRunServiceMethods.GetBankDrafts(data.CheckRunID())
.done(bankDrafts => ko.mapping.fromJS(bankDrafts, null, model.bankDrafts));
}
}
And here's the service call:
public static GetBankDrafts(checkrunID: number): JQueryPromise<BankDraftInfo[]> {
return CommonMethods.doAjax<BankDraftInfo[]>(
"/Corp/Checks/CheckRunApprovalWS.asmx/getBankDrafts",
JSON.stringify({ checkrunID }),
"GetBankDrafts"
);
}
Now the server call does reach the server side code, passing in the correct parameters and returning the list of checks I'm trying to show as part of the invoice. However, the table itself does not have any data.
My thinking is that it has something to do with the way I'm mapping the model to the view model. It could also be the way I've setup the table itself, with the correct knockout attributes, etc. Any help would be greatly appreciated.
Edit: changing parent.bankDrafts to $parent.bankDrafts() it fixed the issue.
You have a typo in your code. Use $parent.bankDrafts instead of parent.bankDrafts in a foreach binding.

Knockout foreach binding not rendering anything

I have, what I thought was a fairly straightforward knockout situation. I have a model that comes in from WebApi that has an array of things with a Success element. I need the value of success to determine what of the properties render. I've validated that all the data is coming down from WebApi ok but nothing but the table shell renders. There are no errors in the dev console.
The HTML
<div id="model1Wrapper">
<table class = "table">
<thead >
<tr >
<th >Stuff</th><th>Things</th>
</tr>
</thead>
<tbody data-bind = "foreach: $data.historyArray" >
<!--ko if: Success -->
<tr class = "success" >
<td data-bind = "text: $data.ThingA" > </td>
<td data-bind = "text: ThingB" > </td>
</tr>
<!-- /ko -->
<!--ko ifnot: Success -->
<tr class = "danger" >
<td colspan="3" data-bind = "text: ThingC" > </td>
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
Example Model Data
[{
"ThingA": "A",
"ThingB": "B",
"ThingC": "C",
"Success": false
}, {
"ThingA": "A",
"ThingB": "B",
"ThingC": "C",
"Success": true
}]
This is monitoring a process that has feeds from several endpoints so I have multiple ViewModels on the page. So I framed up a rough example of how that is working elsewhere on the page.
That business
<script>
var sampleModelData = [{
"ThingA": "A",
"ThingB": "B",
"ThingC": "C",
"Success": false
}, {
"ThingA": "A",
"ThingB": "B",
"ThingC": "C",
"Success": true
}]
var viewModel1 = {
historyArray: ko.observableArray()
};
function onNewHistory(data) {
viewModel1.historyArray(data);
}
$(document).ready(function(){
ko.applyBindings(viewModel1, document.getElementById("model1Wrapper"));
onNewHistory(sampleModelData);
})
</script>
I had to mask of some of the speciffics but the gist is, the ajax call returns an array in the example. There is a function that is called to update the new data into the observable and I would expect the table to rerender, it does not.
Other deets
Sometimes there is no model data in the table so I load it and wait
for an update. All the other Viewmodels are loaded like this but this
is the only one with an array and the only one I'm having trouble
with.
I have tried taking out the if/ifnot business and that does not work.
Fiddler hates me and I have not been able to set up a clean version of this to try.
I leafed though some of the related questions and nothing seems to fit my issue. Or the example is much more complicated to apply.
Thanks!
The problem is in this code:
var viewModel1 = {
historyArray = ko.observableArray();
}
You're mixing the syntax for declaring objects with the syntax for code inside functions. When declaring an object, don't use = and ;. Instead use : and ,.
If you change the declaration to something like below, it should work.
var viewModel1 = {
historyArray: ko.observableArray()
}
Just adding another answer to this question in case someone comes across it in future. I had this issue and it was a result of initialising my observable array within the method. I didn't mean to do this (copy paste error) and it didn't produce any errors in the console so was difficult to trace.
For example:
LoadJSArrayIntoObservable(results) {
vm.validationResults = ko.observableArray(); <---- THIS IS INVALID.
vm.validationResults([]); <---- THIS IS WHAT I MEANT TO DO!!
$.each(results, function () {
try {
vm.validationResults.push(new ValidationResult(this));
}
catch (err) {
alert(err.message);
}
});

AngularJS and XML, how to render it?

I am working along DB guys, they are sending me the data thru XML, and depending the kind of element they specify is what I need to display in the view.
The code you will see is a dynamic table
<table>
<thead>
<tr>
<th ng-repeat="column in cols">
<span>{{column}}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in rows">
<td ng-repeat="column in cols"
ng-init="isXX = column.indexOf('XX') === 0">
<span ng-if="!isXX">{{row[column]}}</span>
<button ng-if="isXX" class="btn btn-xs btn-blue"
ng-click="fillOpen()">
{{column.substring(3).replace('_', ' ')}}
</button>
</td>
</tr>
</tbody>
</table>
and here is what I have in the controller
ReportsFactory.pendingBets(reportParam).then(function(data) {
if (data.length) {
gridInfo = _.forEach(data, function(item) {return item;});
$scope.rows = gridInfo;
$scope.cols = Object.keys($scope.rows[0]);
}
}
as you can see here I have this ng-init
ng-init="isXX = column.indexOf('XX') === 0" where I am telling the app, if the property I am receiving comes with XX at the index, then display a button <button ng-if="isXX" ng-click="fillOpen()">...</button> but so far, I have some more props coming with XX at the beginning, so I need to do it more dynamic.
This is how my view looks so far
what I need to know, is how to read that XML, this is the XML printed in the Nodejs terminal
[{ BET: 57635034,
CUSTOMER: 181645,
SPORT: 'NFL',
'XX_FILL OPEN': '<element><element_type>WAGER_ACTION_BUTTON</element_type><element_call>fillOpen(57635034)</element_call><element_content/></element>',
XX_VIEW: '<element><element_type>BASIC_DROPDOWN</element_type><element_call>callThisFunction()</element_call><element_content><li>1</li><li>2</li><li>3</li><li>4</li></element_content></element>',
XX_CANCEL: '<element><element_type>BASIC_CHECKBOX</element_type><element_call/><element_content>1</element_content></element>'
}]
so, the first says
'XX_FILL OPEN': '<element><element_type>WAGER_ACTION_BUTTON</element_type><element_call>fillOpen(57635034)</element_call><element_content/></element>'
WAGER_ACTION_BUTTON should be a button
the second one says
BASIC_DROPDOWN that should be a dropdown and so on, so, how should I do in order to display the proper HTML element depending on what the XML says ?
Any suggestions ?
if I understood you correctly you want to dynamically render the xml or html content to your view... I assume that element and element type are directive you have or something.
use
ngBindHtml
e.g:
<div class="col-xs-offset-1 m-r-offset-8 p-t-offset-2 font-l-16">
<span mathjax-bind ng-bind-html="question.question.body"></span>
</div>
or you might need to use the trustAsHtml function
<div class="col-xs-offset-1 m-r-offset-8 p-t-offset-2 font-l-16">
<span mathjax-bind ng-bind-html="trustAsHtml(question.question.body)"></span>
</div>
$scope.trustAsHtml = function (val) {
return $sce.trustAsHtml(val);
};
this will take your string xml (html) code and render it...
you could always build a personalize directive and use $compile as well like:
app.directive('ngHtmlCompile',function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.ngHtmlCompile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
});
and in the code just call the ng-html-compile... no need for $sce

How to bind a ko.obersavableArray that is nested in an object

I have my knockout page hub, and I need a ko.obeservableArray nested in a ko.observable object, this is where I define them:
function IncomeDeclarationHub() {
//data comes from a ajax call.
self.myIncomeDeclarationViewModel = ko.observable(new IncomeDeclarationViewModel(data));
}
function IncomeDeclarationViewModel(data) {
var self = this;
self.retentionAmount = ko.observable();
self.taxableMonth = ko.observable();
self.incDecDetGroViewModels = ko.observableArray();
if (data != null) {
var arrayLenght = data.IncDecDetGroViewModels.length;
for (var i = 0; i < arrayLenght; i++) {
var myObject = new IncomeDecDetGroViewModel(data.IncDecDetGroViewModels[i]);
self.incDecDetGroViewModels.push(myObject);
}
}
}
And this is my HTML code:
<span class="label">
Retention Amount
</span>
<input data-bind="value: myIncomeDeclarationViewModel.retentionAmount" />
<table>
<tbody data-bind="foreach: myIncomeDeclarationViewModel.incDecDetGroViewModels">
...
</tbody>
</table>
Ok so the thing is that incDecDetGroViewModels never gets populated, I used to have that ko.obersableArray outside the object, and it worked fine, now that I inserted it in my object myIncomeDeclarationViewModel is not populating the html table. Do I need to call it in a different way at the data-bind
myIncomeDeclarationViewModel is an observable, so you have to unwrap it to access it's properties. Add parenthesis to unwrap it (access the observable's underlying value) like this:
<span class="label">
Retention Amount
</span>
<input data-bind="value: myIncomeDeclarationViewModel().retentionAmount" />
<table>
<tbody data-bind="foreach: myIncomeDeclarationViewModel().incDecDetGroViewModels">
...
</tbody>
</table>
Here's a working jsFiddle based on your example
JsFiddle
well previously you can access just becoz it is in scope but right now you done some nesting so you just need to some looping in your view part to get that .
Something like this may be :
<table data-bind="foreach:myIncomeDeclarationViewModel">
<tbody data-bind="foreach:$data.incDecDetGroViewModels">
...
</tbody>
</table>
You can also ContainerLess foreach if you looking for something different like :
<!-- ko foreach:myIncomeDeclarationViewModel -->
//your table code
<!--/ko-->
I hope this solves the riddle .

Using knockoutJS, how to bind list items to same view?

I am new to Knockout and I am building a Simple POC for using knockout to build SPA(Single Page Application).
What I want to do is to show "Business Units" when the app loads and on selection of a business unit show all "Front End Units" under that business unit and on selection of a front end unit, show all "Sales Segments" under that front end unit.
All this will happen in a single page using the same view and the viewmodel will bind the model based on selected business unit or front end unit.
The issue I am facing is that, I have 5 business units that get bound properly first on document ready, but on selection of business unit, the front end units get repeated 5 times each. In this case, I have 2 front end units and each is shown 5 times. Same issue on selection of front end unit.
You can see this issue mimicked in the following jsFiddle sample - jsFiddle Link
Let me know if you can't access the jsfiddle link. In this sample, I have used arrays, but in actual I will be getting the data through async call to the oData service.
This is the view HTML:
<div id="divbu">
<h4 data-bind="text: Heading"></h4>
<ul data-role="listview" data-inset="true" data-bind="foreach: Collection">
<li data-role="list-divider" data-bind="text: EntityName"></li>
<li>
<a href="#" data-bind="click: $root.fnNextLevel">
<table border="0">
<tr>
<td>
<label style="font-size: 12px;">Bus. Plan: </label>
</td>
<td>
<label style="font-size: 12px;" data-bind="text: BusinessPlan"></label>
</td>
<td>
<label style="font-size: 12px;">Forecast: </label>
</td>
<td>
<label style="font-size: 12px;" data-bind="text: Forecast"></label>
</td>
</tr>
<tr>
<td>
<label style="font-size: 12px;">Gross Sales: </label>
</td>
<td colspan="3">
<label style="font-size: 12px;" data-bind="text: GrossSales"></label>
</td>
</tr>
</table>
</a>
</li>
</ul>
</div>
This is the model and view model:
function CommonModel(model, viewType) {
var self = this;
if (viewType == 'BU') {
self.EntityName = model[0];
self.BusinessUnit = model[0];
self.BusinessPlan = model[1];
self.Forecast = model[2];
self.GrossSales = model[3];
} else if (viewType == 'FEU') {
self.EntityName = model[1];
self.BusinessUnit = model[0];
self.FrontEndUnit = model[1];
self.BusinessPlan = model[2];
self.Forecast = model[3];
self.GrossSales = model[4];
} else if (viewType == 'SS') {
self.EntityName = model[2];
self.BusinessPlan = model[3];
self.Forecast = model[4];
self.GrossSales = model[5];
}
}
function ShipmentReportsViewModel(results, viewType) {
var self = this;
self.Collection = ko.observableArray([]);
for (var i = 0; i < results.length; i++) {
self.Collection.push(new CommonModel(results[i], viewType));
}
if (viewType == 'BU') {
self.Heading = "Business Units";
self.fnNextLevel = function (businessUnit) {
FetchFrontEndUnits(businessUnit);
};
self.Home = function () {
FetchBusinessUnits();
};
} else if (viewType == 'FEU') {
self.Heading = results[0][0];
self.fnNextLevel = function (frontEndUnit) {
FetchSalesSegments(frontEndUnit);
};
self.Home = function () {
FetchBusinessUnits();
};
} else if (viewType == 'SS') {
self.fnNextLevel = function () {
alert('No activity zone');
};
self.Heading = results[0][0] + ' - ' + results[0][1];
self.Home = function () {
FetchBusinessUnits();
};
}
}
You can see the complete code in the jsFiddle link.
I have also tried this with multiple views and multiple view models, where I apply bindings by giving the element ID. In this case, one flow from business unit -> sales segment is fine, but when I click on home or back button and I do binding again to that element, I face the same issue. (home and back button features are not done in jsFiddle example).
Let me know if more details are required. I did look into lot of other links in stack overflow, but nothing addressing this particular problem.
Any help is deeply appreciated. Thanks in advance.
The problem here is that you call your ko.applybindings TWICE and there is a foreach binding that iterate within 5 items, therefore the data are duplicated five times.
you should not call a ko.applybindings more than once on the same model.
Your model is always the same even if it's parametrized.
I had the same problem here: Data coming from an ObservableArray are displayed twice in my table
the fact that you have you business logic inside your viewModel is something that could be discussed, and it makes it not easy to fix this.
Make 3 classes, put them in a common model without logic inside. Then once you have applyed the ko.applyBindings once, you just have to modify the array like this:
viewModel.myArray(newValues)
Here is the fiddle with the amended code: http://jsfiddle.net/MaurizioPiccini/5B9Fd/17/
it does not do exaclty what you need but if remove the multiple bindings by moving the Collection object scope outside of your model.
As you can see the problem IS that you are calling the ko.applybindings twice on the same model.
Finally, I got this working. Thanks to #MaurizioIndenmark.
Though I have removed multiple call for ko.applybindings, I was still calling the view model multiple times. This was causing the issue.
Now, I have cleaner view model and I have different function calls for different actions and modify all the data required to be modified within these functions(events). Now, everything is working as expected.
This is how the view model looks now -
function ShipmentReportsViewModel(results) {
var self = this;
self.Heading = ko.observable();
self.BusinessUnits = ko.observableArray();
self.FrontEndUnits = ko.observableArray();
self.SalesSegments = ko.observableArray();
self.Home = function () {
var bu = FetchBusinessUnits();
self.Heading("Business Units");
self.BusinessUnits(bu);
self.FrontEndUnits(null);
self.SalesSegments(null);
};
self.fnFeu = function (businessUnit) {
var feu = FetchFrontEndUnits(businessUnit);
self.Heading(feu[0].BusinessUnit);
self.FrontEndUnits(feu);
self.BusinessUnits(null);
self.SalesSegments(null);
};
self.fnSalesSeg = function (frontEndUnit) {
var ss = FetchSalesSegments(frontEndUnit);
self.Heading(ss[0].BusinessUnit + ' - ' + ss[0].FrontEndUnit);
self.SalesSegments(ss);
self.BusinessUnits(null);
self.FrontEndUnits(null);
};
self.Home();
}
To see the entire working solution, please refer this jsFiddle
Thanks for all the valuable suggestions in getting this work.

Categories

Resources