WebHelpers / webhelpers / rails / prototype.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
"""
Prototype Helpers

Provides a set of helpers for calling Prototype JavaScript functions, 
including functionality to call remote methods using 
`Ajax <http://www.adaptivepath.com/publications/essays/archives/000385.php>`_. 
This means that you can call actions in your controllers without
reloading the page, but still update certain parts of it using
injections into the DOM. The common use case is having a form that adds
a new element to a list without reloading the page.

To be able to use these helpers, you must include the Prototype 
JavaScript framework in your pages.

See `link_to_remote <module-railshelpers.helpers.javascript.html#link_to_function>`_ 
for documentation of options common to all Ajax helpers.

See also `Scriptaculous <module-railshelpers.helpers.scriptaculous.html>`_ for
helpers which work with the Scriptaculous controls and visual effects library.
"""
# Last synced with Rails copy at Revision 6057 on Feb 9th, 2007.

import sys
if sys.version < '2.4':
    from sets import ImmutableSet as frozenset

from javascript import *
from javascript import options_for_javascript
from form_tag import form
from tags import tag, camelize
from urls import get_url

CALLBACKS = frozenset(['uninitialized', 'loading', 'loaded',
                       'interactive', 'complete', 'failure', 'success'] + \
                          [str(x) for x in range(100,599)])
AJAX_OPTIONS = frozenset(['before', 'after', 'condition', 'url',
                          'asynchronous', 'method', 'insertion', 'position',
                          'form', 'with', 'with_', 'update', 'script'] + \
                             list(CALLBACKS))

def link_to_remote(name, options=None, **html_options):
    """
    Links to a remote function
    
    Returns a link to a remote action defined ``dict(url=url())``
    (using the url() format) that's called in the background using 
    XMLHttpRequest. The result of that request can then be inserted into a
    DOM object whose id can be specified with the ``update`` keyword. 
    
    Any keywords given after the second dict argument are considered html options
    and assigned as html attributes/values for the element.
    
    Example::
    
        link_to_remote("Delete this post", dict(update="posts", 
                       url=url(action="destroy", id=post.id)))
    
    You can also specify a dict for ``update`` to allow for easy redirection
    of output to an other DOM element if a server-side error occurs:
    
    Example::

        link_to_remote("Delete this post",
                dict(url=url(action="destroy", id=post.id),
                     update=dict(success="posts", failure="error")))
    
    Optionally, you can use the ``position`` parameter to influence how the
    target DOM element is updated. It must be one of 'before', 'top', 'bottom',
    or 'after'.
    
    By default, these remote requests are processed asynchronous during 
    which various JavaScript callbacks can be triggered (for progress 
    indicators and the likes). All callbacks get access to the 
    ``request`` object, which holds the underlying XMLHttpRequest. 
    
    To access the server response, use ``request.responseText``, to
    find out the HTTP status, use ``request.status``.
    
    Example::

        link_to_remote(word,
                dict(url=url(action="undo", n=word_counter),
                     complete="undoRequestCompleted(request)"))
    
    The callbacks that may be specified are (in order):
    
    ``loading``
        Called when the remote document is being loaded with data by the browser.
    ``loaded``
        Called when the browser has finished loading the remote document.
    ``interactive``
        Called when the user can interact with the remote document, even
        though it has not finished loading.
    ``success``
        Called when the XMLHttpRequest is completed, and the HTTP status
        code is in the 2XX range.
    ``failure``
        Called when the XMLHttpRequest is completed, and the HTTP status code is
        not in the 2XX range.
    ``complete``
        Called when the XMLHttpRequest is complete (fires after success/failure
        if they are present).
                        
    You can further refine ``success`` and ``failure`` by 
    adding additional callbacks for specific status codes.
    
    Example::
    
        link_to_remote(word,
                dict(url=url(action="action"),
                     404="alert('Not found...? Wrong URL...?')",
                     failure="alert('HTTP Error ' + request.status + '!')"))
    
    A status code callback overrides the success/failure handlers if 
    present.
    
    If you for some reason or another need synchronous processing (that'll
    block the browser while the request is happening), you can specify 
    ``type='synchronous'``.
    
    You can customize further browser side call logic by passing in
    JavaScript code snippets via some optional parameters. In their order 
    of use these are:
    
    ``confirm``
        Adds confirmation dialog.
    ``condition``
        Perform remote request conditionally by this expression. Use this to
        describe browser-side conditions when request should not be initiated.
    ``before``
        Called before request is initiated.
    ``after``
        Called immediately after request was initiated and before ``loading``.
    ``submit``
        Specifies the DOM element ID that's used as the parent of the form
        elements. By default this is the current form, but it could just as
        well be the ID of a table row or any other DOM element.    
    """
    if options is None:
        options = {}
    return link_to_function(name, remote_function(**options), **html_options)

