Use single observable for multiple rows in table - javascript

<div class="tbody" data-bind="foreach: displayItems">
<div class="t-row">
<div class="t-cell">
<div class="manage-location-buttons">
<a href="javascript:void(0)">
<i class="fa fa-pencil" aria-hidden="true" data-bind="toggleClick: $component.openEditPopup"></i> Edit
</a>
<div class="edit-table-popup" data-bind="visible: $component.openEditPopup">
<ul>
<li><a data-hash="#locationmanagement/managelocations/locationediting" data-bind="click: goToTab">Locations</a></li>
<li><a data-hash="#locationmanagement/managelocations/events" data-bind="click: goToTab">Events</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
It is my sample of custom table.
On Link click I will show edit-table-popup div like popup. Cause I use only one observable openEditPopup for all items, onclick I see popup for each row.
openEditPopup = ko.observable<boolean>(false);
toggleClick - is custom dirrective, which changes boolean value to opposite
Is it possible to use only one observable but to show popup only for clicked row?

Yes, it's possible.
The click binding sends two arguments to an event handler:
The clicked $data,
The click-event.
If your click handler is an observable, this means it calls the observable like so: yourObservable(data, event)
Knowing that an observable is set by calling it with an argument, you can imagine what happens. Note that knockout ignores the second argument.
The solution would therefore be to change the openEditPopup from containing a bool, to containing a displayItem, and changing the visible binding to:
visible: $component.openEditPopup() === $data
An example:
var vm = {
selected: ko.observable("A"),
items: ["A", "B", "C", "D"]
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<p>Tabs</p>
<div data-bind="foreach: items">
<span data-bind="text: $data, click: $parent.selected"></span>
</div>
<p>Content</p>
<div data-bind="foreach: items">
<div data-bind="visible: $parent.selected() === $data">
<h1 data-bind="text:$data"></h1>
</div>
</div>

If I understand correctly, all your rows are bound to one observable and so when you click the row it is setting it to true and all pop ups are showing up?
If so, this would suggest you have a popup per row? I'd recommend changing this and having one popup that is toggled per row and then set it's data to the selected row. Something like this can be achieved with the code below.
var viewModel = function() {
var rows = ko.observableArray([
{id: 1, name: "gimble"}, {id: 2, name: "bob"}, {id: 3, name: "jess"}
]);
var selectedRow = ko.observable();
function showRowPopup(row)
{
//console.log(row.id);
if(selectedRow() == row)
selectedRow(null);
else
selectedRow(row);
}
return {
rows: rows,
showRowPopup: showRowPopup,
selectedRow: selectedRow
}
}
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: rows">
<div>
<span data-bind="text: id"></span> -
<span data-bind="text: name"></span>
<button data-bind="click: $parent.showRowPopup">Toggle Modal</button>
</div>
</div>
<div data-bind="with: selectedRow">
<h3>My Fake Modal</h3>
<span data-bind="text: id"></span> -
<span data-bind="text: name"></span>
</div>

Related

Run a jQuery function when all knockout.js has rendered [duplicate]

I have implemented a knockout foreach binding, with multiple templates in the same page, one of the example is given here, what I am interested is in finding out when a block finishes rendering, I have tried afterRender and afterAdd, but I guess it runs for each element, and not after the whole loop is finished.
<ul data-bind="foreach: {data: Contacts, afterAdd: myPostProcessingLogic}">
<li>
<div class="list_container gray_bg mrgT3px">
<div class="list_contact_icon"></div>
<div class="contact_name"><span data-bind="text: first_name"></span> <span data-bind="text: last_name"></span></div>
<div class="contact_number"><span data-bind="text: value"></span></div>
<div class="callsms_container">
<a href="#notification-box" class="notifcation-window">
<div class="hover_btn tooltip_call">
<div class="hover_call_icon"></div>
<span>Call</span></div>
</a>
<a class="sendsms" href="#sendsms" rel="#sendsms">
<div class="hover_btn tooltip_sms">
<div class="hover_sms_icon"></div>
<span>SMS</span></div>
</a>
<a href="#">
<div class="hover_more_btn"></div>
</a>
</div>
<!-- close callsms container -->
<div id="notification-box" class="notification-popup">
<img class="btn_close" src="images/box_cross.png" /> <img class="centeralign" src="images/notification_call.png" /> <span>Calling... +44 7401 287366</span> </div>
<!-- close notification box -->
<!-- close list gray bg -->
<div class="tooltip_description" style="display:none" id="disp"> asdsadaasdsad </div>
</div>
</li>
</ul>
I am interested in finding out just the success callback, when a loop finishes rendering.
here is my afterAdd function, which basically attaches some jQuery events, and nothing much.
myPostProcessingLogic = function(elements) {
$(function(){
$(".list_container_callog").hover(function(){
$(".callsms_container", this).stop().animate({left:"0px"},{queue:false,duration:800});
}, function() {
$(".callsms_container", this).stop().animate({left:"-98%"},{queue:false,duration:800});
});
});
}
thanks in advance, and tell me there is a success callback :)
You have the afterRender callback in knockout.js:
foreach: { data: myItems, afterRender: renderedHandler }
Here's documentation.
Inside your handler check whether the length of the rendered collection is equal to the length of the items collection. If not don't execute the full rendered logic that you intend to use.
renderedHandler: function (elements, data) {
if ($('#containerId').children().length === this.myItems().length) {
// Only now execute handler
}
}
Try wrapping the ul with
<div data-bind='template: {afterRender: myPostProcessingLogic }'>
It will only work the first time everything within the template is rendered. But you will only get the one call to myPostProcessingLogic. Here's a fiddle
<div data-bind='template: {afterRender: myPostProcessingLogic }'>
<ul data-bind="foreach: Contacts">
<li>
<div class="list_container gray_bg mrgT3px">
<div class="list_contact_icon"></div>
<div class="contact_name"><span data-bind="text: first_name"></span> <span data-bind="text: last_name"></span></div>
<div class="contact_number"><span data-bind="text: value"></span></div>
<div class="callsms_container">
<a href="#notification-box" class="notifcation-window">
<div class="hover_btn tooltip_call">
<div class="hover_call_icon"></div>
<span>Call</span></div>
</a>
<a class="sendsms" href="#sendsms" rel="#sendsms">
<div class="hover_btn tooltip_sms">
<div class="hover_sms_icon"></div>
<span>SMS</span></div>
</a>
<a href="#">
<div class="hover_more_btn"></div>
</a>
</div>
<!-- close callsms container -->
<div id="notification-box" class="notification-popup">
<img class="btn_close" src="images/box_cross.png" /> <img class="centeralign" src="images/notification_call.png" /> <span>Calling... +44 7401 287366</span> </div>
<!-- close notification box -->
<!-- close list gray bg -->
<div class="tooltip_description" style="display:none" id="disp"> asdsadaasdsad </div>
</div>
</li>
</ul>
</div>
Chuck Schneider's answer above is the best.
I had to use containerless control as the foreach is on a tbody element:
<!-- ko template: {afterRender: SetupCheckboxes } -->
<tbody data-bind="foreach: selectedItems" id="gridBody">
<tr>
<td>
<input type="checkbox" />
</td>
</tr>
</tbody>
<!-- /ko -->
Just wrap the foreach into another foreach loop using Knockout's container less method like this:
<!-- ko foreach:{data: Contacts, afterRender: myPostProcessingLogic }-->
<ul data-bind="foreach: $data}">
<li>
<div class="list_container gray_bg mrgT3px">
<div class="list_contact_icon"></div>
<div class="contact_name"><span data-bind="text: first_name"></span> <span data-bind="text: last_name"></span></div>
<div class="contact_number"><span data-bind="text: value"></span></div>
<div class="callsms_container">
<a href="#notification-box" class="notifcation-window">
<div class="hover_btn tooltip_call">
<div class="hover_call_icon"></div>
<span>Call</span></div>
</a>
<a class="sendsms" href="#sendsms" rel="#sendsms">
<div class="hover_btn tooltip_sms">
<div class="hover_sms_icon"></div>
<span>SMS</span></div>
</a>
<a href="#">
<div class="hover_more_btn"></div>
</a>
</div>
<!-- close callsms container -->
<div id="notification-box" class="notification-popup">
<img class="btn_close" src="images/box_cross.png" /> <img class="centeralign" src="images/notification_call.png" /> <span>Calling... +44 7401 287366</span> </div>
<!-- close notification box -->
<!-- close list gray bg -->
<div class="tooltip_description" style="display:none" id="disp"> asdsadaasdsad </div>
</div>
</li>
</ul>
<!-- /ko -->
The solution above works great. Additionally, if you need to use the foreach "as" option you can do it as so:
data-bind="foreach: { data: myItems, afterRender: renderedHandlet, as: 'myItem'}">
In version 3.5 Knockout provides events to notify when the contents of a node have been bound
HTML
<div data-bind="childrenComplete: bindingComplete">...</div>
JavaScript
function bindingComplete(){
...
}
If you an create event binding expression at a point in the DOM that encapsulates all your child data binding expression, then it's tantamount to a page binding complete event
reference
https://knockoutjs.com/documentation/binding-lifecycle-events.html
I have just recently made a pull request with knockout for them to add two events to define in the binding, unwrap, then call in the correct spots before rendering the items and after all items have rendered. I haven't heard anything back from them but this does exactly what you want to do but you don't have to write hacky code to get it to work. I'm surprised that nobody has made this request before. I used these callbacks that I added to the source to destroy and reinitialize a knockout bound jquery datatable. This was the simplest solution. I have seen many attempts online that try and do it differently but this is the simplest solution.
Pull request: --> pr 1856
ko.bindingHandlers.DataTablesForEach = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var nodes = Array.prototype.slice.call(element.childNodes, 0);
ko.utils.arrayForEach(nodes, function(node) {
if (node && node.nodeType !== 1) {
node.parentNode.removeChild(node);
}
});
return ko.bindingHandlers.foreach.init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var value = ko.unwrap(valueAccessor()),
key = "DataTablesForEach_Initialized";
var newValue = function() {
return {
data: value.data || value,
beforeRenderAll: function(el, index, data) {
if (ko.utils.domData.get(element, key)) {
$(element).closest('table').DataTable().destroy();
}
},
afterRenderAll: function(el, index, data) {
$(element).closest('table').DataTable(value.options);
}
};
};
ko.bindingHandlers.foreach.update(element, newValue, allBindingsAccessor, viewModel, bindingContext);
//if we have not previously marked this as initialized and there is currently items in the array, then cache on the element that it has been initialized
if (!ko.utils.domData.get(element, key) && (value.data || value.length)) {
ko.utils.domData.set(element, key, true);
}
return {
controlsDescendantBindings: true
};
}
};
Knockout Datatables JSFiddle
Try afterRenderAll callback in knockout.js:
foreach: { data: myItems, afterRenderAll: myPostProcessingLogic }

