I have list of item IDs on page load:
var itemIds = [1, 5, 10, 11];
IDs are rendered into list and shown to user. I have function which loads detailed information about item, it returns Promise/A.
function loadInfo(id) { ... }
Loading of info for all items is initiated on page load:
val infos = {};
$.each(itemIds, function(i, id) { infos[id] = loadInfo(id); }
Now the problem itself.
When user clicks on item ID:
if item info is loaded, info must be shown
if item info is not yet loaded, it must be shown when it is loaded
Looks easy:
$('li').click(function() {
var id = $(this).data('item-id');
infos[id].then(function(info) { $('#info').text(info); });
});
But if user cliked on another item before current one is loaded, then I have to cancel handler on current item promise and schedule on new one.
How to properly do it?
I have several working solutions (like maintaining currentItemId variable and checking it in promise handler), but they are ugly. Should I look to reactive programming libraries instead?
Kriomant,
I guess there's a number of ways you could code this. Here's one :
var INFOS = (function(){
var infoCache = {},
promises = {},
fetching = null;
var load = function(id) {
if(!promises[id]) {
promises[id] = $ajax({
//ajax options here
}).done(function(info){
infoCache[id] = info;
delete promises[id];
});
}
return promises[id];
}
var display = function(id, $container) {
if(fetching) {
//cancel display (but not loading) of anything latent.
fetching.reject();
}
if(infoCache[id]) {
//info is already cached, so simply display it.
$container.text(infoCache[id]);
}
else {
fetching = $.Deferred().done(function() {
$container.text(infoCache[id]);
});
load(id).done(fetching.resolve);
}
}
return {
load: load,
display: display
};
})();
As you will see, all the complexity is bundled in the namespace INFOS, which exposes two methods; load and display.
Here's how to call the methods :
$(function() {
itemIds = ["p7", "p8", "p9"];
//preload
$.each(itemIds, function(i, id) { INFOS.load(id); });
//load into cache on first click, then display from cache.
$('li').on('click', function() {
var id = $(this).data('item-id');
INFOS.display(id, $('#info'));
});
});
DEMO (with simulated ajax)
The tricks here are :
to cache the infos, indexed by id
to cache active jqXHR promises, indexed by id
to display info from cache if previously loaded ...
... otherwise, each time info is requested, create a Deferred associated with display of the info, that can be resolved/rejected independently of the corresponding jqXHR.
Related
The case:
I use dojo to request a page and load it into a div ( view ).
The problem:
The content that gets loaded into a form contains a dojo form and relevant objects, textbox, etc... how can I controller these widgets? I believe the current way I am working around the issue is sloppy and could be more refined.
Comments are in the code to help explain the issue I have. Please let me know your thoughts.
function (parser, domAttr, util, ready, dom, on, request, domStyle, registry, TextBox) {
//This prepares the doc main html page We are looking for a click of a menu option to load in thats pages pages content
ready(function () {
//Look for click of menu option
on(dom.byId('steps'), "a:click", function(e) {
event.preventDefault();
//Get the div we are going to load the page into
var view = domAttr.get(this, "data-view");
// function that loads the page contents
load_page(view);
});
});
function load_page(view) {
//First I see if this page already has widgets and destroy them
//We do this so users can toggle between menu items
// If we do not we get id already registered
var widgets = dojo.query("[widgetId]", dom.byId('apply-view')).map(dijit.byNode);
dojo.forEach(widgets, function(w){
w.destroyRecursive();
});
//get the html page we are going to user for the menu item
request.post("/apply_steps/"+view, {
data: {
id: 2
}
}).then(
function(response){
//Add the content and parse the page
var parentNode = dom.byId('apply-view');
parentNode.innerHTML = response;
parser.parse(parentNode);
//This is where it is sloppy
//What I would prefer is to load a new js file the controlls the content that was just loaded
//What happens now is I create a traffic director to tell the code what main function to use
controller_director(view);
},
function(error){
util.myAlert(0, 'Page not found', 'system-alert');
});
}
function controller_director(view) {
//based on the view switch the function
switch(view) {
case 'screening_questions':
screening_questions();
break;
}
}
function screening_questions() {
//Now we are controlling the page and its widgets
// How would I get this info into a seperate js file that i would load along with the ajax call??
ready(function () {
on(dom.byId('loginForm'), "submit", function(e) {
event.preventDefault();
var formLogin = registry.byId('loginForm');
authenticate();
});
});
this.authenticate = function() {
var formLogin = registry.byId('loginForm');
if (formLogin.validate()) return;
}
}
});
I have a kendo grid, and when an item is selected I want to modify the underlying dataitem so i'm doing this ...
selectionChange: function(e)
{
var component = $(this.table).closest('.component');
var grid = this;
var val = !component.hasClass("secondary");
var selection = grid.dataItems(grid.select());
selection.forEach(function () {
this.set("SaleSelected", val);
});
}
I also have 2 buttons that allow me to push items between the 2 grids which do this ...
select: function (e) {
e.preventDefault();
var sender = this;
// get kendo data source for the primary grid
var source = $(sender).closest(".component")
.find(".component.primary")
.find(".details > [data-role=grid]")
.data("kendoGrid")
.dataSource;
// sync and reload the primary grid
source.sync()
.done(function () {
source.read();
my.Invoice.reloadGridData($(sender).closest(".component").find(".component.secondary").find(".details > [data-role=grid]"));
});
return false;
},
deselect: function (e) {
e.preventDefault();
var sender = this;
debugger;
// get kendo data source for the secondary grid
var source = $(sender).closest(".component")
.find(".component.secondary")
.find(".details > [data-role=grid]")
.data("kendoGrid")
.dataSource;
// sync and reload the primary grid
source.sync()
.done(function () {
source.read();
my.Invoice.reloadGridData($(sender).closest(".component").find(".component.primary").find(".details > [data-role=grid]"));
});
return false;
}
Essentially the "selected items" from grid1 can be marked as such on the server then the grids get reloaded to move the items over.
All good I thought, but apparently Kendo has other ideas.
Editing a data item causes its owning grid to rebind losing the selection state resulting in some confusing behaviour for the user.
Is there a way to tell kendo "i'm going to edit this unbound property right now, don't go messing with binding"?
Ok it turns out kendo is a bit of a wierdo and I still have no idea why they insist you call all their "api stuff" to do simple tasks when doing things more directly actually works better.
In my case I removed the selection change call altogether and let kendo handle that, then in my selection button handlers to move the data between grids I updated the properties directly on the data items instead of calling
"item.set("prop", value)" i now have to do "item.prop = value".
The net result is this ...
select: function (e) {
e.preventDefault();
var sender = this;
// get some useful bits
var component = $(sender).closest(".component");
var primaryGrid = component.find(".component.primary").find(".details > [data-role=grid]").data("kendoGrid");
// get the new selection, and mark the items with val
var selection = $(primaryGrid.tbody).find('tr.k-state-selected');
selection.each(function (i, row) {
primaryGrid.dataItem(row).SaleSelected = true;
primaryGrid.dataItem(row).dirty = true;
});
// sync and reload the primary grid
primaryGrid.dataSource.sync()
.done(function () {
primaryGrid.dataSource.read();
component.find(".component.secondary")
.find(".details > [data-role=grid]")
.data("kendoGrid")
.dataSource
.read();
});
return false;
},
deselect: function (e) {
e.preventDefault();
var sender = this;
// get some useful bits
var component = $(sender).closest(".component");
var secondaryGrid = component.find(".component.secondary").find(".details > [data-role=grid]").data("kendoGrid");
// get the new selection, and mark the items with val
var selection = $(secondaryGrid.tbody).find('tr.k-state-selected');
selection.each(function (i, row) {
secondaryGrid.dataItem(row).SaleSelected = false;
secondaryGrid.dataItem(row).dirty = true;
});
// sync and reload the primary grid
secondaryGrid.dataSource.sync()
.done(function () {
secondaryGrid.dataSource.read();
component.find(".component.primary")
.find(".details > [data-role=grid]")
.data("kendoGrid")
.dataSource
.read();
});
return false;
}
kendo appears to be taking any call to item.set(p, v) as a trigger to reload data so avoiding the kendo wrapper and going directly to the item properties allows me direct control of the process.
Moving the code from the selection change event handler to the button click handler also means i only care about that data being right when it actually needs to be sent to the server, something I just need to be aware of.
I don't like this, but it's reasonably clean and the ui shows the right picture even if the underlying data isn't quite right.
My other option would be to create a custom binding but given that the binding would have to result in different results depending on weather it was binding to the primary or the secondary grid I suspect that would be a lot of js code, this feels like the lesser of 2 evils.
I think you can bind the dataBinding event to just a "preventDefault" and then unbind it and refresh at your leisure
var g = $("#myGrid").data("kendoGrid");
g.bind("dataBinding", function(e) { e.preventDefault(); });
then later...
g.unbind("dataBinding");
I have a sortable accordion loaded with a foreach-template loop over a ko.observableArray() named "Tasks".
In the accordion I render the TaskId, the TaskName, and a task Description - all ko.observable().
TaskName and Description is rendered in input/textarea elements.
Whenever TaskName or Description is changed, an item is de-selected, or another item is clicked on, I want to call a function saveEdit(item) to send the updated TaskName and Description to the database via an ajax request.
I need to match the TaskId with the Tasks-array to fetch the actual key/value-pair to send to the saveEdit().
This is the HTML:
<div id="accordion" data-bind="jqAccordion:{},template: {name: 'task-template',foreach: Tasks,afteradd: function(elem){$(elem).trigger('valueChanged');}}"></div>
<script type="text/html" id="task-template">
<div data-bind="attr: {'id': 'Task' + TaskId}" class="group">
<h3><b><span data-bind="text: TaskId"></span>: <input name="TaskName" data-bind="value: TaskName /></b></h3>
<p>
<label for="Description" >Description:</label><textarea name="Description" data-bind="value: Description"></textarea>
</p>
</div>
</script>
This is the binding:
ko.bindingHandlers.jqAccordion = {
init: function(element, valueAccessor) {
var options = valueAccessor();
$(element).accordion(options);
$(element).bind("valueChanged",function(){
ko.bindingHandlers.jqAccordion.update(element,valueAccessor);
});
},
update: function(element,valueAccessor) {
var options = valueAccessor();
$(element).accordion('destroy').accordion(
{
// options put here....
header: "> div > h3"
, collapsible: true
, active: false
, heightStyle: "content"
})
.sortable({
axis: "y",
handle: "h3",
stop: function (event, ui) {
var items = [];
ui.item.siblings().andSelf().each(function () {
//compare data('index') and the real index
if ($(this).data('index') != $(this).index()) {
items.push(this.id);
}
});
// IE doesn't register the blur when sorting
// so trigger focusout handlers to remove .ui-state-focus
ui.item.children("h3").triggerHandler("focusout");
if (items.length) $("#sekvens3").text(items.join(','));
ui.item.parent().trigger('stop');
}
})
.on('stop', function () {
$(this).siblings().andSelf().each(function (i) {
$(this).data('index', i);
});
})
.trigger('stop');
};
};
My first thought was to place the line
$root.SelectedTask( ui.options.active );
in an .on('click') event function where SelectedTask is a ko.observable defined in my viewModel. However, the .on('click') event seems to be called a lot and it's generating a lot of traffic. Also, I canĀ“t quite figure out where to put the save(item) call that sends the selected "item" from Tasks via an ajax-function to the database.
Any help is highly appreciated. Thanks in advance! :)
Whenever TaskName or Description is changed, an item is de-selected, or another item is clicked on, I want to call a function saveEdit(item) to send the updated TaskName and Description to the database via an ajax request.
This sounds like the core of what you want to do. Let's start out with a Task model
function Task (data) {
var self = this;
data = data || {};
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.description = ko.observable(data.description);
}
And then we need our View Model:
function ViewModel () {
var self = this;
self.tasks = ko.observableArray();
self.selectedTask = ko.observable();
self.saveTask = function (task) {
$.ajax({ ... });// ajax call that sends the changed data to the server
};
var taskSubscription = function (newValue) {
self.saveTask(self.selectedTask());
};
var nameSubscription, descriptionSubscription;
self.selectedTask.subscribe(function (newlySelectedTask) {
if (newlySelectedTask instanceof Task) {
nameSubscription =
newlySelectedTask.name.subscribe(taskSubscription);
descriptionSubscription =
newlySelectedTask.description.subscribe(taskSubscription);
self.saveTask(newlySelectedTask);// But why?
}
});
self.selectedTask.subscribe(function (currentlySelectedTask) {
if (currentlySelectedTask instanceof Task) {
nameSubscription.dispose();
descriptionSubscription.dispose();
self.saveTask(currentlySelectedTask);// But why?
}
}, null, 'beforeChange');
}
So what's going on here? Most of this should be pretty self explanatory so I'm just going to focus on the subscriptions. We created a taskSubscription function so we're not constantly having it defined every time the self.selectedTask changes.
We have two subscriber functions. The first fires after the selectedTask's value has changed and the second fires before it changes. In both, we verify that the new value is an instance of a Task object. In the after change subscription, we set up two subscriptions on the name and description properties. Then I capture the return value from the subscription function into two private variables. These are used in the before change function to dispose of those subscriptions so that if those Tasks are ever updated when they're not currently selected, then we don't continue to fire off the saveTask function.
I've also added self.saveTask in each of the subscriptions to the selectedTask observable. I asked why in here because, why save it if we don't know if the value has changed or not? You may be making ajax requests needlessly here.
Also, as demonstrated by this code, you can set up these subscriptions to make ajax requests every time the value changes but that may end up making a LOT of requests. A better option might be to set up functionality in your Task model that can track whether or not it is 'dirty' or not. Meaning one or more of its values have changed that requires updating.
function Task (data) {
var self = this;
// Make a copy of the data object coming in and use this to save previous values
self._data = data = $.extend(true, { id: null, name: null, description: null }, data);
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.description = ko.observable(data.description);
for (var prop in data) {
if (ko.isSubscribable(self[prop])) {
self[prop].subscribe(function (oldValue) {
data[prop] = oldValue;
}, null, 'beforeChange');
}
}
}
Task.prototype.isDirty = function () {
var self = this;
for (var prop in self._data) {
if (ko.isSubscribable(self[prop])) {
if (self._data[prop] !== self[prop]())
return true;
}
}
return false;
};
And of course you need a way to save it, or make it not dirty
Task.prototype.save = function () {
var self = this;
for (var prop in self._data) {
if (ko.isSubscribable(self[prop])) {
self._data[prop] = self[prop]();
}
}
};
Using the same concept you can also create Task.prototype.revert that does the opposite of what .save does. With all this in place, you could forego setting up the subscriptions on the individual name and description properties. I wanted to show that option to just demonstrate how one might want to use the .dispose method on a subscription. But now you can just subscribe to the selectedTask observable ('beforeChange') and see if the currently selected task that you're about to swap out isDirty. If it is, call the saveTask function, and when that completes, call the .save function on the Task so that it is no longer dirty.
This is probably the route I would go in implementing something like this. The beauty of it is, I haven't written a single line of code that has anything to do with the manipulating the View. You can set the selectedTask any way you see fit. What I would do is, bind the selectedTask observable to a click binding on the <h3> element inside of the accordion. That way, every time a user clicks on any of the accordions, it will potentially save the previously selected task (if any of the property values had changed).
Hopefully that addresses your scenario here of trying to save a Task when certain events are triggered.
Today I'm using the built-in cookies of the jsTree in order to preserve user navigations in the tree.
on node click in the tree the user is redirected to the corresponding page in my site and the clicked node is selected/highlighted thanks to the jsTree cookies integration.
Now, I would like to to select/highlight nodes in the tree also based on a navigation among the web site, i.e., a link in the site might also be a node in the tree, for example, a grid of rows that also appears in the tree.
The question is how can I do this 'manually' node selection/highlighting and I also think that I should know from where the user arrived to the page, from the tree or from some other link in the site.
Thanks,
I already built a complete approach for this using jsTree, hashchange event and actual real SEO-able URLs so this would fit into your idea quite simply and you could toss your cookies but not in a bad way. This also works with bookmarking and arriving from a URL as it looks through the nodes then matches the links to select the node. This is best with AJAX though as it should be when possible.
I'm commenting this for you so you can understand it. The working example is here www.kitgui.com/docs that shows all the content.
$(function () {
// used to remove double reload since hash and click are intertwined
var cancelHashChange = false,
// method sets the selector based off of URL input
setSelector = function (path) {
var startIndex = path.indexOf('/docs');
if (startIndex > -1) {
path = path.substr(startIndex);
}
path = path.replace('/docs', '').replace('/', '');
if ($.trim(path) === '') { path = 'overview'; }
return '.' + path;
};
// sets theme without the folders, plain jane
$('.doc-nav').jstree({
"themes": {
"theme": "classic",
"dots": true,
"icons": false
}
}).bind("loaded.jstree", function (event, data) {
// when loaded sets initial state based off of priority hash first OR url
if (window.location.hash) { // if hash defined then set tree state
$.jstree._focused().select_node(selector);
$(setSelector(window.location.hash.substr(1)) + ' a:first').trigger('click');
} else { // otherwise base state off of URL
$.jstree._focused().select_node(setSelector(window.location.pathname));
}
});
// all links within the content area if referring to tree will affect tree
// and jump to content instead of refreshing page
$('.doc-nav a').live('click', function (ev) {
var $ph = $('<div />'), href = $(this).attr('href');
ev.preventDefault();
cancelHashChange = true;
// sets state of hash
window.location = '#' + $(this).attr('href');
$('.doc-content').fadeOut('fast');
// jQuery magic load method gets remote content (John Resig is the man!!!)
$ph.load($(this).attr('href') + ' .doc-content', function () {
cancelHashChange = false;
$('.doc-content').fadeOut('fast', function () {
$('.doc-content').html($ph.find('.doc-content').html()).fadeIn('fast');
});
});
});
// if doc content is clicked and has referring tree content,
// affect state of tree and change tree content instead of doing link
$('.doc-content a').live('click', function (ev) {
ev.preventDefault();
if ($(this).attr('href').indexOf('docs/') > -1) {
$.jstree._focused().select_node(setSelector($(this).attr('href')));
$(setSelector($(this).attr('href')) + ' a:first').trigger('click', false);
}
});
// if back/forward are used, maintain state of tree as if it was being clicked
// refers to previously defined click event to avoid double-duty
// but requires ensuring no double loading
window.onhashchange = function () {
if (cancelHashChange) { cancelHashChange = false; return; }
$.jstree._focused().select_node(setSelector(window.location.hash.substr(1)));
$(setSelector(window.location.hash.substr(1)) + ' a:first').trigger('click', false);
};
$('#top-doc-link').closest('li').addClass('active');
});
Feel free to ask me if you have more questions.
There are menu button ("clients"), tree panel with clients list (sorted by name) and viewer with selected client details. There is also selectionchange action..
My task - on button click switch to client view and select and load details for first client every time button has been clicked. My problem - store is not loaded, how waiting until ext js will autoload data to the store?
my controller code:
me.control({
'#nav-client': {
click: me.onNavClientClick
},
...
'clientlist': {
// load: me.selectClient,
selectionchange: me.showClient
}
});
onNavClientClick: function(view, records) {
var me = this,
content = Ext.getCmp("app-content");
content.removeAll();
content.insert(0, [{xtype: 'clientcomplex'}]);
var first = me.getClientsStore().first();
if (first) {
Ext.getCmp("clientList").getSelectionModel().select(me.getClientsListStore().getNodeById(first.get('clientId')));
}
},
...
Two main questions:
is it good solution in my case? (to select first client in tree panel)
var first = me.getClientsStore().first();
// i use another store to get first record because of i dont know how to get first record (on root level) in TreeStore
...
Ext.getCmp("clientList").getSelectionModel().select(me.getClientsListStore().getNodeById(first.get('clientId')));
i know this code works ok in case of "load: me.selectClient," (but only once),
if i place this code on button click - i see error
Uncaught TypeError: Cannot read property 'id' of undefined
because of me.getClientsListStore() is not loaded.. so how to check loading status of this store and wait some until this store will be completely autoloaded?..
Thank you!
You can listen the store 'load' event. Like this:
...
onNavClientClick: function(view, records) {
var me = this;
// if the store isn't loaded, call load method and defer the 'client view' creation
if (me.getClientsStore.getCount() <= 0) {
me.getClientsStore.on('load', me.onClientsStoreLoad, me, { single : true});
me.getClientsStore.load();
}
else {
me.onClientsStoreLoad();
}
},
onClientsStoreLoad : function () {
var me = this,
content = Ext.getCmp("app-content");
content.removeAll();
content.insert(0, [{xtype: 'clientcomplex'}]);
var first = me.getClientsStore().first();
if (first) {
Ext.getCmp("clientList").getSelectionModel().select(me.getClientsListStore().getNodeById(first.get('clientId')));
}
},
...