I'm working on KnockoutJS with PagerJS plugin and found this problem. I don't know if it is related to PagerJS or not but here's the problem.
I use page binding of pager.js with sourceOnShow property and there are child page inside the source contents bound with an observable property of its parent's ViewModel.
When the observable property changes, the child tried to update new data. But after the first value is bound, it seems it was stopped working. I put some logs in between each steps and the result comes as follows:
The result of my sample code displays only the job_id, the rest displays blocks with empty bindings and the console logged only log1 and log2. No other errors logged. As if it stopped working after the first binding.
my code is, for example
the main page
<script src="/js/jobspage.js"></script>
<!-- some elements -->
<div data-bind="page: {
id: 'somepage',
title: 'Some Page',
sourceOnShow: 'template/somepage',
role: 'start'
}"></div>
<div data-bind="page: {
id: 'jobs',
title: 'Jobs',
sourceOnShow: 'template/jobs',
with: JobsPageVM
}"></div>
<div data-bind="page: {
id: 'other',
title: 'Other Page',
sourceOnShow: 'template/otherpage'
}"></div>
the /template/jobs
<div class="jobs" id="main" role="main">
<div class="job-list" data-bind="page: {role: 'start'}">
<!-- ko foreach: jobitems -->
<div data-bind="event: {click: item_clicked}">
<!-- item description -->
<!-- item_clicked will set the selectedItem (observable) property of JobsPageVM -->
</div>
<!-- /ko -->
</div>
<div class="job-info" data-bind="page: {id: 'jobinfo', with: selectedItem}">
<!--ko text: console.log('log1')--><!--/ko-->
<!-- some elements -->
<!--ko text: console.log('log2')--><!--/ko-->
Job ID : <span class="job-value" data-bind="text: job_id"></span>
<!--ko text: console.log('log3')--><!--/ko-->
Job Title : <span class="job-value" data-bind="text: job_title"></span>
<!--ko text: console.log('log4')--><!--/ko-->
</div>
</div>
the jobspage.js
var JobsPageVM = function () {
var self = this;
var dataitems = ko.observableArray();
self.isLoading = ko.observable(true);
self.searchTerm = ko.observable("");
self.jobitems = ko.computed(function () {
var search_input = self.searchTerm().toLowerCase();
if (search_input === "") {
return dataitems();
} else {
return ko.utils.arrayFilter(dataitems(), function (item) {
var data = item.cust_first_name + item.cust_last_name;
return data.search(new RegExp(search_input, "i")) >= 0;
});
}
}, this);
self.selectedItem = ko.observable();
self.branchID = ko.observable(sample_branch_id);
self.getJobList = function (status) {
self.isLoading(true);
if (typeof (status) === "undefined") {
status = "all";
}
$.ajax({
url: "/api/job/branch/" + self.branchID(),
data: {
jobstatus: status
},
success: function (data) {
dataitems(data); // data is an array of object items contains `job_id`, `job_title`, and more
self.isLoading(false);
},
error: function (x, s, e) {
console.log(x, s, e);
self.isLoading(false);
}
});
};
self.item_clicked = function (vm, e) {
self.selectedItem(vm);
pager.navigate('jobs/jobinfo');
};
self.getJobList();
};
*I don't know whether it against the rule or not. This question was asked before but didn't answered, so I deleted and re-asking here. Thanks to #Stijn and #KristianNissen for help refine my question.
I found a kind of workaround, or maybe the solution. But I didn't quite sure the cause of the problem.
Originally, I tried to bind the selectedItem to the page: {with: ...} binding which resulted the problem above. Now I changed the binding of selectedItem with the element itself instead of inside page: binding.
I changed from this :
<div class="job-info" data-bind="page: {id: 'jobinfo', with: selectedItem}">
To this :
<div class="job-info" data-bind="page: {id: 'jobinfo'}, with: selectedItem">
And it seems to work fine now.
Related
With Knockout.js I have an observable array in my view model.
function MyViewModel() {
var self = this;
this.getMoreInfo = function(thing){
var updatedSport = jQuery.extend(true, {}, thing);
updatedThing.expanded = true;
self.aThing.theThings.replace(thing,updatedThing);
});
}
this.aThing = {
theThings : ko.observableArray([{
id:1, expanded:false, anotherAttribute "someValue"
}])
}
}
I then have some html that will change depending on the value of an attribute called "expanded". It has a clickable icon that should toggle the value of expanded from false to true (effectively updating the icon)
<div data-bind="foreach: aThing.theThings">
<div class="row">
<div class="col-md-12">
<!-- ko ifnot: $data.expanded -->
<i class="expander fa fa-plus-circle" data-bind="click: $parent.getMoreInfo"></i>
<!-- /ko -->
<!-- ko if: $data.expanded -->
<span data-bind="text: $data.expanded"/>
<i class="expander fa fa-minus-circle" data-bind="click: $parent.getLessInfo"></i>
<!-- /ko -->
<span data-bind="text: id"></span>
(<span data-bind="text: name"></span>)
</div>
</div>
</div>
Look at the monstrosity I wrote in the getMoreInfo() function in order to get the html to update. I am making use of the replace() function on observableArrays in knockout, which will force a notify to all subscribed objects. replace() will only work if the two parameters are not the same object. So I use a jQuery deep clone to copy my object and update the attribute, then this reflects onto the markup. My question is ... is there a simpler way to achieve this?
I simplified my snippets somewhat for the purpose of this question. The "expanded" attribute actually does not exist until a user performs a certain action on the app. It is dynamically added and is not an observable attribute in itself. I tried to cal ko.observable() on this attribute alone, but it did not prevent the need for calling replace() on the observable array to make the UI refresh.
Knockout best suits an architecture in which models that have dynamic properties and event handlers are backed by a view model.
By constructing a view model Thing, you can greatly improve the quality and readability of your code. Here's an example. Note how much clearer the template (= view) has become.
function Thing(id, expanded, name) {
// Props that don't change are mapped
// to the instance
this.id = id;
this.name = name;
// You can define default props in your constructor
// as well
this.anotherAttribute = "someValue";
// Props that will change are made observable
this.expanded = ko.observable(expanded);
// Props that rely on another property are made
// computed
this.iconClass = ko.pureComputed(function() {
return this.expanded()
? "fa-minus-circle"
: "fa-plus-circle";
}, this);
};
// This is our click handler
Thing.prototype.toggleExpanded = function() {
this.expanded(!this.expanded());
};
// This makes it easy to construct VMs from an array of data
Thing.fromData = function(opts) {
return new Thing(opts.id, opts.expanded, "Some name");
}
function MyViewModel() {
this.things = ko.observableArray(
[{
id: 1,
expanded: false,
anotherAttribute: "someValue"
}].map(Thing.fromData)
);
};
MyViewModel.prototype.addThing = function(opts) {
this.things.push(Thing.fromData(opts));
}
MyViewModel.prototype.removeThing = function(opts) {
var toRemove = this.things().find(function(thing) {
return thing.id === opts.id;
});
if (toRemove) this.things.remove(toRemove);
}
var app = new MyViewModel();
ko.applyBindings(app);
// Add stuff later:
setTimeout(function() {
app.addThing({ id: 2, expanded: true });
app.addThing({ id: 3, expanded: false });
}, 2000);
setTimeout(function() {
app.removeThing({ id: 2, expanded: false });
}, 4000);
.fa { width: 15px; height: 15px; display: inline-block; border-radius: 50%; background: green; }
.fa-minus-circle::after { content: "-" }
.fa-plus-circle::after { content: "+" }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="foreach: things">
<div class="row">
<div class="col-md-12">
<i data-bind="click: toggleExpanded, css: iconClass" class="expander fa"></i>
<span data-bind="text: id"></span> (
<span data-bind="text: name"></span>)
</div>
</div>
</div>
I'm trying to render a different section of a page and apply the appropriate bindings for different items contained within a single array. Each item in the array could have a different structure / properties.
As an example we could have 3 different question types, the data associated with that question could be in a different format.
JSON Data
var QuestionTypes = { Textbox: 0, Checkbox: 1, Something: 2 }
var QuestionData = [
{
Title: "Textbox",
Type: QuestionTypes.Textbox,
Value: "A"
},
{
Title: "Checkbox",
Type: QuestionTypes.Checkbox,
Checked: "true"
},
{
Title: "Custom",
Type: QuestionTypes.Something,
Something: { SubTitle : "Something...", Description : "...." }
}
];
JavaScript
$(document).ready(function(){
ko.applyBindings(new Model(QuestionData), $("#container")[0]);
})
function QuestionModel(data){
var self = this;
self.title = ko.observable(data.Title);
self.type = ko.observable(data.Type);
self.isTextbox = ko.computed(function(){
return self.type() === QuestionTypes.Textbox;
});
self.isCheckbox = ko.computed(function(){
return self.type() === QuestionTypes.Checkbox;
});
self.isSomething = ko.computed(function(){
return self.type() === QuestionTypes.Something;
});
}
function Model(data){
var self = this;
self.questionData = ko.observableArray(ko.utils.arrayMap(data, function(question){
return new QuestionModel(question);
}));
}
HTML
<div id="container">
<div data-bind="foreach: questionData">
<h1 data-bind="text: title"></h1>
<!-- ko:if isTextbox() -->
<div data-bind="text: Value"></div>
<!-- /ko -->
<!-- ko:if isCheckbox() -->
<div data-bind="text: Checked"></div>
<!-- /ko -->
<!-- ko:if isSomething() -->
<div data-bind="text: Something">
<h1 data-text: SubTitle></h1>
<div data-text: Description></div>
</div>
<!-- /ko -->
</div>
</div>
The bindings within the if conditions get applied whether the condition if true / false. Which causes JavaScript errors... as not all of the objects within the collection have a 'Value' property etc.
Uncaught ReferenceError: Unable to process binding "foreach: function (){return questionData }"
Message: Unable to process binding "text: function (){return Value }"
Message: Value is not defined
Is there any way to prevent the bindings from being applied to the wrong objects?
Conceptual JSFiddle: https://jsfiddle.net/n2fucrwh/
Please check out the Updated Fiddler without changing your code.Only added $data in side the loop
https://jsfiddle.net/n2fucrwh/3/
<!-- ko:if isTextbox() -->
<div data-bind="text: $data.Value"></div>
<!-- /ko -->
<!-- ko:if isCheckbox() -->
<div data-bind="text: $data.Checked"></div>
<!-- /ko -->
<!-- ko:if isSomething() -->
<div data-bind="text: $data.Something"></div>
<!-- /ko -->
Inside the loop you need provide $data.Value.It seems to Value is the key word in knockout conflicting with the binding.
First of all your "QuestionModel" has no corresponding properties: you create "type" and "title" fields only from incoming data.
Proposed solution:
You can use different templates for different data types.
I've updated your fiddle:
var QuestionTypes = { Textbox: 0, Checkbox: 1, Something: 2 }
var QuestionData = [
{
Title: "Textbox",
Type: QuestionTypes.Textbox,
templateName: "template1",
Value: "A"
},
{
Title: "Checkbox",
Type: QuestionTypes.Checkbox,
templateName: "template2",
Checked: "true"
},
{
Title: "Custom",
Type: QuestionTypes.Something,
templateName: "template3",
Something: "Something"
}
];
$(document).ready(function(){
ko.applyBindings(new Model(QuestionData), $("#container")[0]);
})
function QuestionModel(data){
var self = this;
self.title = ko.observable(data.Title);
self.type = ko.observable(data.Type);
self.data = data;
}
function Model(data){
var self = this;
self.questionData = ko.observableArray(ko.utils.arrayMap(data, function(question){
return new QuestionModel(question);
}));
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script type="text/html" id="template1">
<div data-bind="text: Value"></div>
</script>
<script type="text/html" id="template2">
<div data-bind="text: Checked"></div>
</script>
<script type="text/html" id="template3">
<div data-bind="text: Something"></div>
</script>
<div id="container">
<div data-bind="foreach: questionData">
<h1 data-bind="text: title"></h1>
<!-- ko with: data -->
<!-- ko template: templateName -->
<!-- /ko -->
<!-- /ko -->
</div>
</div>
In the above edition you can get rid of "QuestionTypes".
Update 1
Of course, you can calculate template name from the question type.
Update 2
Explanation of the cause of errors. If you check original view model:
function QuestionModel(data){
var self = this;
self.title = ko.observable(data.Title);
self.type = ko.observable(data.Type);
self.isTextbox = ko.computed(function(){
return self.type() === QuestionTypes.Textbox;
});
self.isCheckbox = ko.computed(function(){
return self.type() === QuestionTypes.Checkbox;
});
self.isSomething = ko.computed(function(){
return self.type() === QuestionTypes.Something;
});
}
You can see, that "QuestionModel" has following properties: "title", "type", "isTextbox", "isCheckbox" and "isSomething".
So, if you will try bind template to "Value", "Checked" or "Something" you will get an error because view model does not contain such a property.
Changing binding syntax to the
<div data-bind="text: $data.Value"></div>
or something similar eliminates the error, but always will display nothing in this case.
How is this:
var Tag = function (data) {
this.name = ko.observable(data.name);
}
//////
self.tags.push(new Tag({name: self.newTagName()}));
different from just this:
self.tags.push({name: self.newTagName()});
I picked up the first form a tutorial and I start learning knockout, but it confused me, and I have tracked down the logic to the second option.
What are the pros for the first one?
Well Both are same when coming to the pushing part but there is a big difference between both as you are pushing a observable in Case-1 were as in other case you trying to assign a value to name .
Performance perspective i don't think it makes a difference . Case-1 is readable and maintainable .
View :
Type 1: Not a observable (Two way binding doesn't exist)
<div data-bind="foreach:tags1">
<input type="text" data-bind="value:name" />
</div>
Type 2: Observable ( Two way binding )
<div data-bind="foreach:tags2">
<input type="text" data-bind="value:name" />
</div>
ViewModel:
var vm = function(){
var self=this;
self.tags1=ko.observableArray();
self.newTagName=ko.observable('Hi there');
self.tags1.push({name: self.newTagName()}); //you just pushing plane text
var Tag = function (data) {
this.name = ko.observable(data.name);
}
self.tags2=ko.observableArray();
self.tags2.push(new Tag({name: self.newTagName()}));
}
ko.applyBindings(new vm());
Working fiddle here
Quick fix to make first case to work do something like this self.tags1.push({name: ko.observable(self.newTagName())})
Basically you would use observables only when the state of the viewmodel property is dynamic, and changes in response to user 'input' (events). For example, if you had a list toolbar with up, down, add and remove buttons, you could have the following JS in your viewmodel:
this.toolbar = [
{name: 'add', action: this.add, icon: 'plus'},
{name: 'remove', action: this.remove, icon: 'close'},
{name: 'up', action: this.moveUp, icon: 'arrow-up'},
{name: 'down', action: this.moveUp, icon: 'arrow-down'}
];
And the following HTML:
<span data-bind="foreach: toolbar">
<button type="button" data-bind="attr: { title: name }, click: action">
<i data-bind="attr: { class: 'fa fa-' + icon}"></i>
</button>
</span>
IE the previous UI requires only one-way binding (model=>view); the buttons will not change.
However, suppose we would add a button to open/ close the details of each list item. This button has a state: open or closed. For this purpose we need to add an observable which holds a boolean in the button object. We also want to change the icon from + to -, and vice-versa on open/close, so 'icon' will be a computed property here, like so:
var toggleButton = {name: 'toggle'};
toggleButton.state = ko.observable(false); // closed by default
toggleButton.action = function() { toggleButton.state(!toggleButton.state()); };
toggleButton.icon = ko.computed(function() {
return toggleButton.state() ? 'minus' : 'plus';});
this.toolbar.push(toggleButton);
And the modified HTML:
<span data-bind="foreach: toolbar">
<button type="button" data-bind="attr: { title: name }, click: action">
<i data-bind="attr: { class: 'fa fa-' + ko.unwrap(icon) }"></i>
</button>
</span>
As for the "what are the pros of regular objects/properties": they are static, so you would use them eg, for a unique "ID" property which never changes after creation. Performance-wise I have had some trouble only when an observable array contains many many items with many many observable properties.
Using constructor functions is handy (vs object literals) when your objects need their own scope, or if you have many of them to share prototype methods, or even, to automate JSON data mapping.
var app = function() {
this.add = this.remove = this.moveUp = this.moveDown = function dummy() { return; };
this.toolbar = [
{name: 'add', action: this.add, icon: 'plus'},
{name: 'remove', action: this.remove, icon: 'close'},
{name: 'up', action: this.moveUp, icon: 'arrow-up'},
{name: 'down', action: this.moveUp, icon: 'arrow-down'}
];
var toggleButton = {name: 'toggle'};
toggleButton.state = ko.observable(false); // closed by default
toggleButton.action = function() { toggleButton.state(!toggleButton.state()); };
toggleButton.icon = ko.computed(function() { return toggleButton.state() ? 'minus' : 'plus';});
this.toolbar.push(toggleButton);
}
ko.applyBindings(new app());
.closed { overflow: hidden; left: -2000px; }
.open { left: 0; }
div { transition: .3s all ease-in-out; position: relative;}
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<i>( only the last (toggle) button working for demo )</i>
<span data-bind="foreach: toolbar">
<button type="button" data-bind="attr: { title: name }, click: action">
<i data-bind="attr: { class: 'fa fa-' + ko.unwrap(icon) }"></i>
</button>
</span>
<h4>Comments</h4>
<div data-bind="css: { 'open': toolbar[4].state, 'closed': !toolbar[4].state() }">
Support requests, bug reports, and off-topic comments will be deleted without warning.
Please do post corrections and additional information/pointers for this article below. We aim to move corrections into our documentation as quickly as possible. Be aware that your comments may take some time to appear.
If you need specific help with your account, please contact our support team.
</div>
I'm using knockoutjs to bind data, and I want to append data bind in one element.
Here is my code :
html:
<div id="wrapper">
<div data-bind="foreach: people">
<h3 data-bind="text: name"></h3>
<p>Credits: <span data-bind="text: credits"></span></p>
</div>
Javascript code:
function getData(pageNumber)
{
//code get data
//binding
ko.applyBindings({ peopla: obj }, document.getElementById('wrapper'));
}
In the first time the pageNumber is 1, then I call getData(1), and I want show more data in page 2 I will call getData(2), and in page 2 data will be show more in wrapper element like append in jquery.
If I use normal jquery I can call some like that
$("#wrapper").append(getData(2));
So I don't know how to use knockout bind more data in one elemnt
Try the following script, this sort of simulates how you can append data to your array by replacing existing data or adding on to it. Hope it helps
function myModel() {
var self = this;
self.people = ko.observableArray([{
name: 'Page 1 data',
credits: 'credits for page 1'
}]);
var i = 2;
self.getData = function () {
var returnedData = [{
name: 'Page ' + i + ' Name',
credits: 'credits for page ' + i
}];
self.people(returnedData);
i++;
}
self.getData2 = function () {
var returnedData = {
name: 'Page ' + i + ' Name',
credits: 'credits for page ' + i
};
self.people.push(returnedData);
i++;
}
}
ko.applyBindings(new myModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="wrapper">
<div data-bind="foreach: people">
<h3 data-bind="text: name"></h3>
<p>Credits: <span data-bind="text: credits"></span>
</p>
</div>
<button data-bind="click: getData">Simulate get data (Replaces current data)</button>
<button data-bind="click: getData2">Append to the same array (Adds to existing array)</button>
I am new to Knockout and have been trying to follow code examples and the documentation, but keep running into an issue. My data bindings printing the Knockout observable function, not the actual values held by my observable fields. I can get the value if I evaluate the field using (), but if you do this you do not get any live data-binding / updates.
Below are some code snippets from my project that are directly related to the issue I am describing:
HTML
<div class="col-xs-6">
<div data-bind="foreach: leftColSocialAPIs">
<div class="social-metric">
<img data-bind="attr: { src: iconPath }" />
<strong data-bind="text: name"></strong>:
<span data-bind="text: totalCount"></span>
</div>
</div>
</div>
Note: leftColSocialAPIs contains an array of SocialAPIs. I can show that code too if needed.
Initializing the totalcount attribute
var SocialAPI = (function (_super) {
__extends(SocialAPI, _super);
function SocialAPI(json) {
_super.call(this, json);
this.totalCount = ko.observable(0);
this.templateName = "social-template";
}
SocialAPI.prototype.querySuccess = function () {
this.isLoaded(true);
appManager.increaseBadgeCount(this.totalCount());
ga('send', 'event', 'API Load', 'API Load - ' + this.name, appManager.getRedactedURL());
};
SocialAPI.prototype.toJSON = function () {
var self = this;
return {
name: self.name,
isActive: self.isActive(),
type: "social"
};
};
return SocialAPI;
})(API);
Updating totalcount attribute for LinkedIn
var LinkedIn = (function (_super) {
__extends(LinkedIn, _super);
function LinkedIn(json) {
json.name = "LinkedIn";
json.iconPath = "/images/icons/linkedin-16x16.png";
_super.call(this, json);
}
LinkedIn.prototype.queryData = function () {
this.isLoaded(false);
this.totalCount(0);
$.get("http://www.linkedin.com/countserv/count/share", { "url": appManager.getURL(), "format": "json" }, this.queryCallback.bind(this), "json").fail(this.queryFail.bind(this));
};
LinkedIn.prototype.queryCallback = function (results) {
if (results != undefined) {
results.count = parseInt(results.count);
this.totalCount(isNaN(results.count) ? 0 : results.count);
}
this.querySuccess();
};
return LinkedIn;
})(SocialAPI);
In the <span data-bind="text: totalCount"></span>, I expect to see a number ranging from 0-Integer.MAX. Instead I see the following:
As you can see, its outputting the knockout function itself, not the value of the function. Every code example I've seen, including those in the official documentation, says that I should be seeing the value, not the function. What am I doing wrong here? I can provide the full application code if needed.
Not sure, but KO view models obviously tend to bind own (not inherited through prototypes) observable properties only. So you should rewrite your code to supply totalCount observable for every social network separately.