PDA

View Full Version : Need Several Ajax calls to happen before page is ready



rkuropkat
29 Jun 2010, 9:27 AM
Hi all, first time poster, long time lurker... (okay, not really that long)

I'd like to bring a recent discussion from the premium forums here:

http://www.sencha.com/forum/showthread.php?101330-Need-several-Ajax-calls-to-happen-before-quot-page-is-ready-quot&highlight=Chaining

There were two solutions mentioned, one was maintaining a counter each call bumps and the other was a generic suggestion to "redesign". Additionally, I've seen a lot of discussion around support for synchronous calls which I believe may be motivated less from a need for synchronous calls and more for something like this. As mentioned in the above post, I don't want a synchronous or chained set of calls, what I want is a "wait". Each call has nothing (more or less) to do with each other, but all three need completed before page rendering. IE. the data calls have no dependency on each other, but the PAGE does. It cannot render properly until all [main] data and lookup data is returned. I'd be interested in more specific discussion on whether redesign is a more proper "ExtJS way" because based on my very limited experience so far, it certainly does not seem so.

In my particular case, I am doing three calls. I have one grid displayed with many records. The user double clicks a chosen record and it opens a pop-up window with details to be edited. The details are also displayed in a grid (that is Ajax call 1). The displayed grid (2) has two fields that are foreign keys to two other tables. Obviously, I want to display a more descriptive value than the sequential foreign key value and give the user a chance to change it. I do this with a combo box for each (that is Ajax call 2 and 3). One of the fields with the combo box is also used to sort and group the grid display.

All three components mentioned (grid 2, combo box 1 and 2) are extensions of existing ExtJS components using the "Module" pattern. So the grid is pre-configured with proper values, etc. and the two combo boxes are reusable (and are reused) elsewhere. Additionally, all data store interaction is done through a JSON Reader, Writer and Proxy using the "api" parameter for CRUD access and uses either a Direct or Grouping Store. The two combo boxes are created as part of grid 2's instantiation. Grid 2, combo box 1 (and really combo box 2, though I currently don't bother) are dependent on the selection made in grid 1 to limit their queries appropriately.

So, while I agree, I **could** design this with one AJAX call, it would be both needlessly complex and would not actually follow "the rules." In the design above, each component is distinct and separate as it should be. By using the proxy api configuration parameter, I get a lot of nice, happy magic for keeping the data stores, data base and form in sync, but it all depends on keeping each store separate (again, as they should be). Many of our earlier forms suffer in this area by using directfn instead of the api configuration.

The problem of course, is that if ALL three calls are not completed, in particular, if the two combo boxes are not yet completed before the main grid (2) is, then the page can not be properly rendered. This manifests itself by displaying the "Empty Text" value for whichever combo box was too slow and losing the grouping if it was that combo box that was too slow.

Based on the reading and forum dredging I've done so far, I'm pretty confident the design is "right", but new enough to ExtJS I am willing to be corrected. Note: I want a better DESIGN, not a hack. Better yet, I'd like the "ExtJS way" so I stand a chance of the solution not breaking in the future.

If the best solution is to use a counter as mentioned in the initial thread, I'd appreciate a bit of a noob level discussion on it. Something deeper than "do a counter on the success callback". I'll of course, post more code if/when that is appropriate (this post is long enough already...).

Robert Kuropkat

CrazyEnigma
29 Jun 2010, 11:51 AM
I am not too sure this should be a redesign, neither does it need to be. How much benefit is EXT JS going to gain from implementing this seemingly rare case? You can do it in the workaround and so it should be. This seems to me to be more of the threading kind of thing, and interthreading to wait for another response. I just don't see it.

Chaining and counters would be the solution I would propose, but that's me.

If you want to create a load handler component, All it would really be would be that you register your store to the manager, and expect that all stores are loaded before continuing, and would count that the stores have loaded. It would essentially encapsulate the algorithm.

rkuropkat
29 Jun 2010, 12:28 PM
Sorry, the "redesign" was suggested for the application, not ExtJS. But I find your claim this is a rare case to be somewhat shocking. Are ExtJS applications generally so simplistic they do no client side validation or data entry assist? Seems like a colossal step backwards in UI usability to me. Are truly the majority of combo boxes out there populated by hard coded, client side code rather than generated from a lookup table? In any properly relational database, your data is going to be broken down into "related" tables. Meaningful detail will never be contained in a single table, thus requiring you to query multiple tables in some manner to gather this data up and create a useful, rich interface.

