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 Widgets 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 Views , Widgets 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_arguments to the Widget class, and decorate it with @expose. Inside the query_arguments method, each argument of the Widget is defined using a Field. In fact, the arguments of a Widget work exactly like the Fields on a model object. Think of the Widget as being the model object, and its .fields as being called its .query_arguments. 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 TwoColumnPage, P, H, Panel, HMenu
from reahl.component.modelinterface import exposed, IntegerField
class WidgetRefreshUI(UserInterface):
def assemble(self):
self.define_page(TwoColumnPage, style='basic')
find = self.define_view('/', title='Refreshing widget')
find.set_slot('main', HomePanel.factory())
class HomePanel(Panel):
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(HMenu.from_bookmarks(view, bookmarks))
self.add_child(panel)
class RefreshedPanel(Panel):
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})
Note
If a Widget has query_arguments, 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 Widgets. 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 Widgets to have such unique IDs for the underlying implementation to work. Forms 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 TwoColumnPage, P, H, Panel
from reahl.web.pager import SequentialPageIndex, PageMenu, PagedPanel
class AddressBookUI(UserInterface):
def assemble(self):
self.define_page(TwoColumnPage, style='basic')
find = self.define_view('/', title='Addresses')
find.set_slot('main', AddressBookPanel.factory())
class AddressBookPanel(Panel):
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)
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))]