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”:
db.define_table('comment_post',
Field('body', 'text', label='Your comment'),
auth.signature)
one action in “controllers/comments.py”
@auth.requires_login()
def post():
return dict(form=SQLFORM(db.comment_post).process(),
comments=db(db.comment_post).select())
and the corresponding “views/comments/post.html”
{{extend 'layout.html'}}
{{for post in comments:}}
<div class="post">
On {{=post.created_on}} {{=post.created_by.first_name}}
says <span class="post_body">{{=post.body}}</span>
</div>
{{pass}}
{{=form}}
You can access it as usual at:
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”:
{{for post in comments:}}
<div class="post">
On {{=post.created_on}} {{=post.created_by.first_name}}
says <blockquote class="post_body">{{=post.body}}</blockquote>
</div>
{{pass}}
{{=form}}
We can access it at the URL
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
{{=LOAD('comments', 'post.load', ajax=True)}}
For example in “controllers/default.py” we can edit
def index():
return dict()
and in the corresponding view add the component:
{{extend 'layout.html'}}
{{=LOAD('comments', 'post.load', ajax=True)}}
Visiting the page
http://127.0.0.1:8000/test/default/index
will show the normal content and the comments component:
The {{=LOAD(...)}}
component is rendered as follows:
<script type="text/javascript"><!--
web2py_component("/test/comment/post.load", "c282718984176")
//--></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:
LOAD(c=None, f='index', args=[], vars={},
extension=None, target=None,
ajax=False, ajax_trap=False,
url=None, user_signature=False,
timeout=None, times=1,
content='loading...', **attr):
Here:
- the first two arguments
c
andf
are the controller and the function that we want to call respectively. args
andvars
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 inf='index.load'
.target
is theid
of the target DIV. If it is not specified a random targetid
is generated.ajax
should be set toTrue
if the DIV has to be filled via Ajax and toFalse
if the DIV has to be filled before the current page is returned (thus avoiding the Ajax call). If set toFalse
, 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 beTrue
ifajax=True
.url
, if specified, overrides the values ofc
,f
,args
,vars
, andextension
and loads the component at theurl
. It is used to load as components pages served by other applications (which may or may not be created with web2py). Note that usingurl
assumesajax
asTrue
always, because web2py can’t know in advance if the component is rendered within web2py or is just an external pageuser_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 iftimes
is greater than 1.content
is the content to be displayed while performing the ajax call. It can be a helper as incontent=IMG(..)
.- optional
**attr
(attributes) can be passed to the containedDIV
.
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:
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:
if form.process().accepted:
...
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:
{{=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:
if form.process().accepted:
...
if request.vars.reload_div:
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:
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.