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.
Related
I've posted my fiddle here, that has comments with it.
How can I convert/map the AllCustomers array into another array of Customer objects??
I need to push the checked checkboxes objects in to self.Customer(), {CustomerType,checked}
Then I would loop through list of Customer object Array and return an array of all checked Customers - self.CheckedCustomers
function Customer(type, checked)
{
var self = this;
self.CustomerType = ko.observable(type);
self.IsChecked = ko.observable(checked || false);
}
function VM()
{
var self = this;
//dynamically populated - this is for testing puposes
self.AllCustomers = ko.observableArray([
{
code: "001",
name:'Customer 1'
},
{
code: "002",
name:'Customer 2'
},
{
code: "003",
name:'Customer 3'
},
]);
self.selectedCustomers = ko.observableArray([]);
self.Customer = ko.observableArray([]);
//How can I convert the AllCustomers array into an array of Customer object??
//I need to push the checked object in to self.Customer(), {CustomerType,checked}
//uncomment below - just for testing looping through Customer objects
/*
self.Customer = ko.observableArray([
new Customer("001"),
new Customer("002")
]);
*/
// This array should return all customers that checked the box
self.CheckedCustomers = ko.computed(function()
{
var selectedCustomers = [];
ko.utils.arrayForEach(self.Customer(), function (customer) {
if(customer.IsChecked())
selectedCustomers.push(customer);
});
return selectedCustomers;
});
}
ko.applyBindings(new VM());
<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.4.2/knockout-min.js"></script>
<!-- ko foreach: AllCustomers -->
<input type="checkbox" data-bind="value: $data.code, checked:$parent.selectedCustomers" />
<span data-bind="text: $data.name"></span>
<!-- /ko -->
<br />
<h4>selectedCustomers code</h4>
<div data-bind="foreach: selectedCustomers">
<span data-bind="text: $data"></span>
</div>
<h4>Checked boxes are??</h4>
<div data-bind="foreach: CheckedCustomers">
<span data-bind="text: CustomerType"></span>
<span data-bind="text: IsChecked"></span>
<span>,</span>
</div>
<!-- Use this to loop through list of Customer object Array, uncomment below to test-->
<!--
<!-- ko foreach: Customer --
<input type="checkbox" data-bind="checked: IsChecked" />
<span data-bind="text: CustomerType"></span>
<!-- /ko --
-->
You're trying to convert object with properties code and name to an object of properties CustomerType and IsChecked. I'm assuming you want code to be mapped to CustomerType when creating new Customer object.
Here's a working jsfiddle for more or less what you wanted.
https://jsfiddle.net/s9yd0e7o/10/
Added the following code:
self.selectedCustomers.subscribe(function() {
self.Customer.removeAll();
ko.utils.arrayForEach(self.selectedCustomers(), function(item) {
self.Customer.push(new Customer(item, true));
});
});
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 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.
I am having some strange functionality with a virtual Knockout compose using 3 pairs of Views/ViewModels
autoAttendant.js
define(['durandal/app', 'viewmodels/settings/autoAttendant/menu'], function(app, Menu){
return function() {
var self = this;
self.attendant = ko.observable();
self.activate = function() {
self.autoAttendant(new Menu());
};
};
});
autoAttendant.html
<div id="content_pane" class="pushed_right">
<div class="content_box">
<h1>Attendant</h1>
<!-- ko compose: 'viewmodels/settings/autoAttendant/menu' --><!--/ko-->
</div>
</div>
menu.js
define(['durandal/app', 'viewmodels/settings/autoAttendant/menuItem'], function(app, MenuItem) {
return function() {
var self = this;
self.menuItems = ko.observableArray([
new MenuItem('val1', 'label1'),
new MenuItem('val2', 'label2'),
// etc...
]);
};
});
menu.html
<div class="list">
<div class="box_item master">
<!-- html content -->
</div>
<!-- ko foreach: { data: menuItems } -->
<!-- ko compose: 'viewmodels/settings/autoAttendant/menuItem' --><!--/ko-->
<!-- /ko -->
</div>
menuItem.js
define(['durandal/app'], function(app) {
var menuItem = function(val, label, active) {
var self = this;
console.log('val:', val, 'label:', label, 'active:', active); // purely for testing purposes
var _val = val || 'default_val',
_label = label || 'default_label',
_active = active || false;
self.val = ko.observable(_val);
self.label = ko.observable(_label);
self.active = ko.observable(_active);
};
return menuItem;
});
menuItem.html
<div class="level">
<div class="box_item clickable">
<!-- html content -->
</div>
</div>
Together these represent a single page within settings that displays a menu and that menu's sub-items.
Menu and MenuItem must be detached from the attendant View/ViewModel as the menu itself is recursive and a menuItem can link to a sub-menu with its own menuItems.
The problem comes in at the 2nd ko compose. The console.log occurs 3 times and the first 2 it shows the correct passing arguments to the MenuItem constructors in the menu.js:
val: val1 label: label1 active: undefined
At the final console.log print out, the parameters that had been passed are overwritten like so:
val: <!-- ko compose: 'viewmodels/settings/autoAttendant/menuItem' --><!--/ko--> label: Object {model: "viewmodels/settings/autoAttendant/menuItem", bindingContext: L.b.z, activeView: null}
active: undefined
Why does this happen?
The following worked, after thorough research into the source and (more than) a little bit of experimentation:
<!-- ko compose: {view:'settings/autoAttendant/menuItem'} --><!--/ko-->
From Durandal docs on compose
I'm trying to modify an object on a click. Here's what I have.
<form>
<ul class="tabs" data-tabs="tabs" data-bind="template: 'lineTemplate'"></ul>
<div class="pill-content" data-bind="template: 'lineDivTemplate'" ></div>
</form>
<script id="lineTemplate" type="text/html">
{{each(i, line) lines()}}
<li><a data-bind="click: function() { viewModel.setActive(line) }, attr : { href : '#line' + id() }"><span style="font-size: 15px;" data-bind="text : model"/></a></li>
{{/each}}
</script>
var viewModel = {
lines: ko.observableArray([]),
setActive : function(line) {
**//I need to modify this object**
line.activeTab = true;
}
};
$.getJSON("/json/all/lines", { customer_id : customer_id } , function(data) {
ko.mapping.fromJS(data, null, viewModel.lines);
});
ko.applyBindings(viewModel);
Basically when the user clicks the tab I need it to update the model(and eventually the database) that it is the currently active tab. The first way I had was the delete the object modify it and then push it back to the array, but pushing adds it to the end of the array, which I don't want. Thanks for any help.
Typically, you would mantain something like a "selectedTab" or "activeTab" observable.
var viewModel = {
lines: ko.observableArray([]),
activeTab: ko.observable(),
};
viewModel.setActive = function(line) {
this.activeTab(line);
}.bind(viewModel);
Then, you can do any binding that you want against activeTab. In KO 1.3, you could do:
<div data-bind="with: activeTab">
...add some bindings here
</div>
Prior to that you could do:
<script id="activeTmpl">
...add your bindings here
</script>