Knockout binding, REUSEABLE code for JSON data - javascript

I am not able to figure out how to continue for the following data-bind.
I am having JSON data want to display in form of vertical grid. So I am fetching all the key values in one array(say columnName) and using to display column names. Now I am trying to display the values of JSON data using the array(columnName).
Please check the code, and let me know the solution.
JavaScript Code:
var _data = new Array({ firstname: 'Name1', lastname: 'LastName1' }, { firstname: 'Name2', lastname: 'Lastname2' });
var getColumnNames = new Array();
for (key in _data[0]) {
getColumnNames.push(key);
}
// Here's my data model
var ViewModel = function () {
this.coulmnNames = ko.observableArray(getColumnNames);
this.keyValue = ko.observableArray(_data);
};
ko.applyBindings(new ViewModel()); // This makes Knockout get to work
The Actual code is below:
<div data-bind="foreach: coulmnNames" style="display: inline-block;">
<div data-bind="text: $data"></div>
</div>
<div data-bind="foreach: keyValue" style="display: inline-block;">
<div style="display: inline-block;">
<div data-bind="text: firstname"></div>
<div data-bind="text: lastname"></div>
</div>
</div>
Want to code like this:
<div data-bind="foreach: coulmnNames" style="display: inline-block;">
<div data-bind="text: $data"></div>
</div>
<div data-bind="foreach: keyValue" style="display: inline-block;">
<div style="display: inline-block;">
<div data-bind="foreach: $parent.coulmnNames">
<div data-bind="text: ???????"></div>
</div>
</div>
</div>
I am just trying to write a reuse-able code, just have to vary the JSON data.
Thanks in advance.

You are almost there:
you need to reference the "current data" from the outer loop in the inner loop with $parent
you can use the indexer syntax to dynamically access a property in your inner loop with writing $parent[$data] where $data is the actual column name
So your binding should look like:
<div data-bind="foreach: keyValue" style="display: inline-block;">
<div style="display: inline-block;">
<div data-bind="foreach: $parent.coulmnNames">
<div data-bind="text: $parent[$data]"></div>
</div>
</div>
</div>
Demo JSFiddle.
You can read more about the $parent and $data binding context properties in the documentation.

Related

My single page application which is implemented using AngularJS is taking long time to load the page

