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

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>

Related

Knockout JS failed to update observableArray

So I'm trying to add content to an observable array, but it doesn't update. The problem is not the first level content, but the sub array. It's a small comments section.
Basically I've this function to declare the comments
function comment(id, name, date, comment) {
var self = this;
self.id = id;
self.name = ko.observable(name);
self.date = ko.observable(date);
self.comment = ko.observable(comment);
self.subcomments = ko.observable([]);
}
I've a function to retrieve the object by the id field
function getCommentByID(id) {
var comment = ko.utils.arrayFirst(self.comments(), function (comment) {
return comment.id === id;
});
return comment;
}
This is where I display my comments
<ul style="padding-left: 0px;" data-bind="foreach: comments">
<li style="display: block;">
<span data-bind="text: name"></span>
<br>
<span data-bind="text: date"></span>
<br>
<span data-bind="text: comment"></span>
<div style="margin-left:40px;">
<ul data-bind="foreach: subcomments">
<li style="display: block;">
<span data-bind="text: name"></span>
<br>
<span data-bind="text: date"></span>
<br>
<span data-bind="text: comment"></span>
</li>
</ul>
<textarea class="comment" placeholder="comment..." data-bind="event: {keypress: $parent.onEnterSubComment}, attr: {'data-id': id }"></textarea>
</div>
</li>
</ul>
And onEnterSubComment is the problematic event form
self.onEnterSubComment = function (data, event) {
if (event.keyCode === 13) {
var id = event.target.getAttribute("data-id");
var obj = getCommentByID(parseInt(id));
var newSubComment = new comment(0, self.currentUser, new Date(), event.target.value);
obj.subcomments().push(newSubComment);
event.target.value = "";
}
return true;
};
It's interesting, because when I try the same operation during initialization(outside of any function) it works fine
var subcomment = new comment(self.commentID, "name1", new Date(), "subcomment goes in here");
self.comments.push(new comment(self.commentID, "name2", new Date(), "some comment goes here"));
obj = getCommentByID(self.commentID);
obj.subcomments().push(subcomment);
If anyone can help me with this, cause I'm kind of stuck :(
You need to make two changes:
1st, you have to declare an observable array:
self.subcomments = ko.observableArray([]);
2nd, you have to use the observable array methods, instead of the array methods. I.e. if you do so:
obj.subcomments().push(subcomment);
If subcomments were declared as array, you'd be using the .push method of Array. But, what you must do so that the observable array detect changes is to use the observableArray methods. I.e, do it like this:
obj.subcomments.push(subcomment);
Please, see this part of observableArray documentation: Manipulating an observableArray:
observableArray exposes a familiar set of functions for modifying the contents of the array and notifying listeners.
All of these functions are equivalent to running the native JavaScript array functions on the underlying array, and then notifying listeners about the change

Push to observable array with concrete object or not?

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>

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/

How to modify object on a click

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>

Categories