0001"""
0002Prototype Helpers
0003
0004Provides a set of helpers for calling Prototype JavaScript functions,
0005including functionality to call remote methods using
0006`Ajax <http://www.adaptivepath.com/publications/essays/archives/000385.php>`_.
0007This means that you can call actions in your controllers without
0008reloading the page, but still update certain parts of it using
0009injections into the DOM. The common use case is having a form that adds
0010a new element to a list without reloading the page.
0011
0012To be able to use these helpers, you must include the Prototype
0013JavaScript framework in your pages.
0014
0015See `link_to_remote <module-railshelpers.helpers.javascript.html#link_to_function>`_
0016for documentation of options common to all Ajax helpers.
0017
0018See also `Scriptaculous <module-railshelpers.helpers.scriptaculous.html>`_ for
0019helpers which work with the Scriptaculous controls and visual effects library.
0020"""
0021# Last synced with Rails copy at Revision 6057 on Feb 9th, 2007.
0022
0023import sys
0024if sys.version < '2.4':
0025 from sets import ImmutableSet as frozenset
0026
0027from javascript import *
0028from javascript import options_for_javascript
0029from form_tag import form
0030from tags import tag, camelize
0031from urls import get_url
0032
0033CALLBACKS = frozenset(['uninitialized', 'loading', 'loaded',
0034 'interactive', 'complete', 'failure', 'success'] + [str(x) for x in range(100,599)])
0036AJAX_OPTIONS = frozenset(['before', 'after', 'condition', 'url',
0037 'asynchronous', 'method', 'insertion', 'position',
0038 'form', 'with', 'with_', 'update', 'script'] + list(CALLBACKS))
0040
0041def link_to_remote(name, options=None, **html_options):
0042 """
0043 Links to a remote function
0044
0045 Returns a link to a remote action defined ``dict(url=url())``
0046 (using the url() format) that's called in the background using
0047 XMLHttpRequest. The result of that request can then be inserted into a
0048 DOM object whose id can be specified with the ``update`` keyword.
0049
0050 Any keywords given after the second dict argument are considered html options
0051 and assigned as html attributes/values for the element.
0052
0053 Example::
0054
0055 link_to_remote("Delete this post", dict(update="posts",
0056 url=url(action="destroy", id=post.id)))
0057
0058 You can also specify a dict for ``update`` to allow for easy redirection
0059 of output to an other DOM element if a server-side error occurs:
0060
0061 Example::
0062
0063 link_to_remote("Delete this post",
0064 dict(url=url(action="destroy", id=post.id),
0065 update=dict(success="posts", failure="error")))
0066
0067 Optionally, you can use the ``position`` parameter to influence how the
0068 target DOM element is updated. It must be one of 'before', 'top', 'bottom',
0069 or 'after'.
0070
0071 By default, these remote requests are processed asynchronous during
0072 which various JavaScript callbacks can be triggered (for progress
0073 indicators and the likes). All callbacks get access to the
0074 ``request`` object, which holds the underlying XMLHttpRequest.
0075
0076 To access the server response, use ``request.responseText``, to
0077 find out the HTTP status, use ``request.status``.
0078
0079 Example::
0080
0081 link_to_remote(word,
0082 dict(url=url(action="undo", n=word_counter),
0083 complete="undoRequestCompleted(request)"))
0084
0085 The callbacks that may be specified are (in order):
0086
0087 ``loading``
0088 Called when the remote document is being loaded with data by the browser.
0089 ``loaded``
0090 Called when the browser has finished loading the remote document.
0091 ``interactive``
0092 Called when the user can interact with the remote document, even
0093 though it has not finished loading.
0094 ``success``
0095 Called when the XMLHttpRequest is completed, and the HTTP status
0096 code is in the 2XX range.
0097 ``failure``
0098 Called when the XMLHttpRequest is completed, and the HTTP status code is
0099 not in the 2XX range.
0100 ``complete``
0101 Called when the XMLHttpRequest is complete (fires after success/failure
0102 if they are present).
0103
0104 You can further refine ``success`` and ``failure`` by
0105 adding additional callbacks for specific status codes.
0106
0107 Example::
0108
0109 link_to_remote(word,
0110 dict(url=url(action="action"),
0111 404="alert('Not found...? Wrong URL...?')",
0112 failure="alert('HTTP Error ' + request.status + '!')"))
0113
0114 A status code callback overrides the success/failure handlers if
0115 present.
0116
0117 If you for some reason or another need synchronous processing (that'll
0118 block the browser while the request is happening), you can specify
0119 ``type='synchronous'``.
0120
0121 You can customize further browser side call logic by passing in
0122 JavaScript code snippets via some optional parameters. In their order
0123 of use these are:
0124
0125 ``confirm``
0126 Adds confirmation dialog.
0127 ``condition``
0128 Perform remote request conditionally by this expression. Use this to
0129 describe browser-side conditions when request should not be initiated.
0130 ``before``
0131 Called before request is initiated.
0132 ``after``
0133 Called immediately after request was initiated and before ``loading``.
0134 ``submit``
0135 Specifies the DOM element ID that's used as the parent of the form
0136 elements. By default this is the current form, but it could just as
0137 well be the ID of a table row or any other DOM element.
0138 """
0139 if options is None:
0140 options = {}
0141 return link_to_function(name, remote_function(**options), **html_options)
0142
0143def periodically_call_remote(**options):
0144 """
0145 Periodically calls a remote function
0146
0147 Periodically calls the specified ``url`` every ``frequency`` seconds
0148 (default is 10). Usually used to update a specified div ``update``
0149 with the results of the remote call. The options for specifying the
0150 target with ``url`` and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.
0151 """
0152 frequency = options.get('frequency') or 10
0153 code = "new PeriodicalExecuter(function() {%s}, %s)" % (remote_function(**options), frequency)
0154 return javascript_tag(code)
0155
0156def form_remote_tag(**options):
0157 """
0158 Create a form tag using a remote function to submit the request
0159
0160 Returns a form tag that will submit using XMLHttpRequest in the
0161 background instead of the regular reloading POST arrangement. Even
0162 though it's using JavaScript to serialize the form elements, the form
0163 submission will work just like a regular submission as viewed by the
0164 receiving side. The options for specifying the target with ``url``
0165 and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.
0166
0167 A "fall-through" target for browsers that doesn't do JavaScript can be
0168 specified with the ``action/method`` options on ``html``.
0169
0170 Example::
0171
0172 form_remote_tag(html=dict(action=url(
0173 controller="some", action="place")))
0174
0175 By default the fall-through action is the same as the one specified in
0176 the ``url`` (and the default method is ``POST``).
0177 """
0178 options['form'] = True
0179 if 'html' not in options: options['html'] = {}
0180 options['html']['onsubmit'] = "%s; return false;" % remote_function(**options)
0181 action = options['html'].get('action', get_url(options['url']))
0182 options['html']['method'] = options['html'].get('method', 'POST')
0183
0184 return form(action, **options['html'])
0185
0186def submit_to_remote(name, value, **options):
0187 """
0188 A submit button that submits via an XMLHttpRequest call
0189
0190 Returns a button input tag that will submit form using XMLHttpRequest
0191 in the background instead of regular reloading POST arrangement.
0192 Keyword args are the same as in ``form_remote_tag``.
0193 """
0194 options['with_'] = options.get('form') or 'Form.serialize(this.form)'
0195
0196 options['html'] = options.get('html') or {}
0197 options['html']['type'] = 'button'
0198 options['html']['onclick'] = "%s; return false;" % remote_function(**options)
0199 options['html']['name_'] = name
0200 options['html']['value'] = '%s' % value
0201
0202 return tag("input", open=False, **options['html'])
0203
0204def update_element_function(element_id, **options):
0205 """
0206 Returns a JavaScript function (or expression) that'll update a DOM
0207 element.
0208
0209 ``content``
0210 The content to use for updating.
0211 ``action``
0212 Valid options are 'update' (assumed by default), 'empty', 'remove'
0213 ``position``
0214 If the ``action`` is 'update', you can optionally specify one of the
0215 following positions: 'before', 'top', 'bottom', 'after'.
0216
0217 Example::
0218
0219 <% javascript_tag(update_element_function("products",
0220 position='bottom', content="<p>New product!</p>")) %>
0221
0222 This method can also be used in combination with remote method call
0223 where the result is evaluated afterwards to cause multiple updates on
0224 a page. Example::
0225
0226 # Calling view
0227 <% form_remote_tag(url=url(action="buy"),
0228 complete=evaluate_remote_response()) %>
0229 all the inputs here...
0230
0231 # Controller action
0232 def buy(self, **params):
0233 c.product = Product.find(1)
0234 return render_response('/buy.myt')
0235
0236 # Returning view (buy.myt)
0237 <% update_element_function(
0238 "cart", action='update', position='bottom',
0239 content="<p>New Product: %s</p>" % c.product.name) %>
0240 <% update_element_function("status", binding='binding',
0241 content="You've bought a new product!") %>
0242 """
0243 content = escape_javascript(options.get('content', ''))
0244 opval = options.get('action', 'update')
0245 if opval == 'update':
0246 if options.get('position'):
0247 jsf = "new Insertion.%s('%s','%s')" % (camelize(options['position']), element_id, content)
0248 else:
0249 jsf = "$('%s').innerHTML = '%s'" % (element_id, content)
0250 elif opval == 'empty':
0251 jsf = "$('%s').innerHTML = ''" % element_id
0252 elif opval == 'remove':
0253 jsf = "Element.remove('%s')" % element_id
0254 else:
0255 raise ValueError("Invalid action, choose one of update, remove, or empty")
0256
0257 jsf += ";\n"
0258 if options.get('binding'):
0259 return jsf + options['binding']
0260 else:
0261 return jsf
0262
0263def evaluate_remote_response():
0264 """
0265 Returns a Javascript function that evals a request response
0266
0267 Returns 'eval(request.responseText)' which is the JavaScript function
0268 that ``form_remote_tag`` can call in *complete* to evaluate a multiple
0269 update return document using ``update_element_function`` calls.
0270 """
0271 return "eval(request.responseText)"
0272
0273def remote_function(**options):
0274 """
0275 Returns the JavaScript needed for a remote function.
0276
0277 Takes the same options that can be passed as ``options`` to
0278 `link_to_remote <#link_to_remote>`_.
0279
0280 Example::
0281
0282 <select id="options" onchange="<% remote_function(update="options",
0283 url=url(action='update_options')) %>">
0284 <option value="0">Hello</option>
0285 <option value="1">World</option>
0286 </select>
0287 """
0288 javascript_options = options_for_ajax(options)
0289
0290 update = ''
0291 if options.get('update') and isinstance(options['update'], dict):
0292 update = []
0293 if options['update'].has_key('success'):
0294 update.append("success:'%s'" % options['update']['success'])
0295 if options['update'].has_key('failure'):
0296 update.append("failure:'%s'" % options['update']['failure'])
0297 update = '{' + ','.join(update) + '}'
0298 elif options.get('update'):
0299 update += "'%s'" % options['update']
0300
0301 function = "new Ajax.Request("
0302 if update: function = "new Ajax.Updater(%s, " % update
0303
0304 function += "'%s'" % get_url(options['url'])
0305 function += ", %s)" % javascript_options
0306
0307 if options.get('before'):
0308 function = "%s; %s" % (options['before'], function)
0309 if options.get('after'):
0310 function = "%s; %s" % (function, options['after'])
0311 if options.get('condition'):
0312 function = "if (%s) { %s; }" % (options['condition'], function)
0313 if options.get('confirm'):
0314 function = "if (confirm('%s')) { %s; }" % (escape_javascript(options['confirm']), function)
0315
0316 return function
0317
0318def observe_field(field_id, **options):
0319 """
0320 Observes the field with the DOM ID specified by ``field_id`` and makes
0321 an Ajax call when its contents have changed.
0322
0323 Required keyword args are:
0324
0325 ``url``
0326 ``url()``-style options for the action to call when the
0327 field has changed.
0328
0329 Additional keyword args are:
0330
0331 ``frequency``
0332 The frequency (in seconds) at which changes to this field will be
0333 detected. Not setting this option at all or to a value equal to or
0334 less than zero will use event based observation instead of time
0335 based observation.
0336 ``update``
0337 Specifies the DOM ID of the element whose innerHTML should be
0338 updated with the XMLHttpRequest response text.
0339 ``with_``
0340 A JavaScript expression specifying the parameters for the
0341 XMLHttpRequest. This defaults to 'value', which in the evaluated
0342 context refers to the new field value.
0343
0344 Additionally, you may specify any of the options documented in
0345 `link_to_remote <#link_to_remote>`_.
0346 """
0347 if options.get('frequency') > 0:
0348 class_ = 'Form.Element.Observer'
0349 else:
0350 class_ = 'Form.Element.EventObserver'
0351 return build_observer(class_, field_id, **options)
0352
0353def observe_form(form_id, **options):
0354 """
0355 Like `observe_field <#observe_field>`_, but operates on an entire form
0356 identified by the DOM ID ``form_id``.
0357
0358 Keyword args are the same as observe_field, except the default value of
0359 the ``with_`` keyword evaluates to the serialized (request string) value
0360 of the form.
0361 """
0362 if options.get('frequency'):
0363 class_ = 'Form.Observer'
0364 else:
0365 class_ = 'Form.EventObserver'
0366 return build_observer(class_, form_id, submit=form_id, **options)
0367
0368def options_for_ajax(options):
0369 js_options = build_callbacks(options)
0370
0371 js_options['asynchronous'] = str(options.get('type') != 'synchronous').lower()
0372 if options.get('method'):
0373 if isinstance(options['method'], str) and options['method'].startswith("'"):
0374 js_options['method'] = options['method']
0375 else:
0376 js_options['method'] = "'%s'" % options['method']
0377 if options.get('position'):
0378 js_options['insertion'] = "Insertion.%s" % camelize(options['position'])
0379 js_options['evalScripts'] = str(options.get('script') is None or options['script']).lower()
0380
0381 if options.get('form'):
0382 js_options['parameters'] = 'Form.serialize(this)'
0383 elif options.get('submit'):
0384 js_options['parameters'] = "Form.serialize('%s')" % options['submit']
0385 elif options.get('with_'):
0386 js_options['parameters'] = options['with_']
0387
0388 return options_for_javascript(js_options)
0389
0390def build_observer(cls, name, **options):
0391 if options.get('update') is True:
0392 options['with_'] = options.get('with', options.get('with_', 'value'))
0393 callback = remote_function(**options)
0394 javascript = "new %s('%s', " % (cls, name)
0395 if options.get('frequency'):
0396 javascript += "%s, " % options['frequency']
0397 javascript += "function(element, value) {%s}" % callback
0398 if options.get('on'):
0399 # FIXME: our prototype isn't supporting the on arg
0400 javascript +=", '%s'" % options['on']
0401 javascript += ")"
0402 return javascript_tag(javascript)
0403
0404def build_callbacks(options):
0405 callbacks = {}
0406 for callback, code in options.iteritems():
0407 if callback in CALLBACKS:
0408 name = 'on' + callback.title()
0409 callbacks[name] = "function(request){%s}" % code
0410 return callbacks
0411
0412__all__ = ['link_to_remote', 'periodically_call_remote', 'form_remote_tag', 'submit_to_remote', 'update_element_function',
0413 'evaluate_remote_response', 'remote_function', 'observe_field', 'observe_form']