I am working on the Single page application and using AngularJS. In my application, all DOM elements get loads using ajax and due to this I have used number of ng-repeat and binding expression and this is the reason my page is taking long time to load the page. Please help to solve my issue.
app
angular.module('tabApp', []);
Service
angular.module('tabApp')
.service('mrcService', ['$http', function($http) {
this.categories = [];
this.getCategories = function() {
return $http({
method: "GET",
url: 'mrcdata.aspx'
}).success(function(data) {
categories = data;
return categories;
});
};
}]);
Controller
angular.module('tabApp')
.controller('dynamicContentCtrl', ['$scope', 'mrcService', function($scope, mrcService) {
$scope.categories = [];
mrcService.getCategories().then(function(response) {
$scope.categories = response.data.Categories;
});
}]);
HTML code
<div class="main-content container" ng-controller="dynamicContentCtrl">
<div ng-controller="desktopTabCtrl" class="row desktop-content">
<div class="col-md-3 col-sm-4 category-items">
<nav class="nav categories-nav">
<ul class="categories">
<li ng-repeat="category in categories" class="category" ng-class="{ active: isSet(category.CategoryRank) }">
<a href="javascript:void(0)" ng-click="setTab(category.CategoryRank)" class="text">
<span>{{category.CategoryTitle}}</span>
</a>
<span class="arrow-right"></span>
</li>
<li class="category static">
<C5:LocalLiteral runat="server" Text="mrc.home.learnmoretab"/>
</li>
</ul>
</nav>
</div>
<div class="col-md-9 col-sm-8 document-tiles">
<div ng-repeat="category in categories" ng-show="isSet(category.CategoryRank)" class="tile-container">
<div class="row">
<div ng-repeat="document in category.Documents" class="col-md-6 col-sm-6 tile">
<div class="tile-content row">
<div class="col-md-12 col-sm-12">
<div class="thumbNail-content col-md-6 col-sm-6">
<p class="title">{{document.DocumentTitle}}</p>
<p class="audience">{{document.Audience}}</p>
</div>
<div class="thumbNail-image col-md-6 col-sm-6">
<img alt="Thumb Nail" src="{{document.ThumbnailUrl}}">
</div>
</div>
<div class="col-md-12 col-sm-12">
<div class="download-section">
<select class="lang-dropdwn"
ng-model="document.DefaultDialectId"
ng-change="selectLang(document , document.LocalizedDocuments , document.DefaultDialectId )">
<option ng-repeat="localizedDocument in document.LocalizedDocuments"
value="{{localizedDocument.DialectId}}">
{{localizedDocument.LanguageName}}
</option>
</select>
</div>
<div class="button-conatiner" ng-init="document.DownloadLink = document.DocumentId +':'+document.DefaultLocalizedDocumentId">
<a class="button" href="documentdownloader.aspx?documentid={{document.DownloadLink}}"><C5:LocalLiteral runat="server" Text="basket.esddelivery"/></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
Here's a few options you can try to improve performance.
1. You should set the track by in the ng-repeats
To minimize creation of DOM elements, ngRepeat uses a function to "keep track" of all items in the collection and their corresponding DOM elements. For example, if an item is added to the collection, ngRepeat will know that all other items already have DOM elements, and will not re-render them.
The default tracking function (which tracks items by their identity) does not allow duplicate items in arrays. This is because when there are duplicates, it is not possible to maintain a one-to-one mapping between collection items and DOM elements.
If you do need to repeat duplicate items, you can substitute the default tracking behavior with your own using the track by expression.
For example, you may track items by the index of each item in the collection, using the special scope property $index.
Example:
<div ng-repeat="item in items track by $index">{{item.name}}</div>
2. You could use one way data binding to items you know wont be changed. This will disable the watchers for those items and improve performance. Example:
Instead of this
{{item.name}}
Use this
{{::item.name}}
Please remember that this is a one way binding and these values will not be updated if something changes in your scope but you will need to manually update it.
3. Try to limit the ng-show and ng-hide attributes since this will add more watchers and leads to poor performance.

Not getting all values in View when binding children properties of observable