As for your load handler component suggestion (or the counter) could you be more specific. I get the idea, not the implementation. ExtJS components have too many events for me still to keep track of what all they do, so I am unsure which components/events will best serve the purpose of (I guess) stalling the grid rendering.

Thanks

CrazyEnigma
30 Jun 2010, 6:51 AM
Of course combo boxes acquire data from the server. I wouldn't want it any other way. A step backwards is an overstatement. Going synchronous is a step backwards. What does it matter to the user if the form has not finished populating in the moment that it is rendered. Do you force the user to wait until it is completely, completely done? The user is not going to access every single form element at one time. So putting up a load mask wall so that the user doesn't touch the components until it has been completely loaded would be a design choice.

In the case of a component, It would be an array of stores that you can register, and the component creates a load listener within it as the stores are registered, and within it, would count the returns of completed, and if the completed stores have finished and exceed the arrays length, you fire a custom event within the component that your grid's custom component can listen to then remove the mask.

mschwartz
30 Jun 2010, 7:00 AM
window.disable();
load your stores
do any other operations (add components to form, set form field values, etc.)
window.enable();

Animal
30 Jun 2010, 7:38 AM
Set



MyApp.outstandingRequests = 3;


Fire the requests off, and in the callbacks, count down, and when the outstanding count is zero, fire up your app.

rkuropkat
1 Jul 2010, 8:22 AM
All,

Thanks for the various replies. I'll have to read through them to improve my ExtJS knowledge. Sounds lame I know, but simple statements such as "..in the callbacks..." still throw me because I don't know which ones you mean :-/

As for why I need the delay, it's not an issue of making the user wait (though that is an amusing idea) but rather the fact the form won't actually render properly if the data for the combo boxes is not yet returned. eg. one of the foreign keys (combo boxes) is to a person table. I want to display the person's name, not data base id. If, at render time, the id in the main record does not match an id in the combo boxes, it displays the "invalid entry" text. Obviously, if there is no data at all, there will be no match.

I agree synchronizing them would be a step backwards in design, which is why I didn't like the Chaining idea either. It just seemed like a backhanded way to synchronize the calls. However, it seems like there are several approaches to creating a "wait" style processing. FWIW, here's what I did the other night...

As I mentioned, every component I use is an extension of an existing ExtJS component so I can configure and customize as needed. I then build screens from those custom components. So, the grid is an extension of the editor grid panel. The two combo boxes it uses are extensions of the extjs combo box and they each use a data store that extends either Direct Store or Grouping Store. This made it easy for me to add properties and methods so I added a property to each of the data stores called "completed" and set it to false by default. Then, on before load, I set it again to false, and then on load, I set it true. Then, back in my grid, I was able to create an Ext.util.DelayedTask that checks those values and calls itself again if needed.




this.on({
afterlayout : {scope : this, single : true, fn : function () {
var task = new Ext.util.DelayedTask(function () {
if (this.combo1.store.completed && this.combo2.store.completed) {
this.store.load({params : {start : 0, limit : PAGE_SIZE, id: this.ID}, scope : this});
} else {
task.delay(5, null, this);
}
});
task.delay(5, null, this);
}}
});

Thanks again for all the help!

mschwartz
1 Jul 2010, 8:41 AM
store1 = Ext.data.Store({ // or variant
...
listeners: {
load: function() {
store2.load();
}
}
...
});

store2 = Ext.data.Store({ // or variant
...
listeners: {
load: function() {
store3.load();
}
}
...
});

store3 = Ext.data.Store({ // or variant
...
listeners: {
load: function() {
// hide loading mask
// all three stores are loaded!
// do whatever else you want
}
}
...
});

// show loading mask
store1.load();

Animal
1 Jul 2010, 9:20 AM
What's this afterlayout thing??

I thought you said that "Need Several Ajax calls to happen before page is ready"

So that means you want to make multiple calls to



Ext.Ajax.request()


Which takes a success property which is a callback function.

Count down in the success callbacks.

mschwartz
1 Jul 2010, 9:22 AM
What's this afterlayout thing??

I thought you said that "Need Several Ajax calls to happen before page is ready"

So that means you want to make multiple calls to



Ext.Ajax.request()


Which takes a success property which is a callback function.

