How to modify object on a click - javascript

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>

Related

How do I add an attribute to an object within an observable array in knockout and trigger a notification?

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>

Knockout does not update view with the observable object

Could you please help with this error? I have spent a lot of time on it, but no progress so for. Thanks!!
Error:
VM4705 knockout-debug.js:3326 Uncaught ReferenceError: Unable to process binding "with: function (){return currentCat }"
Message: Unable to process binding "text: function (){return clickCount }"
Message: clickCount is not defined
Expected behavior:
The program should list the cat names in the div with id "catNames" and update the div that has id "cat" with the information of the first cat from the "data" observable array. Next, when you click on different cat names from the names list, it should set the value of "currentCat" to the cat that is clicked on, which in turn should update the div with id "cat".
Link to JsFiddle
Below is my JS code:
var cats = [
{name:'cat1', photo: 'https://s20.postimg.org/owgnoq5c9/cat_1.jpg', clicks: 0 },
{name:'cat2', photo: 'https://s20.postimg.org/f9d5f0ccp/cat_2.jpg', clicks: 0 },
{name:'cat3', photo: 'https://s20.postimg.org/su3xe4s5l/cat_3.jpg', clicks: 0 },
{name:'cat4', photo: 'https://s20.postimg.org/xdg5zna15/cat_4.jpg', clicks: 0 },
{name:'cat5', photo: 'https://s20.postimg.org/78yuqivex/cat_5.jpg', clicks: 0 }
];
function CatRecord(cat){
var self = this;
self.catName = ko.observable(cat.name);
self.imgSrc = ko.observable(cat.photo);
self.clickCount= ko.observable(cat.clicks);
};
var ViewModel = function(){
var self = this;
self.data = ko.observableArray([]);
// data
cats.forEach(function(cat){
self.data.push(new CatRecord(cat));
}); // -- end of for Each
// view
self.currentCat = ko.observable(self.data()[0]);
self.setCurrentCat = function(catIndex){
self.currentCat(self.data()[catIndex]);
};
// actions
self.incrementClicks = function(){
var clickCount = self.currentCat().clickCount();
self.currentCat().clickCount(clickCount + 1);
};
};
ko.applyBindings(new ViewModel());
and the html:
<body>
<div id="catNames">
<ul id="catList" data-bind="foreach: data">
<li data-bind="text: catName, click:$parents[0].currentCat($index()) "></li>
</ul>
</div>
<div id="cat" data-bind="with: currentCat">
<h2 id="clicks" data-bind="text: clickCount"></h2>
<img id="photo" src="" alt="cat photo" data-bind=" click: $parent.incrementClicks,
attr: {src: imgSrc}">
<h4 id="name" data-bind="text: catName"></h4>
<button type="submit">Admin</button>
</div>
</body>
The issue is actually with the click binding inside foreach. It should be:
<ul id="catList" data-bind="foreach: data">
<li data-bind="text: catName, click:$parent.setCurrentCat"></li>
</ul>
The first parameter of setCurrentCat is the cat object which triggered the event. So you don't need the index. You can simply do this:
self.setCurrentCat = function(cat){
self.currentCat(cat);
};
I'm still not sure why you're getting the error for with binding.
Updated fiddle

Looping over an array of different objects in Knockout - Binding Error

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.

KnockoutJS with PagerJS stop working after data bound

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.

How do I used Knockout's "hasfocus" Click-to-Edit (Example 2) on a page that has multiple field:value pairs

How do I used Knockout's "hasfocus" binding in Click-to-Edit (Example 2) on a page that has multiple field:value pairs? I have a page for View Customer Details, and I want to have this capability to edit upon double click.
You need to create an array of PersonViewModels and foreach loop them in the view. To reuse the example on the knockout page the code could look like this:
(function () {
function PersonViewModel(name) {
// Data
this.name = ko.observable(name);
this.editing = ko.observable(false);
// Behaviors
this.edit = function() { this.editing(true) }
}
function ViewModel(personModels) {
this.persons = ko.observableArray(personModels);
}
var personModels = [
new PersonViewModel('Bert'),
new PersonViewModel('James'),
new PersonViewModel('Eddy')
];
ko.applyBindings(new ViewModel(personModels));
})();
And the view:
<div data-bind="foreach: persons">
<p>
Name:
<b data-bind="visible: !editing(), text: name, click: edit"> </b>
<input data-bind="visible: editing, value: name, hasfocus: editing" />
</p>
<p><em>Click the name to edit it; click elsewhere to apply changes.</em></p>
</div>
Here's a jsfiddle demo: http://jsfiddle.net/danne567/gTHpu/

Categories