In my knockout bound view I am not getting all values.
This is my script file:
var ViewModel = function () {
var self = this;
self.games = ko.observableArray();
self.error = ko.observable();
self.detail = ko.observable();
var gamesUri = '/api/games/';
self.getGameDetail = function (item) {
ajaxHelper(gamesUri + item.Id, 'GET').done(function (data) {
self.detail(data);
});
console.log(self.detail);
};
function ajaxHelper(uri, method, data) {
self.error('');
return $.ajax({
type: method,
url: uri,
dataType: 'json',
contentType: 'application/json',
data: data ? JSON.stringify(data) : null
}).fail(function (jqXHR, textStatus, errorThrown) {
self.error(errorThrown);
});
}
function getAllGames() {
ajaxHelper(gamesUri, 'GET').done(function (data) {
self.games(data);
});
}
getAllGames();
};
ko.applyBindings(new ViewModel());
This is my view:
<div class="row">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Games</h2>
</div>
<div class="panel-body">
<ul class="list-unstyled" data-bind="foreach: games">
<li>
<strong><span data-bind="text: DeveloperName"></span>:<span data-bind="text: Title"></span></strong>
<small>Details</small>
</li>
</ul>
</div>
</div>
<div class="alert alert-danger" data-bind="visible: error"><p data-bind="text: error"></p></div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading"><h2 class="panel-title">Details</h2></div>
</div>
<table class="table">
<tr><td>Developer</td><td data-bind="text: detail().DeveloperName"></td></tr> //Only this value is displayed
<tr><td>Title</td><td data-bind="text: detail().Title"></td></tr>
<tr><td>Price</td><td data-bind="text: detail().Price"></td></tr>
<tr><td>Genre</td><td data-bind="text: detail().Genre"></td></tr>
<tr><td>Year</td><td data-bind="text: detail().Year"></td></tr>
</table>
</div>
<div class="col-md-4">
</div>
</div>
The problem is it only displays DeveloperName in the view. Title, Price, Genre and Year are not dispayed in the view. I tried many things but I don't know where the error is.
There are two approaches.
The easiest one is to use the with or template binding. The technique is similar, but I'll show an example with the with binding:
<table class="table" data-bind="with: details">
<tr><td>Developer</td><td data-bind="text: DeveloperName"></td></tr>
<tr><td>Title</td><td data-bind="text: Title"></td></tr>
<tr><td>Price</td><td data-bind="text: Price"></td></tr>
<tr><td>Genre</td><td data-bind="text: Genre"></td></tr>
<tr><td>Year</td><td data-bind="text: Year"></td></tr>
</table>
With this technique, whenever you change the object inside the details observable, the new values are applied to the children elements inside the elment which has the with binding. In this case all the elements inside the table element. Besides, the syntax is shorter and more clear. NOTE: you must can use $parent, $parents[] or $root if you need to bind something outside the object bound with with.
The hardest one, which is only neccessary if your viewmodel is more complex and has, for example, computed observables, you need to create an object whose properties are also observables. In this case you bind this object once, and, on the next occasions, you update the innser obervable properties, instead of changing the object itself.
For your example, you must create an object which has all its properties, like DeveloperName, Title, Price, etc. defined as observables. Then you must map the values recovered by AJAX to this properties, which you can do by hand, doing details().DeveloperName(newDetails.DeveloperName), and so on, or using the ko.mapping plugin.
Important note: if you use this technique you must keep the original details bound object, and update its properties. If you substitute the details object itself with a new one, the binding will be lost, and will stop working as desired.
Another note: you cannot use cleanNodes for what you think. Please, see this Q&A.

ObservableArray not binding to GUI

I'm new with knockout.js and trying to fix data binding on a site that is build on Laravel and is using knockout.js.
Observable array works well and items can be pushed and popped without issues. The problem is with the binding to GUI. When items are pushed to array those are added to GUI, but nothing else works, like removing items, and also when adding more items later on those are added on the top of the GUI element list, not added after existing items on the GUI. The observable array is having correct items after push/pop/removeall, its just not reflecting to GUI.
I guess that the problem is that observable array is not binded to GUI, but I cannot figure out what could be wrong.
Stripped code:
Chat.init = function(){
Chat.viewModel = new Chat.ViewModel;
ko.applyBindings(Chat.viewModel, $('#msg_canvas').get(0));
};
Chat.ViewModel = function(){
self.messages = ko.observableArray();
self.setMessages = function(msgs){
_.each(msgs, function(msg){
self.messages.push(msg);
});
};
self.clearMessages = function(data, e){
self.messages.removeAll();
}
}
clearMessages is called via onclick: data-bind="click: $parent.clearMessages
The HTML is this:
<div id="msg_canvas" class="msg-wrap col-md-12"
style="height:274px;overflow-y:scroll;" data-bind="foreach: messages">
<div class="media msg">
<div class="media-body">
<span data-bind="text: sent_at"></span>
<small class="col-lg-10" data-bind="text: message"></small>
</div>
</div>
Any help or pointer to what could be causing the problem would be highly appreciated.
UPDATE: added inner HTML which was not included to post before
You need to have a control inside the div to hold your messages, like a <span> or <p>. Otherwise, you're simply doing the foreach without outputting the values. So your div should look something like this, using $data to access the value:
<div id="msg_canvas" data-bind="foreach: messages">
<p data-bind="text: $data"></p>
</div>
Here's a working snippet based on your code (setMessages slightly modified / hard coded with values):
ViewModel = function(){
self.messages = ko.observableArray([]);
self.setMessages = function(){
var msgs = ['message','message','message'];
_.each(msgs, function(msg){
self.messages.push(msg + ' ' + self.messages().length);
});
};
self.clearMessages = function(data, e){
self.messages.removeAll();
}
self.removeMessage = function(item){
self.messages.remove(item);
}
};
ko.applyBindings(new ViewModel());
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="msg_canvas" class="msg-wrap col-md-12"
style="height:274px;overflow-y:scroll;border: black solid 1px" data-bind="foreach: messages">
<p data-bind="text: $data"></p>
<input type="button" data-bind="click: removeMessage" value="Remove Item" />
</div>
<input type="button" data-bind="click: setMessages" value="Add Message" />
<input type="button" data-bind="click: clearMessages" value="Remove All" />

