I am trying to understand some slightly odd behavior I am seeing in a page I am making using KnockoutJS. An observable array seems to get duplicate items every time I clear and reapply bindings. The quickest way to understand the problem is to look at this JSFiddle demo. Just click any edit button several times, and watch this list grow!
The heart of the code for this demo is in the following method:
var _bindItemDetail = function (jsonData) {
//clear existing bindings
ko.cleanNode($("#itemdetails").get(0));
// observables in selected item.
_viewModel.SelectedItem(ko.mapping.fromJS(jsonData));
// Apply Bindings
ko.applyBindings(_viewModel.SelectedItem, $("#itemdetails").get(0));
};
The essence of what I am trying to achieve is to create a list and details page in one. The list JSON is fetch on initial page load, and the detail JSON is fetched and bound to the "detail" html whenever an edit link is clicked.
In addition to solving the problem, I am trying to understand the behavior, and learn some lessons about how to clean up stale resources properly when using knockout.
Thanks for any help
The problem is that in your _bindItemDetail function, you are reapplying the bindings on your modified view where you already had replicated the elements.
var _bindItemDetail = function (jsonData) {
//clear existing bindings
ko.cleanNode($("#itemdetails").get(0));
// observables in selected item.
_viewModel.SelectedItem(ko.mapping.fromJS(jsonData));
// Apply Bindings
ko.applyBindings(_viewModel.SelectedItem, $("#itemdetails").get(0));
};
ko.cleanNode() merely removes bindings from the elements, it doesn't revert the view back to its initial state. In general, you should only ever call ko.applyBindings on a set of nodes once. Doing it more than once, is just asking for problems.
Frankly you're not really making good use of knockout. The majority of your code is using jquery to handle all the low-level details. The point of using knockout is to not have to worry about those lower level details.
I've adjusted your fiddle a bit to make better use of knockout with less emphasis on using jquery.
In the view:
Used the click binding to handle your Edit click events.
Used the with binding to conditionally show the editor fields. The stopBindings handler is not needed.
In the view model:
Added the click handler editClicked to the view model.
Removed jquery event bindings.
Removed the ko.cleanNode/ko.applyBindings combo you had when binding items. You shouldn't do that and you just don't need it, knockout will handle all that for you.
Updated fiddle
Related
I am trying to create kind of a drag and drop playground using AngularJS.
The code is on plunker. And to understand the question, mostly you'll need to see the plunker.
https://plnkr.co/edit/i82UOOqHRSJyEPN9293E
I am trying to create node like structures here which can be dragged over to the playground on the right side.
On drag, I am adding the node id to the nodeChain variable in $scope.
Now I am iterating the nodeChain array in UI with ng-repeat where I am drawing the node just to check that the nodes added to the chain are reflecting correctly.
However,
I do not see the nodeChain updating from watchCollection logs as well as the nodes drawn on the playground are not updating when new nodes are added.
Can someone please tell me why the binding is not taking effect?
Thanks in advance!
You need to trigger a $digest cycle to update the values. When the $digest cycle starts, it fires each of the watchers. These watchers check if the current value of the scope model is different from last calculated value. If yes, then the corresponding listener function executes. As a result if you have any expressions in the view they will be updated.
if (!Bounds.within(event.pageX, event.pageY, leftPane)) {
scope.$parent.nodeChain.push(NodeChain.getNodeFromId(attr.nid))
scope.$apply();
}
Working Plunker:https://plnkr.co/edit/SNNsVOZps5Fy35dAQIqk?p=preview
You are getting ng-repeat dupes error in your console because there are duplicate values in your array nodeChain.
If i understand your question correctly, you want to add the dropped nodes in the below white space. Please see the plunkr.
You should add $scope.apply() when you change the models outside the scope.
I have a button in each row, when pressed that row is removed from one table, and added to another table:
var row = $(this).closest("tr").remove().clone()
$('.my-other-table-class').append(row);
and at first it appears to work perfectly, the row is removed from one table and added to another, but when I force a re-draw (by changing the sorting of one of the columns, for example) all the rows are back as they were, and the buttons no longer work. This is the case for both the removed rows, and the rows added to the other table.
Is this because I'm using a .jsp table as a data-source? Would this work correctly if I dynamically added all the rows to the table using JavaScript at load-time, or if I used a modelMap collection as a data-source?
Thanks a lot for any advice.
Solution for future googlers - I have no idea how my google-Fu did not find the answer, I had all the right keywords!
In short:
I was doing this to add:
$('.my-other-table-class').append(row);
And this to remove:
var row = $(this).closest("tr").remove().clone()
but I should've been doing this add:
$('.my-other-table-class').dataTable().fnAddData([$(this).attr("data-val1"), "var2"]);
And this to remove:
$('.my-table').dataTable().fnDeleteRow($(this).closest('tr')[0]);
With more detail:
What I am really doing here is modifying the DOM with JQuery (well duh, but I'm really new at this, remember...) - I figured the DOM was the data-source for my table, so that made sense? The table is redrawn, it rereads the DOM and updates? Well not really.
In order to be dynamic, if you use DOM (in other words HTML, or in my case .jsp rendered as HTML) as your data-source, upon initialization, datatables will copy all that information into a JavaScript array.... so rather then my original thought:
"The DOM is not updating correctly, and that issues is propogating up in to my table... because HTML is static...or something?"
it turns out the actual problem was:
"I was updating the DOM, but the real data source was a JavaScript array I wasn't seeing. So upon redraw, this array was overwriting the DOM and my changes were being lost."
TL;DR: Use the Data-tables API and don't modify the data-source directly, unless you need to.
If the event handlers are one of the issues, it may help to set up your event handler like this:
$("#myTable").on("click", "button.moveMe", function(e) {
e.preventDefault();
var row = $(this).closest("tr").remove().clone()
$('.my-other-table-class').append(row);
});
This will set a handler on the table that has id="myTable". The handler will look for click events on buttons with the "moveMe" class. It will catch the event on rows that are added later, as well as the rows that exist when this hook is created.
Reference: http://api.jquery.com/on/
I'm attempting to input some values into the grid's new row template from outside the grid since selecting this particular input would be more than impractical to get done from inside de webdatagrid.
How can I reach the to be added row via javascript from outside the control? According to the documentation ig_controls.wdgTransaccion.get_behaviors().get_editingCore().get_behaviors().get_rowAdding().get_row(); should do the trick, but it fails to return any row at all
Thank you
Are you sure you calling this from the right place? Can't really tell without more context, however I think i can help you get the functionality you need. Have a look at this sample:
ASP.NET Data Grid: Add New Row - Client Events
The best place I can think of doing this is probably at the time of actual editing happening, so have a look at the EnteringEditMode event and you can do the following inside:
function WebDataGridView_EnteringEditMode(webDataGrid, evntArgs) {
webDataGrid.get_behaviors().get_editingCore().get_behaviors().get_rowAdding().get_row().get_cell("1").set_value("test");
}
Or if you want to do it on your own flow you can grab the grid client object and use the same code as the event above:
var webDataGrid = $find('<%=WebDataGrid1.ClientID%>');
webDataGrid.get_behaviors().get_editingCore().get_behaviors().get_rowAdding().get_row().get_cell("1").set_value("test");
Both of those methods work and allow you to fill in a cell value.
I know we can create our own events in Backbone.js like
object.trigger('myEvent');
I also know there are some default events like the change or change:<attributename>
now i'm wondering, is there a list somewhere of these default events?
i am looking for a specific event that triggers when someone navigates away from a view,
so either in the removing of the view, or perhaps when a route is changed,
I just hope i don't have to hack in my own event and it can't hurt to see a full list of these events for further reference.
update seems like the list did exist already, as benoit describes in the comment above,
it can be found here... http://documentcloud.github.com/backbone/#FAQ-events
so it seems such a list does not exist, well, i took it upon me to go through the annotated source and present the list here for everyone who might ever need it.
i do also make this a community wiki post, so i hope it will be updated by any of you who feel the need to. whether a new Backbone version comes with extra events or i've got something wrong, feel free to edit.
here goes the list of events triggered by backbone itself:
change:<attributename>
fired after using model.set({<attributename>, 'value'});
to indicate the attribute was changed.
change
fired after using model.set({<attributename>, 'value'});
to indicate the model was changed. this fire's on any attribute you change.
destroy
fires after you destroy a model, model.destroy({options});
error
fires after a model is validated and 1 of the validations fails,
if however a specific error callback function is passed in, that one will
be executed instead of the error event.
also fires when you do NOT give an error callback to the following model methods:
model.fetch();
model.save();
model.destroy();
reset
fired when a collection is sorted through the collection.sort({options}); method
also fired when specifically asking to reset a collection through collection.reset(models, {options});
add
fired when a model is added to a collection collection.add(models, {options});
remove
fired when a model is removed from a collection... collection.remove(models, {options});
The particular event you are looking for does not exist, but it can be added easily.
Backbone.View.prototype.remove = function() {
$(this.el).remove();
this.trigger('remove', this);
return this;
}
There is also a 'navigate' event built in, if you are using the Router.
I'm writing an autocomplete custom component as a learning exercise with JSF
2.1.3. The idea (which is probably pretty familiar) is to enter some text into
and input component and present a list box with matching values. The idea is
to have a keyup javascript event on the input which calls jsf.ajax.request()
to update the component. So far I've got a component which I can include like
this:
<mycc:autocomplete id="myauto" searchMethod="#{bean.doSearch}"/>
This renders html like this:
<span id="myauto">
<input type="text" id="myauto_input" name="myauto_input"
onkeyup="com.myco.ajaxRequest(this, event)"/>
<select id="myauto_listbox" name="myauto_listbox">
<option value="1st">First</option>
<option value="2nd">Second</option>
</select>
</span>
The com.myco.ajaxRequest() javascript function (keyup) does this:
jsf.ajax.request(comp, null, {
execute: 'myauto',
render: 'myauto'
});
So because I want to rebuild and rerender the listbox with the suggestions
list, I'm re-rendering the custom component 'myauto'. By specifying execute:
'myauto' the decode() method executes and I can get the input value. By
specifying render: 'myauto' the encode...() methods execute to regenerate
the html.
This is all fine but because I'm rendering the parent of the myauto_input
component I lose input focus every time the keyup event fires.
If I specify something like render: 'myauto_listbox' (I only really want to
rerender the listbox after all) the problem is that the encode...() methods
don't execute, because they're for the custom component as a whole, not just
the listbox. And it would be in one of the encode...() methods that I rebuild
the listbox containing the suggestions.
The component extends UIInput and I generate markup in a separate renderer
(componentFamily = "javax.faces.Input") in the encodeEnd() method (so this
always runs after any supplied converter - not yet implemented). I suppose
that forcing focus from javascript is a horrible hack and to be avoided.
I'm a bit unsure where to go with this, but I suspect that what I'm seeing
indicates that I'm approaching this in the wrong way somehow. If anyone
would be good enough to point me in the right direction I'd greatly appreciate
it.
I've spent some time looking into this and the general issue of losing focus after
an ajax update is fairly common and is described in Jim Driscoll's blog (see
'Keeping Focus').
In the case of my custom component I (think I...) have to update the custom component
itself which is the parent of the input, so I'm going to lose focus as a result of the
ajax update, and that's just the way it is. As such I've looked at what needs to be
done to restore focus, and it seems that in my renderer encode I simply have to
forcibly restore focus to the input, but only when responding to the POST sent from the onkeyup event by jsf.ajax.request. I use jQuery and just calling .focus() isn't
enough because you also have to get the cursor position to the end of any existing
input. This code below seems to work ok:
<script>
jQuery(function($){var cid='#myauto_input';$(cid).focus().focus().click();$(cid).val($(cid).val());});
</script>
(note: .focus().focus().click() required for IE8, just .focus() works on chrome...)
So it looks like the horrible hack has saved the day. I did wonder if there would be
any difference if I used the jQuery ajax routines rather than the jsf ajax library but
I don't suppose it would make any difference.