Count down in the success callbacks.

He talks about grids and comboboxes and data stores in the opening post.

Animal
1 Jul 2010, 9:28 AM
He talks about grids and comboboxes and data stores in the opening post.

You read all that?

That''s more like a whole specification!

Forum questions should be more focused to get help from busy folks.

mschwartz
1 Jul 2010, 9:30 AM
You read all that?

That''s more like a whole specification!

Forum questions should be more focused to get help from busy folks.

Yeah, I read it. I have similar cases in my own application, though I don't see any reason to avoid doing the one Ajax request and telling the instantiated objects to update their stores from the result/response.

rkuropkat
1 Jul 2010, 10:03 AM
hehehe.... My wife says I lecture too much also. Of course, i didn't want to commit the more common posting error that inevitably results in busy folks having to make several follow-up posts saying "...post more detail..." :-)

FWIW, the sample I followed for the grid class was the Saki, "Grid in Card Layout" sample and loading the data in the after layout was intended to defer the store load as late as possible. I notice most of his other samples load the store in an onRender override. I don't have any idea of the pros or cons of each.

Apologies of course if my verbiage was incorrect...

Animal
1 Jul 2010, 10:04 AM
The timing of data loading is not relevant.

It can arrive before the grid has rendered or after. makes no difference.

rkuropkat
1 Jul 2010, 10:14 AM
animal, not true on the timing. The form did not display the data properly. This was not a logical exercise, it was observed in action. The data in the grid displayed wrong. This of course does not discount some other sub-optimal design choice that caused it.

mschwartz as for loading the data separately it is a result of the design (which may be flawed of course). The stores are not loaded by the grid control. The grid control only loads the data it is associated with. The combo boxes load their own data. The grid simply instantiates the combo box, the combo box loads its own data, provides a default renderer and everything. So my grid column model looks like this:



this.setPersonCombo(new ispatial.person.ComboBox());

