The situation:
I've extended a grid, and added an onkeydown event to listen for tab or arrow, whcih will allow the user to go to the next "editable" cell. I do this using ...
var grid = $("#" + that.gridId).data('kendoMyExtension');
grid.closeCell(currentCell);
grid.editCell(desiredCell);
Current behavior:
it works as expected however the when the cell closes, it doesn't persist the data (thru the correct binding) to the ViewModel.Field to which it is bound.
... However IFF you hit enter after making your changes, you will persist the changes.
What I've tried:
Manually making the update before I make the focus change (and fire off all those other kendo editing goodies) using
{grid Context}
that.saveRow();
that.dataSource.sync();
however these do not work. and usually end up throwing a undefined error somewhere in the bowels of kendo.
What I want:
Ideally kendo would supply at least one MVVM and kendo extension example (for a grid) that has all the functionality, events, etc. being bound to .. but.. since I will probably not get that asking here I will settle for:
Where does kendo "store" the changes?
What method is used to actually call the update {for a MVVM binding NOT a dataSource}.
Did I miss a binding keyword in the spirit of {one-way, two-way}?
Is kendo based on knockout.. can i use knockout techniques to get around the issues I'm having with kendo.
there are also a lot of other modifications to the display logic on this grid.. I am using a template to determine if the cell should be editable, a template to determine what should be rendered in nonedit & edit mode, and some IOC logic to wire it into the extension... (FYI)
o.k. after stepping thru kendo I found the issues... my datasource didn't have a reader (and it shouldn't afik) so sync and saveRow will not work reliably.
You MUST make sure the row is marked as 'k-edit-row' in your edit template :
{your edit template selector}.closest('tr').addClass("k-grid-edit-row");
if you want to get the saveRow to work, however in my case it wasn't reliable( it would only work if i stepped thru the code, otherwise fail every time...I have no clue as to why even tried the ever popular 'setTimeout(...)' ....)
eventually this is what I ended up doing:
myOnKeyDown:function(e){
var cell = $(e.target).closest("td")[0];
var row = $(e.target).closest("tr")[0];
if (cell != undefined) {
that.forceSync(cell, row, e.target.value);
}
},
forceSync: function (cell, row, value){
/// this is in the extension scope /////
/// Note that this uses functions in the dataSource Scope! ///
var cellFieldName = cell.kendoBindingTarget.target.options.fields.field;
var cellRecordUid = row.getAttribute('data-uid');
var that = $("#" + this.gridId).data('kendoMyExtension').dataSource, idx, length;
var data = that._flatData(that._data);
for (idx = 0, length = data.length; idx < length; idx++) {
if(data[idx].uid ===cellRecordUid){
data[idx][cellFieldName] = value;
}
}
},
... well after the considerable amount of time spent on this I felt that someone else should benefit...
Really hope it helps someone else.. -cheers.
Related
I have a project where I am using the vis.js timeline module as a type of image carousel where I have a start and an end time, plot the events on the timeline, and cycle through them automatically and show the image attached to each event in another container.
I already have this working and use something similar to the following to accomplish this, except one part:
var container = document.getElementById('visualization');
var data = [1,2,3,4,5];
var timeline = new vis.Timeline(container, data);
timeline.on('select', function (properties) {
// do some cool stuff
}
var i = 0;
(function timelapseEvents(i) {
setTimeout(function(){
timeline.setSelection(data[i], {focus: true, animation:true});
if (i < data.length - 1) {
timelapseEvents(i+1);
}
}, 2000);
})(i)
The timeline.setSelection() part above works, the timeline event is selected and focused on. However, the "select" event is NOT triggered. This is verified as working as expected in the documentation (under Events > timeline.select) where it says: Not fired when the method timeline.setSelection() is executed.
So my question is, does anyone know how to use the timeline.setSelection() method and actually trigger the select event? Seems unintuitive to me to invoke the timeline.setSelection()method and not actually trigger the select event.
Spent a few hours on this and came up short. I ended up just taking the code I had in my timeline.on('select', function (properties) { block and turning it into a function and calling it after the timeline.setSelection() call.
Basically, I didn't fix the issue but worked around it. Will keep an eye on this in case anyone actually is able to figure out how to add the select() event to the setSelection() method.
I'm using Sortable to organise lists inside of parent groupings, which themselves are also sortable, similar to the multi example on their demo page, but with text. This works fine and uses code along the lines of:
var globObj = {};
function prepSortCats() {
globObj.subCatsGroup = [];
// Changing parent group order
globObj.sortyMainCats = Sortable.create(catscontainer, {
// options here omitted from example
onUpdate: function( /**Event*/ evt) {
// Send order to database. Works fine.
}
});
// Changing sub list order
globObj.subCatsGroup.forEach.call(document.getElementById('catscontainer').getElementsByClassName('subcatlist'), function(el) {
var sortySubCats = Sortable.create(el, {
// options here from example
onUpdate: function( /**Event*/ evt) {
// Send order to database. Works fine.
}
});
});
}
Which is called when the page loads using:
$(document).ready(function () {
// Sortable
prepSortCats();
});
All good so far. However, the user can introduce new elements into any of the lists (sub or parent). In brief, any new elements added by the user are first added to the database, then the relevant section of the page is refreshed using ajax to pull the updated content from the database and display that. The user sees their newly added items added to one of the existing lists. Ajax call is as follows:
function refreshCategories() {
var loadUrl = "page that pulls lists from database and formats it";
$("#catscontainer")
.html(ajax_load)
.load(loadUrl);
return false;
};
This works fine too. Except, Sortable no longer works. I can't drag any lists. My first thought was to destroy the existing Sortable instances and reinitialize them. Right after I called refreshCategories() I call the following:
if(globObj.sortyMainCats.length !== 0) {
globObj.sortyMainCats.destroy();
}
if(globObj.subCatsGroup.length !== 0) {
var i;
for (i = globObj.subCatsGroup.length - 1; i >= 0; i -= 1) {
globObj.subCatsGroup[i].destroy();
globObj.subCatsGroup.splice(i, 1);
}
}
prepSortCats();
But Sortable still has no effect. I introduced the global object (although controversial) so that I could target the Sortable instances outside their scope but I appear to have overlooked something. Any ideas? Apologies for not providing a working example. As I make various ajax calls to a server, I don't think this is possible here.
Update
I'm clearly misunderstanding some action that's taking place. Well, I should preface that by saying I missed that I could still organise the group/parent lists after reloading a section of the page by ajax with refreshCategories(). This is very much a secondary action to being able to sort the sub lists, which is what I noticed was broken and remains so.
But it did point out that although the entirety of $("#catscontainer") was being replaced with a refreshed version of the lists (and that's all it contains, list elements), Sortable still had some sort of instance running on it. I was under the understanding that it was somehow tied to the elements that were removed. Now I'm a bit more lost on how to get Sortable to either: (a) just start from scratch on the page, performing prepSortCats() as if it was a fresh page load and removing any previous Sortable instance, or (b) getting the remaining Sortable instance, after the ajax call to recognise the added elements.
Update 2
Making some progress.
Through trial and error I've found that right after calling refreshCategories(), calling globObj.sortyMainCats.destroy() is preventing even the group lists from being ordered. Then if I call prepSortCats() after this, I can move them again. But not the sub lists.
This isn't conclusive but it looks like I'm successfully destroying and reinitializing Sortable, which was my goal, but something about the ajax loaded elements isn't working with Sortable.
I was looking for the answer in the wrong place, being sure it was an issue with ajax loaded content and the dom having some inconsistencies with what Sortable expected.
Turns out it was an asynchronous problem. Or, to put it simpler, the section of the page being loaded by ajax wasn't quite ready when Sortable was being asked to be reinitalized.
For anyone having the same trouble, I changed:
$("#catscontainer")
.html(ajax_load)
.load(loadUrl);
to
$("#catscontainer")
.html(ajax_load)
.load(loadUrl, function() {
reinitSortable();
});
where reinitSortable() is just a function that fires off the destroy and prepSortCats() functions similar to how they're displayed above.
I'm a fairly experienced knockout user, so I understand quite a bit of the under the hood stuff, I have however been battling now for a few days trying to figure out how to achieve a given scenario.
I have to create a system that allows observable's within a given knockout component to be able to translate themselves to different languages.
to facilitate this, I've created a custom binding, which is applied to a given element in the following way.
<p data-bind="translatedText: {observable: translatedStringFour, translationToken: 'testUiTransFour'}"></p>
This is in turn attached to a property in my knockout component with a simple standard observable
private translatedStringFour: KnockoutObservable<string> = ko.observable<string>("I'm an untranslated string four....");
(YES, I am using typescript for the project, but TS/JS either I can work with.....)
With my custom binding I can still do 'translatedStringFour("foo")' and it will still update in exactly the same way as the normal text binding.
Where storing the translations in the HTML5 localStorage key/value store, and right at the beginning when our app is launched, there is another component that's responsible, for taking a list of translation ID's and requesting the translated strings from our app, based on the users chosen language.
These strings are then stored in localStorage using the translationToken (seen in the binding) as the key.
This means that when the page loads, and our custom bind fires, we can grab the translationToken off the binding, and interrogate localStorage to ask for the value to replace the untranslated string with, the code for our custom binding follows:
ko.bindingHandlers.translatedText = {
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
},
update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Ask local storage if we have a token by that name
var translatedText = sessionStorage[translationToken];
// Check if our translated text is defined, if it's not then substitute it for a fixed string that will
// be seen in the UI (We should really not change this but this is for dev purposes so we can see whats missing)
if (undefined === translatedText) {
translatedText = "No Translation ID";
}
associatedObservable(translatedText);
ko.utils.setTextContent(element, associatedObservable());
}
}
Now, thus far this works brilliantly, as long as the full cache of translations has been loaded into localStorage, the observables will self translate with the correct strings as needed.
HOWEVER......
Because this translation loader may take more than a few seconds, and the initial page that it's loading on also needs to have some elements translated, the first time the page is loaded it is very possible that the translations the UI is asking for have not yet been loaded into into localStorage, or may be in the process of still loading.
Handling this is not a big deal, I'm performing the load using a promise, so the load takes place, my then clause fires, and I do something like
window.postMessage(...);
or
someElement.dispatchEvent(...);
or even (my favorite)
ko.postbox.publish(...)
The point here is I have no shortage of ways to raise an event/message of some description to notify the page and/or it's components that the translations have finished loading, and you are free to retry requesting them if you so wish.
HERE IN.... Lies my problem.
I need the event/message handler that receives this message to live inside the binding handler, so that the very act of me "binding" using our custom binding, will add the ability for this element to receive this event/message, and be able to retry.
This is not a problem for other pages in the application, because by the time the user has logged in, and all that jazz the translations will have loaded and be safely stored in local storage.
I'm more than happy to use post box (Absolutely awesome job by the way Ryan -- if your reading this.... it's an amazingly useful plugin, and should be built into the core IMHO) but, I intend to wrap this binding in a stand alone class which I'll then just load with requireJs as needed, by those components that need it. I cannot however guarantee that postbox will be loaded before or even at the same instant the binding is loaded.
Every other approach i've tried to get an event listener working in the binding have just gotten ignored, no errors or anything, they just don't fire.
I've tried using the postmessage api, I've tried using a custom event, I've even tried abusing JQuery, and all to no avail.
I've scoured the KO source code, specifically the event binding, and the closest I've come to attaching an event in the init handler is as follows:
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Set up an event handler that will respond to events on session storage, by doing this
// the custom binding will instantly update when a key matching it's translation ID is loaded into the
// local session store
//ko.utils.registerEventHandler(element, 'storage', (event) => {
// console.log("Storage event");
// console.log(event);
//});
ko.utils.registerEventHandler(element, 'customEvent', (event) => {
console.log("HTML5 custom event recieved in the binding handler.");
console.log(event);
});
},
None of this has worked, so folks of the Knockout community.....
How do I add an event handler inside of a custom binding, that I can then trigger from outside that binding, but without depending on anything other than Knockout core and my binding being loaded.
Shawty
Update (About an hour later)
I wanted to add this part, beacuse it's not 100% clear why Regis's answer solves my problem.
Effectively, I was using exactly the same method, BUT (and this is the crucial part) I was targeting the "element" that came in as part of the binding.
This is my mind was the correct approach, as I wanted the event to stick specifically with the element the binding was applied too, as it was said element that I wanted to re-try it's translation once it knew it had the go-ahead.
However, after looking at Regis's code, and comparing it to mine, I noticed he was attaching his event handlers to the "Window" object, and not the "Element".
Following up on this, I too changed my code to use the window object, and everything I'd been attempting started to work.
More's the point, the element specific targeting works too, so I get the actual event, on the actual element, in the actual binding that needs to re-try it's translation.
[EDIT: trying to better answer the question]
I don't really get the whole point of the question, since I don't see how sessionStorage load can be asynchronous.
I supposed therefore sessionStorage is populated from som asynchronous functions like an ajax call to a translation API.
But I don't see what blocks you here, since you already have all the code in your question:
var sessionStorageMock = { // mandatory to mock in code snippets: initially empty
};
var counter = 0;
var attemptTranslation = function() {
setInterval(function() { // let's say it performs some AJAX calls which result is cached in the sessionStorage
var token = "token"; // that should be a collection
sessionStorageMock[token] = "after translation " + (counter++); // we're done, notifying event handlers
window.dispatchEvent(new Event("translation-" + token));
}, 500);
};
ko.bindingHandlers.translated = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var val = valueAccessor();
var token = val.token;
console.log("init");
window.addEventListener("translation-" + token, function() {
if (token && sessionStorageMock[token]) {
val.observable(sessionStorageMock[token]);
}
});
}
};
var vm = function() {
this.aftertranslation = ko.observable("before translation");
};
ko.applyBindings(new vm());
attemptTranslation();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="translated: { observable: aftertranslation, token: 'token' }, text: aftertranslation" />
Forgive me, I am new to backbone, and the MVC javascript concept.
So, I am making a comments system:
createComment: function () {
// create new comment model
var comment = new CommentModel({});
// render form view right after new button
var formview = new FormView({model: comment});
this.$el.after(formview.render().$el);
// add saved model to collection after form was submitted successfully
formview.on('success', this.handleFormSuccess, this);
// finally, return false to stop event propagation
return false;
},
What I can't understand is how to get a list of comments which have been rendered, but have not been sent to the collection. See, I want to ensure that only one comment box is opened at once.
My approach is to do a check to see how many comments are open, and close everyone except the current model.
Using Backbone.js & Underscore, how to get a count of items from the model? seems to give advice for how to do this after the model hits a collection.
I am very new with backbone, so it is entirely possible I am in the exact wrong direction with this.
How do I get the list?
As Joe suggested, I think your problem is this line:
formview.on('success', this.handleFormSuccess, this);
However, I don't think his suggestion (of changing "success" to "sync") will work either, because formview is a View, not a Model or Collection, so it doesn't even have an on method.
What does have an on method is the view's element, so you can do:
formview.$el.on('success', this.handleFormSuccess, this);
Two problems with that though:
jQuery is lame and doesn't let you set the context like that
"success" isn't a form event; you want this code to trigger on "submit"
so to fix those two issues you need to change the line to:
formview.$el.on('submit', _(this.handleFormSuccess).bind(this));
Alternatively you could also call:
_(this).bindAll('handleFormSuccess');
in FormView's initialize, which would make it so that you don't need to bind this.handleFormSuccess):
formview.$el.on('submit', this.handleFormSuccess);
Hope that helps.
I have noticed that when multiple attributes of a Backbone model are set like so
model.set({
att1:val1,
att2:val2
});
two change events are triggered. I was wrongly assuming that only one change event would be triggered after all the attributes had been set.
This might not seem like a problem, but it is when a function is bound to att1 that also uses the value of att2. In other words, when you do this
model.bind('change:att1', func1);
...
func1 = function() {
var att2 = model.get('att2');
}
the variable att2 will be set to the old value of the model's attribute att2.
The question is how to prevent this in an elegant manner. Of course, one option is to set att2 before setting att1 or to bind to att2 (instead of att1), but it seems that this is only a viable option in simple situations. The latter option also assumes that the attributes are set in the order in which they are listed in the set method (which is the case I think).
I have run into this issue several times hence my question. The issue is that it took me some time to realize what was actually happening.
On a final note, just like you can pass {silent:true} as an option of the set method, it would be nice to have an option {group:true} (or something like that) indicating that the change events should only be fired after all the attributes have been set.
In more complex situations i'd go for custom events.
instead of binding to a change:att1 or change:att2 i'd look for a specific custom event, that you trigger after you have set all attributes you wanted to change on the model.
model.set({
att1:val1,
att2:val2
});
model.trigger('contact:updated'); // you can chose your custom event name yourself
model.bind('contact:updated', func1);
...
func1 = function() {
var att2 = model.get('att2');
}
downside on this idea is you have to add a new line of code everywhere you want to trigger the event. if this happens alot you might like to change or override the model.set() to do it for you, but then you're already changing backbone code, don't know how you feel about that.
EDIT
after looking into the sourcecode of backbone, i noticed the change event is triggered right after the change:attribute triggers. (proven by the snippit below)
// Fire `change:attribute` events.
for (var attr in changes) {
if (!options.silent) this.trigger('change:' + attr, this, changes[attr], options);
}
// Fire the `"change"` event, if the model has been changed.
if (!alreadyChanging) {
if (!options.silent && this._changed) this.change(options);
this._changing = false;
}
while the this.change(options); refers to this:
change: function(options) {
this.trigger('change', this, options);
this._previousAttributes = _.clone(this.attributes);
this._changed = false;
},
so if you would be binding to the change event instead of the specific change:argument event, you will arrive at a callback function after both (or all) attributes are changed.
the only downside is, it will trigger on ANY change, even if you change a third or fourth attribute. you need to calculate that in...
small example of how it works on jsfiddle http://jsfiddle.net/saelfaer/qm8xY/