What ever happened to Ajax?¶
Ajax is a prominent word in the web development world. You may be wondering why it has not been mentioned in this tutorial yet. The thing is, Ajax is a low level implementation technique, and the whole vision of Reahl is to try and hide such low level implementation details. The framework should deal with it for you: Ajax is in there, with many other techniques, you just do not have to know about it.
Sometimes though, one needs to build your own Widget
s with some
behaviour that smacks of Ajax. For example, you’d want a part of the
page to change without refreshing the entire page. Is this high-level
framework flexible enough to let you do that?
To say yes does not do the gravity of that question justice. How Reahl allows a programmer to do this, and the ways in which other classes in the Reahl framework use such capabilities say a lot about the values underlying Reahl. Hence, the two examples in this section.
Refreshing Widgets¶
It is possible for a programmer to build a Widget
which gets refreshed
without reloading the entire page. To show how this works, we have
concocted an example. The example consists of a single page with a
Panel
that contains a paragraph with some text. The page contains a
Menu
too. When the user clicks on a MenuItem
, the contents of the
Panel
changes to indicate what was clicked on in the Menu
. This
happens without reloading the page.
The test illustrates this idea:
@test(RefreshFixture)
def refreshing_widget(fixture):
"""Clicking on a link, refreshes the displayed text to indicate which link
was clicked, without triggering a page load."""
fixture.reahl_server.set_app(fixture.wsgi_app)
browser = fixture.browser
browser.open('/')
assert fixture.text_shows_selected(1)
assert not fixture.text_shows_selected(3)
with browser.no_page_load_expected():
browser.click(XPath.link_with_text('Select 3'))
assert not fixture.text_shows_selected(1)
assert fixture.text_shows_selected(3)
Forget about Ajax, though, and learn these concepts: Like View
s ,
Widget
s can have arguments too. A Widget
can be
set to be re-rendered when the value of one or more of its arguments on a given page change.
(Behind the scenes, this happens via Ajax.)
The beauty of this mechanism in Reahl is that it will all still work for users even when their JavaScript is turned off – just not via Ajax. This is really good, because it makes the parts of the application that work like this crawlable by search engines, and bookmarkable by browser users.
Just like you can get a Bookmark
to a View
with specific arguments,
you can also get a Bookmark
to a Widget
with specific arguments. If a
user clicks on a MenuItem
for such an in-page Bookmark
, the user
stays on the same View
, the only thing that changes is the value of
the arguments to the applicable Widget
. This is what triggers the
Widget
to be re-rendered.
The arguments of a Widget
are called query arguments. To declare query
arguments on the Widget
you add a method called query_fields to
the Widget
class, and decorate it with @expose. Inside the
query_fields method, each argument of the Widget
is defined using
a Field
. In fact, the arguments of a Widget
work exactly like the
Field
s on a model object. Think of the Widget
as being the model
object, and its .fields as being called its .query_fields. When
the Widget
is constructed, an attribute will be set for each query
argument on the Widget
instance:
from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.web.fw import UserInterface, Bookmark
from reahl.web.ui import HTML5Page, P, H, Div, Menu, HorizontalLayout
from reahl.web.layout import PageLayout
from reahl.web.pure import ColumnLayout
from reahl.component.modelinterface import exposed, IntegerField
class WidgetRefreshUI(UserInterface):
def assemble(self):
page_layout = PageLayout(contents_layout=ColumnLayout('main').with_slots())
self.define_page(HTML5Page, style='basic').use_layout(page_layout)
find = self.define_view('/', title='Refreshing widget')
find.set_slot('main', HomePanel.factory())
class HomePanel(Div):
def __init__(self, view):
super(HomePanel, self).__init__(view)
panel = RefreshedPanel(view, 'my_refreshedpanel')
bookmarks = [panel.get_bookmark(1),
panel.get_bookmark(2),
panel.get_bookmark(3)]
self.add_child(H(view, 1, text='Refreshing widget'))
self.add_child(Menu(view).use_layout(HorizontalLayout()).with_bookmarks(bookmarks))
self.add_child(panel)
class RefreshedPanel(Div):
def __init__(self, view, css_id):
super(RefreshedPanel, self).__init__(view, css_id=css_id)
self.add_child(P(view, text='You selected link number %s' % self.selected))
self.enable_refresh()
@exposed
def query_fields(self, fields):
fields.selected = IntegerField(required=False, default=1)
def get_bookmark(self, for_selected):
return Bookmark.for_widget('Select %s' % for_selected, query_arguments={'selected': for_selected}).on_view(self.view)
Note
If a Widget
has query_fields, it is required to have a unique
css_id. (The framework raises a ProgrammerError
if this rule is
violated.)
Paging long lists¶
Frequently in web applications there is a need to present a long list of items that do not fit onto a single web page. The Reahl way of dealing with such a requirement is to provide a bunch of classes that solve the problem together, on a conceptual level. Think of it almost as a design pattern of sorts:
To deal with long lists, the long list is seen as being broken up into
different “pages”, each page containing a smaller, manageable list of
items. Breaking a list up into such pages is the responsibility of the
PageIndex
. A PageMenu
is a Widget
which allows the user to choose
which of these pages should be viewed. The PagedPanel
presents the
items on the currently selected page to the user. There are two kinds
of PageIndex
that a programmer can choose from: AnnualPageIndex
assumes that a date is associated with each item, and puts all items
sharing the same year on a page, ordering pages by
year. SequentialPageIndex
just breaks a list of items up into a fixed
number of items per page.
In the example below, a long list of Addresses is divided into pages
using a SequentialPageIndex
. For simplicity, the Addresses are not
stored in the database in this example – Address.all_addresses()
simply creates the Addresses instead of finding them from the
database.
AddressList is the PagedPanel
in this example. This is the piece the
programmer needs to supply. Note that AddressList inherits a property
.current_contents which is a list of all the items on the current
page. In the implementation of AddressBookPanel.__init__ you will
also see that the SequentialPageIndex
is created first, and sent to
both of the other two Widget
s. It is created with the long list of
Addresses, and told how many items to put per page.
Both the PageMenu
and the PagedPanel
also need to have unique IDs
specified, as strings. The ID of PageMenu
is passed into its
constructor, and AddressList passes its ID to the super call in
AddressList.__init__. Reahl sometimes need Widget
s to have such
unique IDs for the underlying implementation to work. Form
s also need
a unique ID upon construction, for example.
from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.web.fw import UserInterface
from reahl.web.ui import HTML5Page, P, H, Div, HorizontalLayout
from reahl.web.layout import PageLayout
from reahl.web.pure import ColumnLayout
from reahl.web.pager import SequentialPageIndex, PageMenu, PagedPanel
class AddressBookUI(UserInterface):
def assemble(self):
page_layout = PageLayout(contents_layout=ColumnLayout('main').with_slots())
self.define_page(HTML5Page, style='basic').use_layout(page_layout)
find = self.define_view('/', title='Addresses')
find.set_slot('main', AddressBookPanel.factory())
class AddressBookPanel(Div):
def __init__(self, view):
super(AddressBookPanel, self).__init__(view)
self.add_child(H(view, 1, text='Addresses'))
self.page_index = SequentialPageIndex(Address.all_addresses(), items_per_page=5)
self.address_list = AddressList(view, self.page_index)
self.page_menu = PageMenu(view, 'page_menu', self.page_index, self.address_list, menu_layout=HorizontalLayout())
self.add_children([self.page_menu, self.address_list])
class AddressList(PagedPanel):
def __init__(self, view, page_index):
super(AddressList, self).__init__(view, page_index, 'addresslist')
for address in self.current_contents:
self.add_child(P(view, text='%s: %s' % (address.name, address.email_address)))
class Address(object):
def __init__(self, name, email_address):
self.name = name
self.email_address = email_address
@classmethod
def all_addresses(cls):
return [Address('friend %s' % i,'friend%s@some.org' % i ) for i in list(range(200))]