Using knockout.js, I have a list of items that I filter on search using ko.utils.arrayFilter.
The filtering part works ok, but then when I delete the search term and the list repopulates as the full list, some of the DOM elements lose their id. How can I make them not lose their id? What am I missing?
In particular, the items with the id "tabsDiv1", "tabsDiv2", etc lose their ids.
Thanks in advance for your help, I'm stumped!
full git repo here with example github pages site: https://github.com/andrewcockerham/UdacityFENDP5NeighborhoodMap
var ViewModel = function() {
var self = this;
this.placeList = ko.observableArray([]);
this.filter = ko.observable("");
this.filteredItems = ko.computed(function() {
var filter = self.filter().toLowerCase();
if (!filter) {
return this.placeList();
} else {
return ko.utils.arrayFilter(self.placeList(), function(item) {
return item.name().toLowerCase().indexOf(filter) !== -1;
});
}
}, this);
The problem is that you're setting the ids of the tabs manually on load instead of letting knockout set it. When knockout renders the page, it will use the current state of the dom elements as templates for your tabs. In this case, your elements had blank ids. I see you apparently had knockout set the id at one point but you commented it out. You should go back and use knockout to add the id if you really needed it.
<div class="tabsDiv col-md-12 col-sm-12" id=""
style="display: none; padding-left: 0px; padding-right: 0px;
border-style: solid;border-width:1px;"
>
<!-- data-bind="attr: { id: 'tabsDiv' + $index() }" -->
...
<script>
window.onload = function() {
// get all tabDiv elements and add id with index to each one
var tabsDivArray = document.getElementsByClassName("tabsDiv");
for (var i = 0; i < tabsDivArray.length; i++) {
var tabsDiv = tabsDivArray[i];
tabsDiv.id = 'tabsDiv' + i.toString();
};
...
This might not be the only problem, but it's a start. You're mixing up a lot of knockout and manual dom manipulation and jquery. You really should try to limit the mixing up to keep it manageable. It would be a tough read for anyone else who came and looked at this.
I am beyond confused...
I am creating a list using Knockout.js components, templates, and custom elements. For some reason, the steps I create in my Viewmodel are being initialized in random order within the custom element definition! And it is completely randomized so that it is different each time!
To help better illustrate this, it is best to look at the JSFiddle. I put alert("break") after each step initialization. Load it once, and then click "run" again to see the demo properly. Look in the output window and you can see that other than step 1 being written first, the steps always appear randomly (though they maintain their order in the end).
https://jsfiddle.net/uu4hzc41/8/
I need to have these in the correct order because I will add certain attributes from my model into an array. When they are random I can't access the array elements properly.
HTML:
<ul>
<sidebar-step params="vm: sidebarStepModel0"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel1"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel2"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel3"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel4"></sidebar-step>
</ul>
JS/Knockout:
//custom element <sidebar-step>
ko.components.register("sidebar-step", {
viewModel: function (params) {
this.vm = params.vm;
alert("break");
},
template: "<li data-bind='text: vm.message'>vm.onChangeElement</li>"
});
// model
var SidebarStepModel = function () {
this.message = ko.observable("step description");
};
// viewmodel
var OrderGuideViewModel = function () {
this.sidebarStepModel0 = new SidebarStepModel();
this.sidebarStepModel0.message("step 1");
this.sidebarStepModel1 = new SidebarStepModel();
this.sidebarStepModel1.message("step 2");
this.sidebarStepModel2 = new SidebarStepModel();
this.sidebarStepModel2.message("step 3");
this.sidebarStepModel3 = new SidebarStepModel();
this.sidebarStepModel3.message("step 4");
this.sidebarStepModel4 = new SidebarStepModel();
this.sidebarStepModel4.message("step 5");
};
ko.applyBindings(new OrderGuideViewModel());
By default knockout components load asynchronously. In version 3.3 an option was added to allow the component to load synchronously.
Add synchronous:true when registering to get the behavior you want.
Example:
ko.components.register("sidebar-step", {
viewModel: function (params) {
this.vm = params.vm;
alert("break");
},
template: "<li data-bind='text: vm.message'>vm.onChangeElement</li>",
synchronous: true
});
I have a div that is setup to bind to a observeableArray ,but I only want to show at most 50 items from that observeableArray at any given time. I want to handle this with pagination with a previous and next button along with indices on the page to allow users to cycle through pages of items from the collection. I know I could probably do this with a computedObservable and a custom data binding but I'm not sure how to do it (I'm still a Knockout neophyte). Can anyone point me in the right direction?
Here is my code (the JS is in TypeScript):
<div class="container-fluid">
<div class="row-fluid">
<div class="span12">
<%=
if params[:q]
render 'active_search.html.erb'
else
render 'passive_search.html.erb'
end
%>
<%= form_tag("/search", method: "get", :class => "form-search form-inline") do %>
<%= label_tag(:q, "Search for:") %>
<%= text_field_tag(:q, nil, class:"input-medium search-query") %>
<%= submit_tag("Search", :class=>"btn") %>
<% end %>
<div class="media" data-bind="foreach: tweetsArray">
<%= image_tag('twitter-icon.svg', :class=>"tweet_img", :style=>"display:inline;") %>
<div class="media-body" style="display:inline;">
<h4 class="media-heading" data-bind="text: user.screen_name" style="display:inline;"></h4>
<span data-bind="text:text" style="display:inline;"></span> <br />
<span data-bind="text:'Created at '+created_at"></span> <br />
</div>
</div>
<div class="pagination pagination-centered">
<ul>
<li>
Prev
</li>
<li>
1
</li>
<li>
Next
</li>
</ul>
</div>
</div>
</div>
</div>
<script>
var viewModel = new twitterResearch.TweetViewModel();
ko.applyBindings(viewModel);
//TODO: notes to self, use custom binding for pagination along with a computed observable to determine where at in the list you are
//document.onReady callback function
$(function() {
$.getJSON('twitter', {}, function(data) {
viewModel.pushTweet(data);
console.log(data.user);
});
});
</script>
declare var $: any;
declare var ko: any;
module twitterResearch {
class Tweet {
text: string;
created_at: string;
coordinates: string;
user: string;
entities: string;
id: number;
id_str: string;
constructor(_text: string, _created_at: string, _coordinates: any, _user: any,
_entities: any, _id_str: string, _id: number){
this.text = _text;
this.created_at = _created_at;
this.coordinates = _coordinates;
this.user = _user;
this.entities = _entities;
this.id_str = _id_str;
this.id = _id;
}
}
export class TweetViewModel{
tweetsArray: any;
constructor()
{
this.tweetsArray = ko.observableArray([]);
}
//tweet is going to be the JSON tweet we return
//from the server
pushTweet(tweet)
{
var _tweet = new Tweet(tweet.text, tweet.created_at, tweet.coordinates,
tweet.user, tweet.entities, tweet.id_str, tweet.id);
this.tweetsArray.push(_tweet);
this.tweetsArray.valueHasMutated();
}
}
}
Pagination is quite simple with Knockout. I would personally achieve it this way:
Have an observableArray containing all your elements
Have an observable containing the current page (initialized to 0)
Have a variable declaring the number of elements per page
Have a computed that returns the number of pages, calculated thanks to the number of elements per page and the total number of elements.
Finally, add a computed that slices the array containing all the elements.
Given that, you can now add a function that increments (next) or decrements (previous) the current page.
Here is a quick example:
var Model = function() {
var self = this;
this.all = ko.observableArray([]);
this.pageNumber = ko.observable(0);
this.nbPerPage = 25;
this.totalPages = ko.computed(function() {
var div = Math.floor(self.all().length / self.nbPerPage);
div += self.all().length % self.nbPerPage > 0 ? 1 : 0;
return div - 1;
});
this.paginated = ko.computed(function() {
var first = self.pageNumber() * self.nbPerPage;
return self.all.slice(first, first + self.nbPerPage);
});
this.hasPrevious = ko.computed(function() {
return self.pageNumber() !== 0;
});
this.hasNext = ko.computed(function() {
return self.pageNumber() !== self.totalPages();
});
this.next = function() {
if(self.pageNumber() < self.totalPages()) {
self.pageNumber(self.pageNumber() + 1);
}
}
this.previous = function() {
if(self.pageNumber() != 0) {
self.pageNumber(self.pageNumber() - 1);
}
}
}
You'll find a simple and complete example here: http://jsfiddle.net/LAbCv/ (might be a bit buggy, but the idea is there).
Actually I am working on a website, which has a lot of tables (most of them need paging).So actually, I needed some reusable-component for paging to use it in all the cases which I need paging.
Also, I needed more advanced features than which provided in the accepted answer to this question.
So I developed my own component to solving this issue, here it is.
Now on Github
JsFiddle
And for more details, continue reading (Please consider to take the code from GitHub, not from here, as the GitHub code was updated and enhanced since I put it here)
JavaScript
function PagingVM(options) {
var self = this;
self.PageSize = ko.observable(options.pageSize);
self.CurrentPage = ko.observable(1);
self.TotalCount = ko.observable(options.totalCount);
self.PageCount = ko.pureComputed(function () {
return Math.ceil(self.TotalCount() / self.PageSize());
});
self.SetCurrentPage = function (page) {
if (page < self.FirstPage)
page = self.FirstPage;
if (page > self.LastPage())
page = self.LastPage();
self.CurrentPage(page);
};
self.FirstPage = 1;
self.LastPage = ko.pureComputed(function () {
return self.PageCount();
});
self.NextPage = ko.pureComputed(function () {
var next = self.CurrentPage() + 1;
if (next > self.LastPage())
return null;
return next;
});
self.PreviousPage = ko.pureComputed(function () {
var previous = self.CurrentPage() - 1;
if (previous < self.FirstPage)
return null;
return previous;
});
self.NeedPaging = ko.pureComputed(function () {
return self.PageCount() > 1;
});
self.NextPageActive = ko.pureComputed(function () {
return self.NextPage() != null;
});
self.PreviousPageActive = ko.pureComputed(function () {
return self.PreviousPage() != null;
});
self.LastPageActive = ko.pureComputed(function () {
return (self.LastPage() != self.CurrentPage());
});
self.FirstPageActive = ko.pureComputed(function () {
return (self.FirstPage != self.CurrentPage());
});
// this should be odd number always
var maxPageCount = 7;
self.generateAllPages = function () {
var pages = [];
for (var i = self.FirstPage; i <= self.LastPage() ; i++)
pages.push(i);
return pages;
};
self.generateMaxPage = function () {
var current = self.CurrentPage();
var pageCount = self.PageCount();
var first = self.FirstPage;
var upperLimit = current + parseInt((maxPageCount - 1) / 2);
var downLimit = current - parseInt((maxPageCount - 1) / 2);
while (upperLimit > pageCount) {
upperLimit--;
if (downLimit > first)
downLimit--;
}
while (downLimit < first) {
downLimit++;
if (upperLimit < pageCount)
upperLimit++;
}
var pages = [];
for (var i = downLimit; i <= upperLimit; i++) {
pages.push(i);
}
return pages;
};
self.GetPages = ko.pureComputed(function () {
self.CurrentPage();
self.TotalCount();
if (self.PageCount() <= maxPageCount) {
return ko.observableArray(self.generateAllPages());
} else {
return ko.observableArray(self.generateMaxPage());
}
});
self.Update = function (e) {
self.TotalCount(e.TotalCount);
self.PageSize(e.PageSize);
self.SetCurrentPage(e.CurrentPage);
};
self.GoToPage = function (page) {
if (page >= self.FirstPage && page <= self.LastPage())
self.SetCurrentPage(page);
}
self.GoToFirst = function () {
self.SetCurrentPage(self.FirstPage);
};
self.GoToPrevious = function () {
var previous = self.PreviousPage();
if (previous != null)
self.SetCurrentPage(previous);
};
self.GoToNext = function () {
var next = self.NextPage();
if (next != null)
self.SetCurrentPage(next);
};
self.GoToLast = function () {
self.SetCurrentPage(self.LastPage());
};
}
HTML
<ul data-bind="visible: NeedPaging" class="pagination pagination-sm">
<li data-bind="css: { disabled: !FirstPageActive() }">
<a data-bind="click: GoToFirst">First</a>
</li>
<li data-bind="css: { disabled: !PreviousPageActive() }">
<a data-bind="click: GoToPrevious">Previous</a>
</li>
<!-- ko foreach: GetPages() -->
<li data-bind="css: { active: $parent.CurrentPage() === $data }">
<a data-bind="click: $parent.GoToPage, text: $data"></a>
</li>
<!-- /ko -->
<li data-bind="css: { disabled: !NextPageActive() }">
<a data-bind="click: GoToNext">Next</a>
</li>
<li data-bind="css: { disabled: !LastPageActive() }">
<a data-bind="click: GoToLast">Last</a>
</li>
</ul>
Features
Show on need When there is no need for paging at all (for example the items which need to display less than the page size) then the HTML component will disappear.
This will be established by statementdata-bind="visible: NeedPaging".
Disable on need
for example, if you are already selected the last page, why the last page or the Next button should be available to press?
I am handling this and in that case I am disabling those buttons by applying the following binding data-bind="css: { disabled: !PreviousPageActive() }"
Distinguish the Selected page
a special class (in this case called active class) is applied on the selected page, to make the user know in which page he/she is right now. This is established by the binding data-bind="css: { active: $parent.CurrentPage() === $data }"
Last & First
going to the first and last page is also available by simple buttons dedicated to this.
Limits for displayed buttons
suppose you have a lot of pages, for example, 1000 pages, then what will happen? would you display them all for the user? absolutely not you have to display just a few of them according to the current page. for example, showing 3 pages before and other 3 pages after the selected page.
This case has been handled here <!-- ko foreach: GetPages() -->
the GetPages function applying a simple algorithm to determine if we need to show all the pages (the page count is under the threshold, which could be determined easily), or to show just some of the buttons.
you can determine the threshold by changing the value of the maxPageCount variable
Right now I assigned it as the following var maxPageCount = 7; which mean that no more than 7 buttons could be displayed for the user (3 before the SelectedPage, and 3 after the Selected Page) and the Selected Page itself.
You may wonder, what if there were not enough pages after OR before the current page to display? do not worry I am handling this in the algorithm for example, if you have 11 pages and you have maxPageCount = 7 and the current selected page is 10, Then the following pages will be shown
5,6,7,8,9,10(selected page),11
so we always stratifying the maxPageCount, in the previous example showing 5 pages before the selected page and just 1 page after the selected page.
Selected Page Validation
All set operation for the CurrentPage observable which determine the selected page by the user, is going through the function SetCurrentPage. In only this function we set this observable, and as you can see from the code, before setting the value we make validation operations to make sure that we will not go beyond the available page of the pages.
Already clean
I use only pureComputed not computed properties, which means you do not need to bother yourself with cleaning and disposing of those properties. Although, as you will see in the example below, you need to dispose of some other subscriptions which are outside of the component itself
NOTE 1
You may notice that I am using some bootstrap classes in this component,
This is suitable for me, but , of course, you can use your own classes instead of the bootstrap classes.
The bootstrap classes which I used here are pagination, pagination-sm, active and disabled
Feel free to change them as you need.
NOTE 2
So I introduced the component for you, It is time to see how it could work.
You would integrate this component into your main ViewModel as like this.
function MainVM() {
var self = this;
self.PagingComponent = ko.observable(new Paging({
pageSize: 10, // how many items you would show in one page
totalCount: 100, // how many ALL the items do you have.
}));
self.currentPageSubscription = self.PagingComponent().CurrentPage.subscribe(function (newPage) {
// here is the code which will be executed when the user changes the page.
// you can handle this in the way you need.
// for example, in my case, I am requesting the data from the server again by making an ajax request
// and then updating the component
var data = /*bring data from server , for example*/
self.PagingComponent().Update({
// we need to set this again, why? because we could apply some other search criteria in the bringing data from the server,
// so the total count of all the items could change, and this will affect the paging
TotalCount: data.TotalCount,
// in most cases we will not change the PageSize after we bring data from the server
// but the component allows us to do that.
PageSize: self.PagingComponent().PageSize(),
// use this statement for now as it is, or you have to made some modifications on the 'Update' function.
CurrentPage: self.PagingComponent().CurrentPage(),
});
});
self.dispose = function () {
// you need to dispose the manual created subscription, you have created before.
self.currentPageSubscription.dispose();
}
}
Last but not least, Sure do not forget to change the binding in the HTML component according to your special viewModel, or wrap all the component with the with binding like this
<div data-bind="with: PagingComponent()">
<!-- put the component here -->
</div>
Cheers
I have created a blogpost with detailed explanation on how to create pagination with the help of a little JQuery plugin (here).
Basically, I have used normal knockout data binding with AJAX and after data has been retrieved from the server, I call the plugin. You can find the plugin here. It's called Simple Pagination.
This question is still one of the top searches for "knockout pagination", so the knockout extension knockout-paging (git) is worth mentioning.
It provides pagination by extending ko.observableArray. It is well documented and easy to use.
The usage example is here.
I've got a ListView that was using HTML-defined templates like this:
<div id="mediumListIconTextTemplate" data-win-control="WinJS.Binding.Template">
<div>
<!-- Displays the "picture" field. -->
<img data-win-bind="alt: title; src: picture" />
<div>
<!-- Displays the "title" field. -->
<h4 data-win-bind="innerText: title"></h4>
<!-- Displays the "text" field. -->
<h6 data-win-bind="innerText: description"></h6>
</div>
</div>
</div>
<div id="basicListView" data-win-control="WinJS.UI.ListView"
data-win-options="{itemDataSource : DataExample.itemList.dataSource, itemTemplate: select('#mediumListIconTextTemplate')}">
</div>
When my list items changed, my item template would be updated to reflect the change. However, out of need, I had to change to using a javaScript function to build my template. I modeled my code after the code found on the sample site:
app.onactivated = function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
// TODO: This application has been newly launched. Initialize
// your application here.
} else {
// TODO: This application has been reactivated from suspension.
// Restore application state here.
}
args.setPromise(WinJS.UI.processAll().then(function () {
var lView = document.getElementById("templateFunctionListView").winControl;
lView.itemTemplate = itemTemplateFunction;
}));
}
};
function itemTemplateFunction(itemPromise) {
return itemPromise.then(function (item) {
var div = document.createElement("div");
var img = document.createElement("img");
img.src = item.data.picture;
img.alt = item.data.title;
div.appendChild(img);
var childDiv = document.createElement("div");
var title = document.createElement("h4");
title.innerText = item.data.title;
childDiv.appendChild(title);
var desc = document.createElement("h6");
desc.innerText = item.data.text;
childDiv.appendChild(desc);
div.appendChild(childDiv);
return div;
});
};
After changing to the javascript function, my display items never change when my binding data changes.
What do I need to do to make them update?
I think there are two approaches that could work for you.
In my case, when I refresh the data for my app, it's possible that one or more entities may be completely out of date, and there may be new entities to display. So I simply re-set the binding of the ListView, like so:
listView.itemDataSource = Data.items.dataSource;
where Data is the namespace I set up in data.js to contain all my data functions and objects.
When I update the value of the itemDataSource property, the ListView will re-bind to the new data, and display the correct items.
The other thing to look at, if your data is only being updated one property at a time, is using the WinJS.Binding.as function to make the items in the binding list observable, as described here:
http://msdn.microsoft.com/en-us/library/windows/apps/hh781224.aspx#updating_changing_records
If you haven't seen it already, there's some good info on databinding here:
http://msdn.microsoft.com/en-us/library/windows/apps/hh758311.aspx
And I found the following MSDN forum thread that may be helpful:
http://social.msdn.microsoft.com/Forums/en-US/winappswithhtml5/thread/21b9603f-e28d-4c93-b164-a2c91ba5c4ca
Hope the above helps!
For more information on Windows Store app development, register for App Builder.
Instead of rebinding the whole data set you can just rebind the single item. Find the items position in the list and then splice it replacing it with itself.
for (var i = 0; i < list.length; i++){
item = list.getAt(i);
if (item.key == itemToBeReBound.key){
list.splice(i, 1, item);
i = list.length;
}
}
Working on a little feedback form and I'm new at the Knockout/jQuery game so I'm sure this is a syntax error.
Goal / Background
I have a feedback form, part of which includes a list with feedback types. The actual text of the feedback type I'd like to use is stored in the "Title" attribute of the LI tags.
I'd like to pass an onclick from each of a set of LI tags denoting the type of feedback.
I would like knockout to receive this onclick event with the calling element
I'd like the ViewModel function to update the ViewModel's feedback type based on the content of the LI's title attribute
I'd then like to remove a class from all the list and apply it to the selected element.
I already have jQuery that does this; just want to incorporate it into the model change.
What I Have So Far
The relevant part of the HTML Feedback Form (the UL list):
<ul class="thumbnails" id="feedbackList">
<li class="feedbackItem" id="feedbackItemPraise" title="Praise" data-bind="click: updateFeedbackType"><i class="icon-thumbs-up"></i>Praise</li>
<li class="feedbackItem" id="feedbackItemCriticism" title="Criticism" data-bind="click: updateFeedbackType"><i class="icon-thumbs-down"></i>Criticism</li>
<li class="feedbackItem" id="feedbackItemProblem" title="Problem" data-bind="click: updateFeedbackType"><i class="icon-warning-sign"></i>Problem</li>
<li class="feedbackItem" id="feedbackItemQuestion" title="Question" data-bind="click: updateFeedbackType"><i class="icon-question-sign"></i>Question</li>
</ul>
The ViewModel so far (including some irrelevant parts):
var FeedbackViewModel = function () {
var self = this;
self.manualEMailAddress = "MyEmail#MyProvider.com";
self.manualApplicationName = "MyApplication";
self.username = ko.observable($("#feedbackUsernameFromServer").val());
self.feedbackType = ko.observable("Praise");
self.wantsFollowUp = ko.observable(true);
self.enteredName = ko.observable("");
self.feedbackText = ko.observable("");
self.userNameCaptured = ko.computed(function () { return self.username().length > 3; }, self);
self.mailToLink = ko.computed(function () { return "mailto:" + self.manualEMailAddress + "?subject=" + encodeURIComponent(self.feedbackType()) + encodeURIComponent(" for ") + encodeURIComponent(self.manualApplicationName) + "&body=" + encodeURIComponent(self.feedbackText()) }, self);
};
var feedbackViewModel = new FeedbackViewModel();
ko.applyBindings(feedbackViewModel, document.getElementById("feedbackModal"));
The current jQuery to change the style (not linked to the model yet):
$("#feedbackList li").click(function () {
$("#feedbackList li.feedbackItem-Highlighted").removeClass("feedbackItem-Highlighted");
$(this).addClass("feedbackItem-Highlighted");
});
What I think I need to add to the ViewModel, but doesn't quite work:
self.updateFeedbackType = function (elementToChangeTo) {
self.feedbackType($(elementToChangeTo).attr("title"));
$("#feedbackList li.feedbackItem-Highlighted").removeClass("feedbackItem-Highlighted");
$(elementToChangeTo).addClass("feedbackItem-Highlighted");
};
This results in feedbackType being turned into an undefined and the visual change not happening.
Where am I going wrong? Thanks for any help!
I think you just needed that function in the definition of the vm.
Here's a jsfiddle that seems to work:
http://jsfiddle.net/gN3HV/
Update: Here's a fiddle which better leverages knockout and properly accomplishes the goal:
http://jsfiddle.net/gN3HV/7/
elementToChangeTo returns the FeedbackViewModel (same as this) and not the element clicked on--the behavior is a bit different than jQuery.
The second argument passed into updateFeedbackType will be an event, so you could use $(event.target) to get a reference to the clicked element.
self.updateFeedbackType = function (view, event) {
var $elementToChangeTo = $(event.target);
self.feedbackType($elementToChangeTo.attr("title"));
$("#feedbackList li.feedbackItem-Highlighted").removeClass("feedbackItem-Highlighted");
$elementToChangeTo.addClass("feedbackItem-Highlighted");
};
However, #daedalus28 has addressed the larger problem, which is that you're not utilizing knockout.js's strengths and are over-complicating the process. You don't really need both to solve this simplistic condition.