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

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/

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.js + ES6 + Underscore Templating

I am trying to get my head around using Knockout.js to create a product cart. Each item outputs a plus and minus button as well as a remove button. My aim is to be able to have the plus and minus increment or decrement the quantity, and the remove button to remove the product. My constraints are that I can't use JQuery.
I've attempted to separate my app concerns so that I have ShopView, ShopModel and ShopItem (ShopItem is the individual item that is pushed to an observable array within the ShopModel). The buttons are rendered, however when clicking on an individual remove/add/minus button and logging the value of this to the console I only am able to see my JS class, not the individual element to remove or alter. Any insight would be greatly appreciated. I've included the bare-bones snippets of the key parts:
index.html
<script type="text/html" id="itemsList">
{{ _.each(items(), function(item) { }}
<a href="#" data-bind="click: minus" class='left-minus'>–</a>
<p class="qty" data-bind="text: item.quantity"></p>
Remove
<a href="#" data-bind="click: plus" class='right-plus'>&plus;</a>
{{ }) }}
</script>
<section data-bind="template: { name: 'itemsList' }" class="items-inner"></section>`
shopView.js
class shopView {
constructor() {
this.setupShop()
}
setupShop(){
this.model.items.push(new Item(97, 'cover-3', '/media/img/cover-3.jpg', 'Issue 5', 'Spring Summer 17', 1, 10));
ko.applyBindings(this.model);
}
}
module.exports = shopView
shopView.js
let ko = require('knockout');
class shopItem{
constructor (id, url, thumbnail, title, edition, quantity, price) {
this.id = ko.observable(id)(),
this.thumbnail = ko.observable(url)(),
this.title = ko.observable(title)(),
this.edition = ko.observable(edition)(),
this.quantity = ko.observable(quantity)(),
this.price = ko.observable(price)();
this.add = function(){
};
this.minus = function(){
};
}
}
module.exports = shopItem;
shopModel
Shop Item
class shopModel {
constructor() {
this.items = ko.observableArray([]);
this.minus = function(item){
console.log(item);
};
this.plus = function(){
};
this.remove = (item) => {
this.items.remove(item);
};
}
}
module.exports = shopModel;
The click binding provides the current $data value to the callback function. But because you are using Underscore for the loop, $data isn't the item. You can change your click binding to something like this:
<a href="#" data-bind="click: function() {minus(item)}" class='left-minus'>–</a>

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>

why does this knockout method receive a form element instead of the object its nested in?

I have this HTML:
<ul class="chat_list" data-bind="foreach: chats">
<li>
<div class="chat_response" data-bind="visible: CommentList().length == 0">
<form data-bind="submit: $root.addComment">
<input class="comment_field" placeholder="Comment…"
data-bind="value: NewCommentText"
/>
</form>
</div>
</li>
</ul>
and this JavaScript:
function ChatListViewModel(chats) {
// var self = this;
self.chats = ko.observableArray(ko.utils.arrayMap(chats, function (chat) {
return { CourseItemDescription: chat.CourseItemDescription,
CommentList: ko.observableArray(chat.CommentList),
CourseItemID: chat.CourseItemID,
UserName: chat.UserName,
ChatGroupNumber: chat.ChatGroupNumber,
ChatCount: chat.ChatCount,
NewCommentText: ko.observable("")
};
}));
self.newChatText = ko.observable();
self.addComment = function (chat) {
var newComment = { CourseItemDescription: chat.NewCommentText(),
ParentCourseItemID: chat.CourseItemID,
CourseID: $.CourseLogic.dataitem.CourseID,
AccountID: $.CourseLogic.dataitem.AccountID,
SystemObjectID: $.CourseLogic.dataitem.CommentSystemObjectID,
SystemObjectName: "Comments",
UserName: chat.UserName
};
chat.CommentList.push(newComment);
chat.NewCommentText("");
};
}
ko.applyBindings(new ChatListViewModel(initialData));
When I go into the debugger it shows that the chat parameter of the addComment() function is a form element instead of a chat object.
Why is this happening?
Because of KO behavior. To pass chat variable to submit handler you may use this:
<ul class="chat_list" data-bind="foreach: chats">
<li>
<div class="chat_response" data-bind="visible: CommentList().length == 0">
<form data-bind="submit: function(form){$root.addComment($data, form)}">
<input class="comment_field" placeholder="Comment…" data-bind="value: NewCommentText" />
</form>
</div>
</li>
</ul>
This is by design. From the Knockout.js docs:
As illustrated in this example, KO passes the form element as a
parameter to your submit handler function. You can ignore that
parameter if you want, but for an example of when it’s useful to have
a reference to that element, see the docs for the ko.postJson utility.
As noted by Serjio you can use currying to pass additional parameters into the function, or you can make use of Knockout's Unobtrusive Event Handling, which allows you to get the entire context associated with the form element.
self.addComment = function (form) {
var context = ko.contextFor(form);
var chat = context.$data;
//rest of your method here
};

Categories