Components, LOAD and Ajax

A component is a functionally autonomous part of a web page.

A component may be composed of modules, controllers and views, but there is no strict requirement other than, when embedded in a web page, it must be localized within an html tag (for example a DIV, a SPAN, or an IFRAME) and it must perform its task independently of the rest of the page. We are specifically interested in components that are loaded in the page and communicate with the component controller function via Ajax.

An example of a component is a “comments component” that is contained into a DIV and shows users’ comments and a post-new-comment form. When the form is submitted, it is sent to the server via Ajax, the list is updated, and the comment is stored server-side in the database. The DIV content is refreshed without reloading the rest of the page.

LOAD

The web2py LOAD function makes this easy to do without explicit JavaScript/Ajax knowledge or programming.

Our goal is to be able to develop web applications by assembling components into page layouts.

Consider a simple web2py app “test” that extends the default scaffolding app with a custom model in file “models/db_comments.py”:

  1. db.define_table('comment_post',
  2. Field('body', 'text', label='Your comment'),
  3. auth.signature)

one action in “controllers/comments.py”

  1. @auth.requires_login()
  2. def post():
  3. return dict(form=SQLFORM(db.comment_post).process(),
  4. comments=db(db.comment_post).select())

and the corresponding “views/comments/post.html”

  1. {{extend 'layout.html'}}
  2. {{for post in comments:}}
  3. <div class="post">
  4. On {{=post.created_on}} {{=post.created_by.first_name}}
  5. says <span class="post_body">{{=post.body}}</span>
  6. </div>
  7. {{pass}}
  8. {{=form}}

You can access it as usual at:

  1. http://127.0.0.1:8000/test/comments/post

So far there is nothing special in this action, but we can turn it into a component by defining a new view with extension “.load” that does not extend the layout.

Hence we create a “views/comments/post.load”:

  1. {{for post in comments:}}
  2. <div class="post">
  3. On {{=post.created_on}} {{=post.created_by.first_name}}
  4. says <blockquote class="post_body">{{=post.body}}</blockquote>
  5. </div>
  6. {{pass}}
  7. {{=form}}

We can access it at the URL

  1. http://127.0.0.1:8000/test/comments/post.load

This is a component that we can embed into any other page by simply doing

  1. {{=LOAD('comments', 'post.load', ajax=True)}}

For example in “controllers/default.py” we can edit

  1. def index():
  2. return dict()

and in the corresponding view add the component:

  1. {{extend 'layout.html'}}
  2. {{=LOAD('comments', 'post.load', ajax=True)}}

Visiting the page

  1. http://127.0.0.1:8000/test/default/index

will show the normal content and the comments component:

image

The {{=LOAD(...)}} component is rendered as follows:

  1. <script type="text/javascript"><!--
  2. web2py_component("/test/comment/post.load", "c282718984176")
  3. //--></script><div id="c282718984176">loading...</div>

(the actual generated code depends on the options passed to the LOAD function).

The web2py_component(url, id) function is defined in “web2py_ajax.html” and it performs all the magic: it calls the url via Ajax and embeds the response into the DIV with corresponding id; it traps every form submission into the DIV and submits those forms via Ajax. The Ajax target is always the DIV itself.

LOAD signature

The full signature of the LOAD helper is the following:

  1. LOAD(c=None, f='index', args=[], vars={},
  2. extension=None, target=None,
  3. ajax=False, ajax_trap=False,
  4. url=None, user_signature=False,
  5. timeout=None, times=1,
  6. content='loading...', **attr):