def periodically_call_remote(**options):
    """
    Periodically calls a remote function
    
    Periodically calls the specified ``url`` every ``frequency`` seconds
    (default is 10). Usually used to update a specified div ``update``
    with the results of the remote call. The options for specifying the
    target with ``url`` and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.    
    """
    frequency = options.get('frequency') or 10
    code = "new PeriodicalExecuter(function() {%s}, %s)" % (remote_function(**options), frequency)
    return javascript_tag(code)

def form_remote_tag(**options):
    """
    Create a form tag using a remote function to submit the request
    
    Returns a form tag that will submit using XMLHttpRequest in the 
    background instead of the regular reloading POST arrangement. Even 
    though it's using JavaScript to serialize the form elements, the form
    submission will work just like a regular submission as viewed by the
    receiving side. The options for specifying the target with ``url``
    and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.
    
    A "fall-through" target for browsers that doesn't do JavaScript can be
    specified with the ``action/method`` options on ``html``.
    
    Example::

        form_remote_tag(html=dict(action=url(
                                    controller="some", action="place")))
    
    By default the fall-through action is the same as the one specified in 
    the ``url`` (and the default method is ``POST``).
    """
    options['form'] = True
    if 'html' not in options: options['html'] = {}
    options['html']['onsubmit'] = "%s; return false;" % remote_function(**options)
    action = options['html'].get('action', get_url(options['url']))
    options['html']['method'] = options['html'].get('method', 'POST')
    
    return form(action, **options['html'])

def submit_to_remote(name, value, **options):
    """
    A submit button that submits via an XMLHttpRequest call
    
    Returns a button input tag that will submit form using XMLHttpRequest 
    in the background instead of regular reloading POST arrangement. 
    Keyword args are the same as in ``form_remote_tag``.    
    """
    options['with_'] = options.get('form') or 'Form.serialize(this.form)'
    
    options['html'] = options.get('html') or {}
    options['html']['type'] = 'button'
    options['html']['onclick'] = "%s; return false;" % remote_function(**options)
    options['html']['name_'] = name
    options['html']['value'] = str(value)
    
    return tag("input", open=False, **options['html'])

def update_element_function(element_id, **options):
    """
    Returns a JavaScript function (or expression) that'll update a DOM 
    element.
    
    ``content``
        The content to use for updating.
    ``action``
        Valid options are 'update' (assumed by default), 'empty', 'remove'
    ``position``
        If the ``action`` is 'update', you can optionally specify one of the
        following positions: 'before', 'top', 'bottom', 'after'.
    
    Example::
    
        <% javascript_tag(update_element_function("products", 
            position='bottom', content="<p>New product!</p>")) %>
    
    This method can also be used in combination with remote method call 
    where the result is evaluated afterwards to cause multiple updates on
    a page. Example::
    
        # Calling view
        <% form_remote_tag(url=url(action="buy"), 
                complete=evaluate_remote_response()) %>
            all the inputs here...
    
        # Controller action
        def buy(self, **params):
            c.product = Product.find(1)
            return render_response('/buy.myt')
    
        # Returning view (buy.myt)
        <% update_element_function(
                "cart", action='update', position='bottom', 
                content="<p>New Product: %s</p>" % c.product.name) %>
        <% update_element_function("status", binding='binding',
                content="You've bought a new product!") %>
    """
    content = escape_javascript(options.get('content', ''))
    opval = options.get('action', 'update')
    if opval == 'update':
        if options.get('position'):
            jsf = "new Insertion.%s('%s','%s')" % (camelize(options['position']), element_id, content)
        else:
            jsf = "$('%s').innerHTML = '%s'" % (element_id, content)
    elif opval == 'empty':
        jsf = "$('%s').innerHTML = ''" % element_id
    elif opval == 'remove':
        jsf = "Element.remove('%s')" % element_id
    else:
        raise "Invalid action, choose one of update, remove, or empty"
    
    jsf += ";\n"
    if options.get('binding'):
        return jsf + options['binding']
    else:
        return jsf

def evaluate_remote_response():
    """
    Returns a Javascript function that evals a request response
    
    Returns 'eval(request.responseText)' which is the JavaScript function
    that ``form_remote_tag`` can call in *complete* to evaluate a multiple
    update return document using ``update_element_function`` calls.    
    """
    return "eval(request.responseText)"