forEach with object properties in KnockoutJS

I have found a few answer regarding this but has not been able to put things together. First day of using Knockout so please let me know if I am thinking about it completely wrong!
I want to represent a list of comments that I have. The comments contain multiple attributes such as {text: ..., score: ..., ...}
I understand that I have a view model
var MatchupViewModel = function(comments) {
this.comments = ko.observableArray(comments);
}
ko.applyBindings(new MatchupViewModel(comments), document.getElementById("leftchat"));
And I understand that the forEach looks somewhat like this:
in jade (which I am using:
#leftchat.chat(data-bind="forEach: comments")
.fullMessage
.content
p.textcontent(data-bind="text: text, visible: text")
img.imagecontent(data-bind="visible: isImage, attr={src: url}")
.scorecontainer
.buttonContainer
p.likeButtonMessage(bind-data="click=voteComment(id, true)") &#x25B2
p.dislikeButtonMessage(bind-data="click=voteComment(id, false)") &#x25BC
p.messageScore(data-bind="text: score")
translated to html:
<div id="leftchat" data-bind="forEach: comments" class="chat">
<div class="fullMessage">
<div class="content">
<p data-bind="text: text, visible: text" class="textcontent"></p><img data-bind="visible: isImage, attr={src: url}" class="imagecontent"/>
</div>
<div class="scorecontainer">
<div class="buttonContainer">
<p bind-data="click=voteComment(id, true)" class="likeButtonMessage">&#x25B2</p>
<p bind-data="click=voteComment(id, false)" class="dislikeButtonMessage">&#x25BC</p>
</div>
<p data-bind="text: score" class="messageScore"></p>
</div>
</div>
</div>
It complains that text is not a function, which is the attribute I had hoped it would have been able to find. How do I work around this?
If you fix the typo's you code should work. forEach should be foreach, and bind-data should be data-bind (unless you made it a custom binding).
Have a look at the example below. When text is a falsy value (null, empty string), the node will be hidden, else its value will be visible. This holds true for observable properties too.
var m = {
comments: [
{text: 'a'},
{text: ko.observable('') },
{text: null},
{text: ko.observable('c')}
]
};
ko.applyBindings(m);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="leftchat" data-bind="foreach: comments" class="chat">
<div data-bind="text: text, visible: text" class="textcontent"></div>
</div>
Try changing this
data-bind="text: text, visible: text"
to this
data-bind="text: $data"
Also maybe change this
data-bind="forEach: comments"
to this
data-bind="forEach: comments()"
that should evaluate properly the underlying array (allthough this should be working just fine without the ())

Dynamically adding items to HTML list

I'm currently building a wizard in asp.net MVC 5 application using knockout. On one of the steps, I need to add items to an html list. that is, text is enter into a textbox and you click the added button, the text is added to the list.
My template:
<script type="text/html" id="addProduct">
<li class="dd-item bordered-inverse animated fadeInDown">
<div class="dd-handle dd-nodrag">
<div class="checkbox">
<label>
<a data-bind="attr: { 'href': '#' }" class='btn btn-xs icon-only success'><i class="fa fa-trash-o"></i></a>
<label data-bind="text: Name, uniqueName: true"></label>
</label>
</div>
</div>
</li>
</script>
<div class="row">
<div class="col-md-6">
<div id="newProduct" class="dd">
<ol class="dd-list" data-bind="template: { name: 'addProduct', foreach: Model.Step1.ProductServices }"></ol>
</div>
</div>
</div>
input textbox:
#Html.TextBoxFor(model => model.Step1.ProductService, new { data_bind = "value: Model.Step1.ProductService")
<button type="button" id="addservice" data-bind="event:{ click: AddProduct }">Add Service/Product</button>
knockout add event:
self.AddProduct = function () {
self.Model.CurrentStep().ProductServices.push({ name: self.Model.CurrentStep().ProductService });
}
self.Model.CurrentStep().ProductServices is a List Product as just one property name.
The code above add an item to the html list and updates the UI, but self.Model.CurrentStep().ProductServices.length is always zero, meaning nothing was added to the Model itself.
Please how can I force it to update the self.Model.CurrentStep().ProductServices?
length is not a property of an observableArray. Check ProductServices().length instead.

Click button to copy text to another div with angularjs

I have a list of Items with different button with them. Plunker
Quick View:
I want something like if I click on any of the buttons, related text will be copy to the div above. Also if I click on the button again it will removed from the Div.Same for each of the buttons. [I added manually one to show how it may display ]
I am not sure how to do that in Angular. Any help will be my life saver.
<div ng-repeat="item in csTagGrp">
<ul>
<li ng-repeat="value in item.csTags">
<div class="pull-left">
<button type="button" ng-class='{active: value.active && !value.old}' class="btn btn-default btn-xs">{{value.keys}}</button>
<span>=</span>
</div>
<div class="pull-left cs-tag-item-list">
<span>{{value.tags}}</span>
</div>
</li>
</ul>
</div>
The simplest thing would be to use $scope.tags object to store selected tags and add/remove them with the scope method similar to this:
$scope.tags = {};
$scope.toggleTag = function(tag) {
if (!$scope.tags[tag]) {
$scope.tags[tag] = true;
}
else {
delete $scope.tags[tag];
}
};
Demo: http://plnkr.co/edit/FrifyCrl0yP0T8l8XO4K?p=info
You can use ng-click to put in your scope the selected value, and then display this value instead of "Win".
http://plnkr.co/edit/IzwZFtRBfSiEcHGicc9l?p=preview
<div class="myboard">
<span>{{selected.tags}}</span>
</div>
...
<button type="button" ng-click="select(value)">{{value.keys}}</button>

Knockout click event in Appframework header not triggering

I am making a Cordova/Phonegap app using Appframework and Knockout js for front and back end. The issue I am running into is occurring in the header of the firstly loaded panel.
The HTML:
<div id="afui">
<div id="content">
<!--CLASS LIST VIEW -->
<div class="panel" title="Classes" id="classList"
data-footer="none" selected="true">
<header>
<div style="float:right" data-bind="click:addClass"
class="button icon add white"></div>
<h1>Classes</h1>
</header>
<ul class="list" data-bind="foreach: classes">
<li data-bind="click:openClass, attr: { id: id, name: name }">
<a href="#categoryList">
<span style="padding-right:20px" data-bind="text: name">
</span>Grade:
</a>
</li>
</ul>
</div>
<!-- CATEGORY VIEW -->
<div class="panel" id="categoryList" title="Class Name">
<header>
<a id="backButton" href="javascript:;" class="button"
style="visibility: visible; ">Back</a>
<h1 data-bind="text: header "></h1>
<div style="float:right" data-bind="click:addCategory"
class="button icon add white">
</div>
</header>
<ul class="list" data-bind="foreach: categories">
<li data-bind="text:name"></li>
</ul>
</div>
</div>
</div>
So in the first panel of id="classList" the addClass button in the header does not get triggered when clicked, tapped, or any other sort of user interaction.
If I tap on a list item with a link to the panel of id="categoryList" and transition to that panel, the button in the top right of that header with event click:addCategory DOES work.
The only way I can get the addClass click event to fire is when I init my models...
ko.applyBindings(classModel, document.getElementById('classList'));
ko.applyBindings(categoryModel, document.getElementById('categoryList'));
is by not specifying the second parameter for the classModel ko binding. Of course this will be a problem, because I have multiple views and need to specify which container each of my models should observe.
VIEW MODELS
ClassModel:
function classInfo(name, id, parent) {
var self = this;
self.name = ko.observable(name);
self.score = ko.observable('');
self.id = ko.observable(id);
self.openClass = function(data, event) {
parent.currentClass(event.currentTarget.name);
};
}
function classListViewModel() {
var self = this;
//class array object
self.classes = ko.observableArray([]);
//current class name
self.currentClass = ko.observable().publishOn("currentClass");
//remove a class from the model
self.removeClass = function(obj) {
//remove the class
};
//add a new class to the model
self.addClass = function() {
//add a new using classInfo object
};
}
So, what is going on here? Is Appframework doing something weird to the header on the main page causing the click:addClass event to not fire?
Any insight/help would be much appreciate, thanks!

Categories