Angular JS - Need to apply different filter (for a table) depending upon different drop downs on page

I'm having three different sets of drop downs and also search box to apply filter for a table. My table should filter if someone enters text in textbox (or) depending on the value selected in one of the three drop downs (textbox searches and drop down searches are exclusive of each other).
This is my HTML:
<div ng-app="myModule" ng-controller="myController">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<form class="navbar-form navbar-left form-horizontal" role="search">
<div class="form-group">
<label for="searchSeries" class="control-label">Find:</label>
<input type="text" id="searchSeries" ng-model="searchText" class="form-control" placeholder="Search TV Series" />
<div class="btn-group btn-input clearfix">
<button type="button" class="btn btn-default dropdown-toggle form-control" data-toggle="dropdown"> <span data-bind="label">Select One</span> <span class="caret"></span> </button>
<ul class="dropdown-menu" role="menu">
<li>Action</li>
<li>Animation</li>
<li>Comedy</li>
<li>Fantasy</li>
<li>Drama</li>
<li>Mystery</li>
<li>Romance</li>
<li>Science Fiction</li>
<li>Mini Series</li>
<li>Reality</li>
<li>Documentary</li>
<li>Crime</li>
<li>Game Show</li>
<li>Talk Show</li>
<li>Adventure</li>
<li>Home and Garden</li>
<li>Thriller</li>
<li>Sport</li>
<li>Family</li>
<li>Children</li>
<li>News</li>
<li>Horror</li>
</ul>
</div>
<select ng-model="properties.config" ng-options="prop.value as prop.name for prop in properties.configs">
</select>
<select ng-model="order.config" ng-options="template.value as template.name for template in order.configs">
</select>
</div>
</form>
</div>
<!-- /.navbar-collapse -->
</div>
<!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<tbody>
<tr ng-repeat="user in users[0].tvseries | filter: searchText | filter: order.config | filter:properties.config">
<td><img ng-src="{{user.thumbnail}}" alt="" /></td>
<td><div>{{user.tv_show_name}}</div>
<div>{{user.brief_description}}</div>
<div>{{user.rating}}</div></td>
<td><div>{{user.show_time}}</div>
<div>{{user.genre}}</div>
<div>{{user.current_season}}</div>
<div>{{user.current_episode}}</div></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
My controllers.js:
var myModule = angular.module('myModule', []);
myModule.controller('myController', function($scope, userRepository) {
userRepository.getAllUsers().success(function(users) {$scope.users = users;});
$scope.order = {};
$scope.properties = {};
//Configuration
$scope.order.configs = [
{'name': 'Ascending',
'value': 'tv_show_name'},
{'name': 'Descending',
'value': '-tv_show_name'}
];
$scope.properties.configs = [
{'name': 'Show Time',
'value': 'show_time'},
{'name': 'Rating',
'value': 'rating'}
];
//Setting first option as selected in configuration select
$scope.order.config = $scope.order.configs[0].value;
$scope.properties.config = $scope.properties.configs[0].value;
});
myModule.factory('userRepository', function($http) {
return {
getAllUsers: function() {
var url = "https://api.mongolab.com/api/1/databases/tvshowsdb/collections/tvshowsdbcollections?apiKey=e2SbmkVmLOQN-wH_ga84n9prYsgU8lQ5";
return $http.get(url);
}
};
});
Note that: when I apply "filter: searchText" in my html, it works fine and I face no issue. But when I try to apply two more filters "filter: order.config | filter:properties.config", my code doesn't execute. Please help me understand where I'm going wrong :(
Because you are adding and removing - before tv_show_name, I think that you are trying to sort the results by multiple fields? If that is the case, the syntax would be:
orderBy: [order.config, properties.config].
Here is a demo: http://plnkr.co/edit/yLYQGYGcGZMqoj6yfqoT?p=preview
Note: the demo is reading from a json file, instead of the mongolab api, because I needed to add duplicate names and start times, to show the effects of multi-level sorting. Also, notice that I changed the order of the selects to match the order in which you were applying the filters. I think it is intuitive that the first select take precedence over the next select.
Update
If you only want to sort by one of the options at a time, you can use a string variable, much like the orderBy api demo:
<div ng-click="predicate = 'tv_show_name'">Show Name ascending</div>
<div ng-click="predicate = '-tv_show_name'">Show Name descending</div>
<div ng-click="predicate = 'show_time'">Show Time</div>
<div ng-click="predicate = 'rating'">Rating</div>
...
<tr ng-repeat="user in users[0].tvseries | filter: searchText | orderBy:predicate">
Here is the updated Demo

How to do a foreach binding using KnockoutJS in Bootstrap-themed dropdown

I'm developing a small notifications-like module, where the user can see his 5 latest activities that are logged in the DB (MSSQL). The values that I need are all there, but for some reason knockout binding is not working. Here are code snippets:
<div class="dropdown-menu toolbar pull-right" data-bind="with: layoutLogsModel">
<h3 style="border: none;">Recent activities:</h3>
<!-- "mailbox-slimscroll-js" identifier is used with Slimscroll.js plugin -->
<ul id="mailbox-slimscroll-js" class="mailbox" data-bind="foreach: layoutLogsModel.notification">
<div class="alert inbox">
<a href="javascript:void(0)">
<i class="icon-book" style="color: orange;"></i>
Some text
</a>
<br>
Some text #2
</div>
</ul>
</div>
For now, I only want to display random text for every item that is in the observableArray.
ViewModel is the following:
var layoutLogsModel = {
notification: ko.observableArray()
};
function getLastFiveActivities() {
get(apiUrl + "Logs/GetLastFiveActivities", { ClientUserID: loggedUserID }, function (data) {
layoutLogsModel.notification(data);
});
}
And every time I call this function, the list is empty (IMAGE)
(the function is called on click, and absolutely no errors are shown in the console).
What is it that I am doing wrong?
EDIT:
The thing was, I forgot to execute ko.applyBindings for that viewModel. Then, I changed the HTML to look like this:
<ul id="mailbox-slimscroll-js" class="mailbox" data-bind="foreach: notification">
<div class="alert inbox">
<a href="javascript:void(0)">
<i class="icon-user" style="color: green;"></i>
<span data-bind="text: $data"></span>
</a>
</div>
</ul>
Aslo, I modified the get function slightly, like this:
function getLastFiveActivities() {
get(apiUrl + "Logs/GetLastFiveActivities", { ClientUserID: loggedUserID }, function (data) {
layoutLogsModel.notification(data.Notification);
});
}
(changed data to data.Notification based on the MVC model property that contains the array)
After all that, the data was available immediately.
try removing the layoutLogsModel from the foreach, you are already using it with the binding "with", so eveything in that div will be part of layoutLogsModel.
<div class="dropdown-menu toolbar pull-right" data-bind="with: layoutLogsModel">
<h3 style="border: none;">Recent activities:</h3>
<!-- "mailbox-slimscroll-js" identifier is used with Slimscroll.js plugin -->
<ul id="mailbox-slimscroll-js" class="mailbox" data-bind="foreach: notification">
<div class="alert inbox">
<a href="javascript:void(0)">
<i class="icon-book" style="color: orange;"></i>
Some text
</a>
<br>
Some text #2
</div>
</ul>
</div>

Categories

Resources