var cm = new Ext.grid.ColumnModel([
{
dataIndex : 'id',
hidden : true
}, {
header : "Person",
width : 70,
dataIndex : 'person_id',
sortable : true,
editor : this.personCombo,
renderer : this.personCombo.renderer(this.personCombo)
...

mschwartz
1 Jul 2010, 10:33 AM
animal, not true on the timing. The form did not display the data properly. This was not a logical exercise, it was observed in action. The data in the grid displayed wrong. This of course does not discount some other sub-optimal design choice that caused it.

mschwartz as for loading the data separately it is a result of the design (which may be flawed of course). The stores are not loaded by the grid control. The grid control only loads the data it is associated with. The combo boxes load their own data. The grid simply instantiates the combo box, the combo box loads its own data, provides a default renderer and everything. So my grid column model looks like this:



this.setPersonCombo(new ispatial.person.ComboBox());

var cm = new Ext.grid.ColumnModel([
{
dataIndex : 'id',
hidden : true
}, {
header : "Person",
width : 70,
dataIndex : 'person_id',
sortable : true,
editor : this.personCombo,
renderer : this.personCombo.renderer(this.personCombo)
...


I get what you are trying to do. However, you don't have to let the stores load themselves (you have one for the grid, 2 for the combos, right?)

One Ext.Ajax.request can get you an array like:


{ store1_records: [ /* array of records .... */], store2_records: [ /* array of records ... */ ], store3_records: [ ... ] }


Your success function would be something like:


...
success: function(response) {
var data = Ext.decode(response.responseText);
store1.loadData(data.store1_records);
store2.loadData(data.store2_records);
store3.loadData(data.store3_records);
// hide loading mask
// grid.enable()
}
...


How you generate the json result for the ajax call is up to you, and you control the server. If you have to request the three result sets from three other URIs on the server side, so be it.

mschwartz
1 Jul 2010, 10:38 AM
BTW, don't over OO-ify things, which is what it seems like you're doing :)

You're not exposing those lower level things the stores are bound to, just the higher level popup window - it needs to be OO to the rest of the world.

What I'm saying is consider what's going to interact with your objects and only those that are publicly exposed do you need to take care about the API.

Consider any of the ExtJS widgets. They all maintain lower level objects that are (mostly) hidden from the code using instantations of them. You rarely care about a PagingToolbar's TextField widget for the page number, all you care about is there's an API to "getCurrentPageNumber"

Make sense?

rkuropkat
1 Jul 2010, 10:52 AM
nice, definitely something I want to keep in my bag o' tricks for later.

The primary reason that lead me to this design was that the grid doesn't have to care about the internals of the combo boxes, it just says "give me one" and it shows up ready to use. The fact I have to reach deep into the combo object at the moment to insure the store is loaded bothers me, but is functional at least. I like an earlier suggestion in this thread where components could register themselves and that registry could be checked.

Additionally, I don't know much about Direct and I was also hoping to generate some idea of a standard template for our forms. Right now, they are a scattered mish-mash of design and hack and slash. That doesn't really affect much, but keeping things well isolated from the internals of each other is complimentary to those goals.

rkuropkat
1 Jul 2010, 10:54 AM
HAHAHA, Funny that you commented on not over OO-ifying things! That's exactly what I was trying to balance, but as you suggest, probably went too far on anyway (see my post just before this).

Thanks again for the insights!

mschwartz
1 Jul 2010, 11:02 AM
ExtJS forms do work, but they really need a lot of help. We've discussed fixing them, but nobody's ever gotten around to it. ComboBoxes are a real pain to use. If you can use static arrays (like load once from the server, use every time), then it's a LOT easier.

If you look at a lot of the extensions, they show the practice I just talked about. A date+time field has a date picker and some means to enter a time. All you care about is "instantiate datetime field" and before submit, "get datetime field's value" - you don't care about how the date field gets the current time to initialize itself.

The form fields don't have any concept of related fields. One combo might be "choose a state" and another below it might be "choose a city" so they have to work together as a "choose city/state" field (logically).

rkuropkat
1 Jul 2010, 11:18 AM
I agree with you on the combo boxes. That was one of our initial stumbling blocks. Our initial sample forms had a static array for the combo box, so we thought "cool, works, makes sense" Then we tried to get the data from the data base and we initially stood around making confused dog faces at it because we didn't understand what just went wrong. I actually did consider the possibility of loading certain data all at once (like a person data store) and then using filters for forms that needed to narrow it for some reason, but that would take even more work to get right, plus the filters are actually based on other related tables (eg. narrow the list of people to just those in the selected class).

Do you have pointers to some of the past discussions on form improvements? I'd like to look over them to get a better understanding of them.

We have other sets of fields like your city/state combo box example that work together, more like a single, virtual widget than a collection of fields. So the idea of creating completely pre-packaged widgets any of our developers can (and should) use so every form acts consistently is valuable. So I am sort of on a personal Vision Quest to figure out a good way to approach this.

mschwartz
1 Jul 2010, 11:55 AM
I agree with you on the combo boxes. That was one of our initial stumbling blocks. Our initial sample forms had a static array for the combo box, so we thought "cool, works, makes sense" Then we tried to get the data from the data base and we initially stood around making confused dog faces at it because we didn't understand what just went wrong. I actually did consider the possibility of loading certain data all at once (like a person data store) and then using filters for forms that needed to narrow it for some reason, but that would take even more work to get right, plus the filters are actually based on other related tables (eg. narrow the list of people to just those in the selected class).

Do you have pointers to some of the past discussions on form improvements? I'd like to look over them to get a better understanding of them.

We have other sets of fields like your city/state combo box example that work together, more like a single, virtual widget than a collection of fields. So the idea of creating completely pre-packaged widgets any of our developers can (and should) use so every form acts consistently is valuable. So I am sort of on a personal Vision Quest to figure out a good way to approach this.

I know I started a thread about forms in the general discussion forum. Animal posted he'd like to work on a ux version, etc. That might help you find it.

Here's a neat trick (in PHP) that translates to any language (server-side) for setting up things like a global array of info that needs to be loaded in the browser once, like a list of states and cities:



<html>
<head>
<title>blah blah</title>
</head>
<body>
<?php
$state_data = QueryStates(); // however you get from the DB
$states = array();
foreach ($states as $state) {
$states[$state] = QueryCities($state);
}
print "global_states_object = " . json_encode($states) . ";\n";
?>
<!-- rest of HTML here -->
<!-- put your scripts here for speed! -->
</body>
</html>


Now in your JS code, you can access the global_states_object variable which has an array of cities per state:


// this might be in your store's select event handler (when user chooses a state)
// variable 'state' has name of state
var cities = global_states_object[state];
// load 'cities' combobox store with cities
...