Here:

  • the first two arguments c and f are the controller and the function that we want to call respectively.
  • args and vars are the arguments and variables that we want to pass to the function. The former is a list, the latter is a dictionary.
  • extension is an optional extension. Notice that the extension can also be passed as part of the function as in f='index.load'.
  • target is the id of the target DIV. If it is not specified a random target id is generated.
  • ajax should be set to True if the DIV has to be filled via Ajax and to False if the DIV has to be filled before the current page is returned (thus avoiding the Ajax call). If set to False, the component’s code and view will be executed in the same web2py environment as the caller.
  • ajax_trap=True means that any form submission in the DIV must be captured and submitted via Ajax, and the response must be rendered inside the DIV. ajax_trap=False indicates that forms must be submitted normally, thus reloading the entire page. ajax_trap is ignored and assumed to be True if ajax=True.
  • url, if specified, overrides the values of c, f, args, vars, and extension and loads the component at the url. It is used to load as components pages served by other applications (which may or may not be created with web2py). Note that using url assumes ajax as True always, because web2py can’t know in advance if the component is rendered within web2py or is just an external page
  • user_signature defaults to False but, if you are logged in, it should be set to True. This will make sure the ajax callback is digitally signed. This is documented in chapter 4.
  • times specifies how many times the component is to be requested. Use “infinity” to keep loading the component continuously. This option is useful for triggering regular routines for a given document request.
  • timeout sets the time to wait in milliseconds before starting the request or the frequency if times is greater than 1.
  • content is the content to be displayed while performing the ajax call. It can be a helper as in content=IMG(..).
  • optional **attr (attributes) can be passed to the contained DIV.

If no .load view is specified, there is a generic.load that renders the dictionary returned by the action without layout. It works best if the dictionary contains a single item.

If you LOAD a component having the .load extension and the corresponding controller function redirects to another action (for example a login form), the .load extension propagates and the new url (the one to redirect too) is also loaded with a .load extension.

Redirect from a component

To redirect from a component, use this:

  1. redirect(URL(...), client_side=True)

but note that the redirected URL will default to the extension of the component. See notes about the argument extension in the URL function in Chapter 4

Reload page via redirect after component submission

If you call an action via Ajax and you want the action to force a redirect of the parent page you can do it with a redirect from the LOADed controller function. If you want to reload the parent page, you can redirect to it. The parent URL is known (see Client-Server component communications )

so after processing the form submit, the controller function reloads the parent page via redirect:

  1. if form.process().accepted:
  2. ...
  3. redirect(request.env.http_web2py_component_location, client_side=True)

Note that the section below, Client-Server component communications, describes how the component can return javascript, which could be used for more sophisticated actions when the component is submitted. The specific case of reloading another component is described next.

Reload another component

If you use multiple components on a page, you may want the submission of one component to reload another. You do this by getting the submitted component to return some javascript.

It’s possible to hard-code the target DIV, but in this recipe we use a query-string variable to inform the submitting-controller which component we want to reload. It’s identified by the id of the DIV containing the target component. In this case, the DIV has id ‘map’. Note that it is necessary to use target='map' in the LOAD of the target; without this, the target id is randomised and reload() won’t work. See LOAD signature above.

In the view, do this:

  1. {{=LOAD('default', 'submitting_component.load', ajax=True, vars={'reload_div':'map'})}}

The controller belonging to the submitting component needs to send javascript back, so just add this to the existing controller code when processing the submit:

  1. if form.process().accepted:
  2. ...
  3. if request.vars.reload_div:
  4. response.js = "jQuery('#%s').get(0).reload()" % request.vars.reload_div

(Of course, remove the redirect if you were using the approach of the previous section.)

That’s it. web2py’s javascript libraries look after the reload. This could be generalised to handle multiple components with javascript looking like:

  1. jQuery('#div1,#div2,#div3').get(0).reload()

For more information about response.js see Client-Server component communications (below).

Ajax post does not support multipart forms

Because Ajax post does not support multipart forms, i.e. file uploads, upload fields will not work with the LOAD component. You could be fooled into thinking it would work because upload fields will function normally if POST is done from the individual component’s .load view. Instead, uploads are done with ajax-compatible 3rd-party widgets and web2py manual upload store commands.