def remote_function(**options):
    """
    Returns the JavaScript needed for a remote function.
    
    Takes the same options that can be passed as ``options`` to
    `link_to_remote <#link_to_remote>`_.
    
    Example::
    
        <select id="options" onchange="<% remote_function(update="options", 
                url=url(action='update_options')) %>">
            <option value="0">Hello</option>
            <option value="1">World</option>
        </select>    
    """
    javascript_options = options_for_ajax(options)
    
    update = ''
    if options.get('update') and isinstance(options['update'], dict):
        update = []
        if options['update'].has_key('success'): 
            update.append("success:'%s'" % options['update']['success'])
        if options['update'].has_key('failure'):
            update.append("failure:'%s'" % options['update']['failure'])
        update = '{' + ','.join(update) + '}'
    elif options.get('update'):
        update += "'%s'" % options['update']
    
    function = "new Ajax.Request("
    if update: function = "new Ajax.Updater(%s, " % update
    
    function += "'%s'" % get_url(options['url'])
    function += ", %s)" % javascript_options
    
    if options.get('before'):
        function = "%s; %s" % (options['before'], function)
    if options.get('after'):
        function = "%s; %s" % (function, options['after'])
    if options.get('condition'):
        function = "if (%s) { %s; }" % (options['condition'], function)
    if options.get('confirm'):
        function = "if (confirm('%s')) { %s; }" % (escape_javascript(options['confirm']), function)
    
    return function

def observe_field(field_id, **options):
    """
    Observes the field with the DOM ID specified by ``field_id`` and makes
    an Ajax call when its contents have changed.
    
    Required keyword args are:
    
    ``url``
        ``url()``-style options for the action to call when the
        field has changed.
    
    Additional keyword args are:
    
    ``frequency``
        The frequency (in seconds) at which changes to this field will be
        detected. Not setting this option at all or to a value equal to or
        less than zero will use event based observation instead of time
        based observation.
    ``update``
        Specifies the DOM ID of the element whose innerHTML should be
        updated with the XMLHttpRequest response text.
    ``with_``
        A JavaScript expression specifying the parameters for the
        XMLHttpRequest. This defaults to 'value', which in the evaluated
        context refers to the new field value.
    
    Additionally, you may specify any of the options documented in
    `link_to_remote <#link_to_remote>`_.
    """
    if options.get('frequency') > 0:
        class_ = 'Form.Element.Observer'
    else:
        class_ = 'Form.Element.EventObserver'
    return build_observer(class_, field_id, **options)

def observe_form(form_id, **options):
    """
    Like `observe_field <#observe_field>`_, but operates on an entire form
    identified by the DOM ID ``form_id``.
    
    Keyword args are the same as observe_field, except the default value of
    the ``with_`` keyword evaluates to the serialized (request string) value
    of the form.
    """
    if options.get('frequency'):
        class_ = 'Form.Observer'
    else:
        class_ = 'Form.EventObserver'
    return build_observer(class_, form_id, submit=form_id, **options)

def options_for_ajax(options):
    js_options = build_callbacks(options)
    
    js_options['asynchronous'] = str(options.get('type') != 'synchronous').lower()
    if options.get('method'):
        if isinstance(options['method'], str) and options['method'].startswith("'"):
            js_options['method'] = options['method']
        else:
            js_options['method'] = "'%s'" % options['method']
    if options.get('position'):
        js_options['insertion'] = "Insertion.%s" % camelize(options['position'])
    js_options['evalScripts'] = str(options.get('script') is None or options['script']).lower()
    
    if options.get('form'):
        js_options['parameters'] = 'Form.serialize(this)'
    elif options.get('submit'):
        js_options['parameters'] = "Form.serialize('%s')" % options['submit']
    elif options.get('with_'):
        js_options['parameters'] = options['with_']
    
    return options_for_javascript(js_options)

def build_observer(cls, name, **options):
    if options.get('update') is True:
        options['with_'] = options.get('with', options.get('with_', 'value'))
    callback = remote_function(**options)
    javascript = "new %s('%s', " % (cls, name)
    if options.get('frequency'): 
        javascript += "%s, " % options['frequency']
    javascript += "function(element, value) {%s}" % callback
    if options.get('on'):
        # FIXME: our prototype isn't supporting the on arg
        javascript +=", '%s'" % options['on']
    javascript += ")"
    return javascript_tag(javascript)

def build_callbacks(options):
    callbacks = {}
    for callback, code in options.iteritems():
        if callback in CALLBACKS:
            name = 'on' + callback.title()
            callbacks[name] = "function(request){%s}" % code
    return callbacks

__all__ = ['link_to_remote', 'periodically_call_remote', 'form_remote_tag', 'submit_to_remote', 'update_element_function',
           'evaluate_remote_response', 'remote_function', 'observe_field', 'observe_form']
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.