Upgrade to ExtJS 3.1.1 - Released 02/08/2010
[extjs.git] / src / data / ScriptTagProxy.js
1 /*!
2  * Ext JS Library 3.1.1
3  * Copyright(c) 2006-2010 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**\r
8  * @class Ext.data.ScriptTagProxy\r
9  * @extends Ext.data.DataProxy\r
10  * An implementation of Ext.data.DataProxy that reads a data object from a URL which may be in a domain\r
11  * other than the originating domain of the running page.<br>\r
12  * <p>\r
13  * <b>Note that if you are retrieving data from a page that is in a domain that is NOT the same as the originating domain\r
14  * of the running page, you must use this class, rather than HttpProxy.</b><br>\r
15  * <p>\r
16  * The content passed back from a server resource requested by a ScriptTagProxy <b>must</b> be executable JavaScript\r
17  * source code because it is used as the source inside a &lt;script> tag.<br>\r
18  * <p>\r
19  * In order for the browser to process the returned data, the server must wrap the data object\r
20  * with a call to a callback function, the name of which is passed as a parameter by the ScriptTagProxy.\r
21  * Below is a Java example for a servlet which returns data for either a ScriptTagProxy, or an HttpProxy\r
22  * depending on whether the callback name was passed:\r
23  * <p>\r
24  * <pre><code>\r
25 boolean scriptTag = false;\r
26 String cb = request.getParameter("callback");\r
27 if (cb != null) {\r
28     scriptTag = true;\r
29     response.setContentType("text/javascript");\r
30 } else {\r
31     response.setContentType("application/x-json");\r
32 }\r
33 Writer out = response.getWriter();\r
34 if (scriptTag) {\r
35     out.write(cb + "(");\r
36 }\r
37 out.print(dataBlock.toJsonString());\r
38 if (scriptTag) {\r
39     out.write(");");\r
40 }\r
41 </code></pre>\r
42  * <p>Below is a PHP example to do the same thing:</p><pre><code>\r
43 $callback = $_REQUEST['callback'];\r
44 \r
45 // Create the output object.\r
46 $output = array('a' => 'Apple', 'b' => 'Banana');\r
47 \r
48 //start output\r
49 if ($callback) {\r
50     header('Content-Type: text/javascript');\r
51     echo $callback . '(' . json_encode($output) . ');';\r
52 } else {\r
53     header('Content-Type: application/x-json');\r
54     echo json_encode($output);\r
55 }\r
56 </code></pre>\r
57  * <p>Below is the ASP.Net code to do the same thing:</p><pre><code>\r
58 String jsonString = "{success: true}";\r
59 String cb = Request.Params.Get("callback");\r
60 String responseString = "";\r
61 if (!String.IsNullOrEmpty(cb)) {\r
62     responseString = cb + "(" + jsonString + ")";\r
63 } else {\r
64     responseString = jsonString;\r
65 }\r
66 Response.Write(responseString);\r
67 </code></pre>\r
68  *\r
69  * @constructor\r
70  * @param {Object} config A configuration object.\r
71  */\r
72 Ext.data.ScriptTagProxy = function(config){\r
73     Ext.apply(this, config);\r
74 \r
75     Ext.data.ScriptTagProxy.superclass.constructor.call(this, config);\r
76 \r
77     this.head = document.getElementsByTagName("head")[0];\r
78 \r
79     /**\r
80      * @event loadexception\r
81      * <b>Deprecated</b> in favor of 'exception' event.\r
82      * Fires if an exception occurs in the Proxy during data loading.  This event can be fired for one of two reasons:\r
83      * <ul><li><b>The load call timed out.</b>  This means the load callback did not execute within the time limit\r
84      * specified by {@link #timeout}.  In this case, this event will be raised and the\r
85      * fourth parameter (read error) will be null.</li>\r
86      * <li><b>The load succeeded but the reader could not read the response.</b>  This means the server returned\r
87      * data, but the configured Reader threw an error while reading the data.  In this case, this event will be\r
88      * raised and the caught error will be passed along as the fourth parameter of this event.</li></ul>\r
89      * Note that this event is also relayed through {@link Ext.data.Store}, so you can listen for it directly\r
90      * on any Store instance.\r
91      * @param {Object} this\r
92      * @param {Object} options The loading options that were specified (see {@link #load} for details).  If the load\r
93      * call timed out, this parameter will be null.\r
94      * @param {Object} arg The callback's arg object passed to the {@link #load} function\r
95      * @param {Error} e The JavaScript Error object caught if the configured Reader could not read the data.\r
96      * If the remote request returns success: false, this parameter will be null.\r
97      */\r
98 };\r
99 \r
100 Ext.data.ScriptTagProxy.TRANS_ID = 1000;\r
101 \r
102 Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, {\r
103     /**\r
104      * @cfg {String} url The URL from which to request the data object.\r
105      */\r
106     /**\r
107      * @cfg {Number} timeout (optional) The number of milliseconds to wait for a response. Defaults to 30 seconds.\r
108      */\r
109     timeout : 30000,\r
110     /**\r
111      * @cfg {String} callbackParam (Optional) The name of the parameter to pass to the server which tells\r
112      * the server the name of the callback function set up by the load call to process the returned data object.\r
113      * Defaults to "callback".<p>The server-side processing must read this parameter value, and generate\r
114      * javascript output which calls this named function passing the data object as its only parameter.\r
115      */\r
116     callbackParam : "callback",\r
117     /**\r
118      *  @cfg {Boolean} nocache (optional) Defaults to true. Disable caching by adding a unique parameter\r
119      * name to the request.\r
120      */\r
121     nocache : true,\r
122 \r
123     /**\r
124      * HttpProxy implementation of DataProxy#doRequest\r
125      * @param {String} action\r
126      * @param {Ext.data.Record/Ext.data.Record[]} rs If action is <tt>read</tt>, rs will be null\r
127      * @param {Object} params An object containing properties which are to be used as HTTP parameters\r
128      * for the request to the remote server.\r
129      * @param {Ext.data.DataReader} reader The Reader object which converts the data\r
130      * object into a block of Ext.data.Records.\r
131      * @param {Function} callback The function into which to pass the block of Ext.data.Records.\r
132      * The function must be passed <ul>\r
133      * <li>The Record block object</li>\r
134      * <li>The "arg" argument from the load function</li>\r
135      * <li>A boolean success indicator</li>\r
136      * </ul>\r
137      * @param {Object} scope The scope (<code>this</code> reference) in which the callback function is executed. Defaults to the browser window.\r
138      * @param {Object} arg An optional argument which is passed to the callback as its second parameter.\r
139      */\r
140     doRequest : function(action, rs, params, reader, callback, scope, arg) {\r
141         var p = Ext.urlEncode(Ext.apply(params, this.extraParams));\r
142 \r
143         var url = this.buildUrl(action, rs);\r
144         if (!url) {\r
145             throw new Ext.data.Api.Error('invalid-url', url);\r
146         }\r
147         url = Ext.urlAppend(url, p);\r
148 \r
149         if(this.nocache){\r
150             url = Ext.urlAppend(url, '_dc=' + (new Date().getTime()));\r
151         }\r
152         var transId = ++Ext.data.ScriptTagProxy.TRANS_ID;\r
153         var trans = {\r
154             id : transId,\r
155             action: action,\r
156             cb : "stcCallback"+transId,\r
157             scriptId : "stcScript"+transId,\r
158             params : params,\r
159             arg : arg,\r
160             url : url,\r
161             callback : callback,\r
162             scope : scope,\r
163             reader : reader\r
164         };\r
165         window[trans.cb] = this.createCallback(action, rs, trans);\r
166         url += String.format("&{0}={1}", this.callbackParam, trans.cb);\r
167         if(this.autoAbort !== false){\r
168             this.abort();\r
169         }\r
170 \r
171         trans.timeoutId = this.handleFailure.defer(this.timeout, this, [trans]);\r
172 \r
173         var script = document.createElement("script");\r
174         script.setAttribute("src", url);\r
175         script.setAttribute("type", "text/javascript");\r
176         script.setAttribute("id", trans.scriptId);\r
177         this.head.appendChild(script);\r
178 \r
179         this.trans = trans;\r
180     },\r
181 \r
182     // @private createCallback\r
183     createCallback : function(action, rs, trans) {\r
184         var self = this;\r
185         return function(res) {\r
186             self.trans = false;\r
187             self.destroyTrans(trans, true);\r
188             if (action === Ext.data.Api.actions.read) {\r
189                 self.onRead.call(self, action, trans, res);\r
190             } else {\r
191                 self.onWrite.call(self, action, trans, res, rs);\r
192             }\r
193         };\r
194     },\r
195     /**\r
196      * Callback for read actions\r
197      * @param {String} action [Ext.data.Api.actions.create|read|update|destroy]\r
198      * @param {Object} trans The request transaction object\r
199      * @param {Object} res The server response\r
200      * @protected\r
201      */\r
202     onRead : function(action, trans, res) {\r
203         var result;\r
204         try {\r
205             result = trans.reader.readRecords(res);\r
206         }catch(e){\r
207             // @deprecated: fire loadexception\r
208             this.fireEvent("loadexception", this, trans, res, e);\r
209 \r
210             this.fireEvent('exception', this, 'response', action, trans, res, e);\r
211             trans.callback.call(trans.scope||window, null, trans.arg, false);\r
212             return;\r
213         }\r
214         if (result.success === false) {\r
215             // @deprecated: fire old loadexception for backwards-compat.\r
216             this.fireEvent('loadexception', this, trans, res);\r
217 \r
218             this.fireEvent('exception', this, 'remote', action, trans, res, null);\r
219         } else {\r
220             this.fireEvent("load", this, res, trans.arg);\r
221         }\r
222         trans.callback.call(trans.scope||window, result, trans.arg, result.success);\r
223     },\r
224     /**\r
225      * Callback for write actions\r
226      * @param {String} action [Ext.data.Api.actions.create|read|update|destroy]\r
227      * @param {Object} trans The request transaction object\r
228      * @param {Object} res The server response\r
229      * @protected\r
230      */\r
231     onWrite : function(action, trans, response, rs) {\r
232         var reader = trans.reader;\r
233         try {\r
234             // though we already have a response object here in STP, run through readResponse to catch any meta-data exceptions.\r
235             var res = reader.readResponse(action, response);\r
236         } catch (e) {\r
237             this.fireEvent('exception', this, 'response', action, trans, res, e);\r
238             trans.callback.call(trans.scope||window, null, res, false);\r
239             return;\r
240         }\r
241         if(!res.success === true){\r
242             this.fireEvent('exception', this, 'remote', action, trans, res, rs);\r
243             trans.callback.call(trans.scope||window, null, res, false);\r
244             return;\r
245         }\r
246         this.fireEvent("write", this, action, res.data, res, rs, trans.arg );\r
247         trans.callback.call(trans.scope||window, res.data, res, true);\r
248     },\r
249 \r
250     // private\r
251     isLoading : function(){\r
252         return this.trans ? true : false;\r
253     },\r
254 \r
255     /**\r
256      * Abort the current server request.\r
257      */\r
258     abort : function(){\r
259         if(this.isLoading()){\r
260             this.destroyTrans(this.trans);\r
261         }\r
262     },\r
263 \r
264     // private\r
265     destroyTrans : function(trans, isLoaded){\r
266         this.head.removeChild(document.getElementById(trans.scriptId));\r
267         clearTimeout(trans.timeoutId);\r
268         if(isLoaded){\r
269             window[trans.cb] = undefined;\r
270             try{\r
271                 delete window[trans.cb];\r
272             }catch(e){}\r
273         }else{\r
274             // if hasn't been loaded, wait for load to remove it to prevent script error\r
275             window[trans.cb] = function(){\r
276                 window[trans.cb] = undefined;\r
277                 try{\r
278                     delete window[trans.cb];\r
279                 }catch(e){}\r
280             };\r
281         }\r
282     },\r
283 \r
284     // private\r
285     handleFailure : function(trans){\r
286         this.trans = false;\r
287         this.destroyTrans(trans, false);\r
288         if (trans.action === Ext.data.Api.actions.read) {\r
289             // @deprecated firing loadexception\r
290             this.fireEvent("loadexception", this, null, trans.arg);\r
291         }\r
292 \r
293         this.fireEvent('exception', this, 'response', trans.action, {\r
294             response: null,\r
295             options: trans.arg\r
296         });\r
297         trans.callback.call(trans.scope||window, null, trans.arg, false);\r
298     },\r
299 \r
300     // inherit docs\r
301     destroy: function(){\r
302         this.abort();\r
303         Ext.data.ScriptTagProxy.superclass.destroy.call(this);\r
304     }\r
305 });