Personal tools
You are here: Home Documentation Tutorials Using YUI on Grok

Using YUI on Grok

Note: Return to tutorial view.

In this tutorial you'll be learning how to use the Yahoo! User Interface Library, or YUI in short in your Grok project. YUI is a JavaScript and CSS library providing all sorts of handy components that you can use to make you site work an look better without too much effort, and in a cross-browser compatible way. In the process, you'll also be learning some basic uses of megrok.layout, megrok.navigation and JSON.

Setting up the basics

Adding the requirements and setting up a basic layout

Requirements

We will be using YUI 2. More details on the YUI website [1]

The main packages that we will use are

but along the way, we'll also be using

You should know something about layouts, and understanding layers is a plus for some part of the tutorial.

Let's first set up a basic project to get going. Create a new grok project (see other docs on how to do that) and edit the file 'setup.py'. Add these lines at the bottom of the install_requires section (under # Add extra requirements here):

'hurry.yui',
'megrok.resource',
'megrok.layout',
'megrok.navigation',
'megrok.pagetemplate',

If you are using Grok 1.0 and you would run bin/buildout now, you would get version conflicts on some packages because they were developed for Grok1.1 and up. Adapting your 'buildout.cfg' should solve this:

[versions]
zope.site = 3.5.1
megrok.layout = 0.9
megrok.pagetemplate = 0.3

Now run bin/buildout again to download the needed packages.

Main Layout, Reset and Grids

Layout.pt

We will use megrok.layout for our main layout as it gives a nice separation between the layout template and the view templates. Without megrok.layout, you would be either redefining your site's layout in each view or create a macro that you call from each view, so each view will need to know what layout you will be using.

With megrok.layout you just define a layout template and you indicate with a structure view/content TALES expression where the view template should be rendered. You then derive your views from megrok.layout.Page instead of grok.View, and the framework automatically looks up the layout for the current layer and renders your view inside it.

Enough talk, let's get cracking, Mr Bond!

Let's start by defining the actual template first. Create a file called 'layout.pt' in the app_templates dir.

<html xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
   <title>YUI Demo</title>
   <link rel="stylesheet" type="text/css" tal:attributes="href static/style.css" />
</head>
<body>
<div id="doc" class="yui-t7">
   <div id="header" role="navigation">Navigation will go here!</div>
   <div id="body" role="main">
        <div class="yui-g" tal:content="structure view/content">
        </div>
   </div>
   <div id="footer" role="contentinfo">The Grok YUI demo</div>
</div>
</body>
</html>

Create a 'style.css' file in the 'static' dir:

html, #footer {background-color:#eee;}
body {margin:0;}
#doc {background-color:white;}

We created a basic layout with a link to a stylesheet, and a basic Grid by defining the main div-element to be of class yui-t7. The astute reader may have noticed that I did not refer to any YUI CSS or javascript file. This is where hurry.yui and megrok.resource come in!

Megrok.layout and hurry.yui

Create a new file called 'layout.py' alongside the 'app.py' file:

import grok
from megrok import layout
from zope.interface import Interface
from hurry import yui

grok.templatedir('app_templates')

class Layout(layout.Layout):
    grok.context(Interface)

    def update(self):
        yui.reset_fonts_grids.need()
        yui.base.need()

Hurry.yui is a 'resource library' based on hurry.resource. Hurry.resource implements a quick way of defining which parts of your code need specific resources: simply write <resource>.need() and the library keeps track of all the necessary resources for the current request. This also means that you use need() statements inside methods, unlike grok directives that apply at class level.

In the update method we specify that our layout needs Reset, Fonts, Grids and the Base CSS components from the YUI lib. Reset, Fonts and Grids can be 'rolled up' in one statement because those are parts that are very commonly used in tandem. Reset overwrites all browser default styling to a common set, so your site looks the same, no matter what browser you use, even without specifying any styles yourself. Fonts does this for fonts specifically, and Grids predefines some typical site layouts you can use (check out the YUI Grid Builder tool to see what you can do with it). Base defines some basic styles.

Megrok.layout and megrok.resource

'OK, but defining your requirements, doesn't include them', I hear you thinking. It doesn't, indeed.

Enter megrok.resource. This package uses hurry.zoperesource, which will override the default Request object with a special one that will add the needed resources to the <head> tag of your HTML just before rendering. Kind of magic...

Before you can actually try it out, we should redefine the default index page. Edit 'app.py' and change the index view to

from megrok import layout

class Index(layout.Page):
    pass # see app_templates/index.pt

and the 'index.pt' template to

<h1>YUI demo</h1>

<p>This project demonstrates how to use the YUI library inside a Grok Project.</p>

The Result

Now fire up your server, add your app and browse to it.

It should look like this:

site_p1.jpg

You see from the code that the view never refers to the layout, yet when rendered, it becomes part of your layout.

Congratulations, you got YUI working under Grok!

[1]The YUI website: http://developer.yahoo.com/yui/

Adding a YUI menu

We'll first define a menu using megrok.navigation, and then override the default template to make it YUI compatible. Then call some YUI js code to make it work.

Defining the Menu

For this part of the tutorial, you should have some basic understanding of what megrok.navigation does. Check the megrok.navigation page on PyPI.

The menus created by megrok.navigation are viewletmanagers, and the items are viewlets, so if you define a menu, you are in fact defining a viewletmanager. Create a new source file 'menu.py', and add this code

import grok
from megrok import navigation

class MainMenu(navigation.Menu):
        grok.name('main-menu')

Edit 'layout.pt', and replace

<div id="header" role="navigation">Navigation will go here!</div>

with

<div id="header" role="navigation" tal:content="structure provider:main-menu"></div>

Edit 'app.py' by adding some imports and changing the Index view definition to

from megrok import navigation
from menu import MainMenu

class Index(layout.Page):
    navigation.sitemenuitem(MainMenu, 'Home', order=-1)

We created a menu, called it 'main-menu', told the layout template to render it at the top of the page, and added the main index view as the home link, setting its order to -1 to have it rendered first (the default order is 0).

Restart your server and take a look.

site_p2a.jpg

Not exactly state-of-the-art, is it? The default templates of megrok.navigation just render an unordered list (<ul>), and there is no CSS or javascript in place to do anything fancy. A YUI menu requires a bit more than just an <ul>, so we need to tell megrok.navigation to use a different template.

Overriding the templates

megrok.template makes this an easy job. Go back to 'menu.py' and change the menu definition, and add 2 page templates

from megrok import pagetemplate
from hurry import yui

class MainMenu(navigation.Menu):
    grok.name('main-menu')

    def update(self):
        super(MainMenu, self).update()
        yui.menu.need()

    id = 'main-menu'
    cssClass='yuimenubar yuimenubarnav'
    cssItemClass='yuimenubaritem'
    cssItemLabelClass='yuimenubaritemlabel'

class MenuTemplate(pagetemplate.PageTemplate):
    grok.template('menu')
    pagetemplate.view(navigation.interfaces.IMenu)

class ItemTemplate(pagetemplate.PageTemplate):
    grok.template('item')
    pagetemplate.view(navigation.interfaces.IMenuItem)

The menu definition overrides the update method to tell hurry.yui that it need() s its menu resource library. Don't forget to call the super classes update when you invoke need() here, as a menu is a viewletmanager, and as such is required to call its viewlets' update method. Create a directory 'menu_templates', and add a file 'menu.pt' with this content:

<div tal:attributes="id viewletmanager/id|default;class viewletmanager/cssClass"
        tal:define="viewlets viewletmanager/viewlets"
        tal:condition="viewlets">
        <div class="bd">
                <ul class="first-of-type">
                        <tal:repeat tal:repeat='viewlet viewlets'
                            tal:replace='structure viewlet/render'/>
                </ul>
        </div>
</div>

Also, add a file 'item.pt' with this content:

<li  tal:attributes="class viewletmanager/cssItemClass"
        tal:define="submenu viewlet/subMenu | nothing">
  <a href="#"
     tal:attributes="href viewlet/link; class viewletmanager/cssItemLabelClass">
     <img tal:condition="viewlet/icon | nothing" tal:attributes="src viewlet/icon;
                                                                     title viewlet/title"/>
     <span tal:content="viewlet/title">Title</span></a>
  <tal:block condition="submenu"
             tal:replace="structure provider:${submenu}">sub menu items</tal:block>
</li>

All we did now was making sure the menu is rendered in a YUI compatible manner, using DIV and UL elements, and setting the right CSS classes. When overriding the megrok.navigation templates, you should bear two things in mind:

  • don't forget to render the viewlets (the structure viewlet/render TALES expression).
  • if you have submenus, render them in the item after the title (using a structure provider:$viewlet/subMenu statement or equivalent)

Letting YUI loose

So far, so good, but the menu still looks the same. Let's get YUI to do it's magic with it! We need to add a piece of javascript to the page. It's common practice to do that at the bottom of the page. We could just add it to the layout, but there is a more elegant way: add a viewletmanager to the bottom of the layout, and add scripts as viewlets, so the layout doesn't have to know what is needed.

Add this line to 'layout.pt' just above the </body> tag:

<tal:block tal:replace="structure provider:scripts"/>

Edit 'layout.py', and add this:

class Scripts(grok.ViewletManager):
    grok.context(Interface)

Now, in 'menu.py' add this:

from layout import Scripts
from zope.interface import Interface

class MenuScript(grok.Viewlet):
    grok.viewletmanager(Scripts)
    grok.context(Interface)
    grok.template('script')

And add a file 'script.pt' in the 'menu_templates' dir containing

<script type="text/javascript">
    YAHOO.util.Event.onContentReady("main-menu", function () {
        var oMenuBar = new YAHOO.widget.MenuBar("main-menu", {
                                                    autosubmenudisplay: true,
                                                    hidedelay: 750,
                                                    lazyload: true });
        oMenuBar.render();
    });
</script>

Now there is still one important part missing: the default YUI skin called 'sam'. First of all, you need to activate it by adding an attribute 'class' with the value 'yui-skin-sam' to the <body> tag in 'layout.pt':

<body class="yui-skin-sam">

But the skin CSS also needs to be loaded. A quick way is to load the entire sam skin by adding a yui.sam.need() call in the layout's update. But this loads the styles for all YUI components. That makes up for quite a hefty file, whereas we're only interested in the menu part. YUI also provides separate skin files for each component. We'll use such a separate file, but again, we're going to do it using a viewletmanager.

Add this to the 'layout.pt's <head> element just between the <title> and <link> elements:

<tal:block tal:replace="structure provider:stylesheets"/>

And add this class to 'layout.py':

class StyleSheets(grok.ViewletManager):
    grok.context(Interface)

Now edit 'menu.py' to add

from layout import StyleSheets

class MenuStylesheet(grok.Viewlet):
    grok.viewletmanager(StyleSheets)
    grok.context(Interface)
    template = grok.PageTemplate('<link rel="stylesheet" type="text/css" '
                   'tal:attributes="href context/++resource++yui/menu/assets/skins/sam/menu.css" />')

We took a shortcut here in order not to have to create a separate .pt file for 1 line of code.

Now restart the server and take a look.

site_p2b.jpg

Looks a lot better, doesn't it?

Some more fun!

Let's have some more fun. Let's pretend we are creating (yet another) blogging app. Add a new menu in 'menu.py':

class BlogMenu(navigation.ContentMenu):
    grok.name('blog-menu')

    def update(self):
        super(BlogMenu, self).update()
        yui.menu.need()

    def getContent(self):
        return grok.getSite().values()
    def getTitle(self, entry):
        return entry.title

    id = 'blog-menu'
    cssClass='yuimenu yuimenunav'
    cssItemClass='yuimenuitem'
    cssItemLabelClass='yuimenuitemlabel'

and add this directive to the MainMenu class definition

navigation.submenu('blog-menu', 'Blogs')

Here we created a 'ContentMenu': a menu linking to site content, which is a specialization of a normal menu. A standard menu links to other views of the current context (navigation.menuitem) or of the site (navigation.sitemenuitem). A ContentMenu links to views (typically the 'index' view) of arbitrary (but locatable) objects you choose in the getContent() method. We then add the Blog menu as a submenu (navigation.submenu) to the Main menu so that it will appear as a typical desktop application style drop-down menu.

Create 'blog.py' containing

import grok
from zope.interface import Interface
from zope import schema
from megrok import layout, navigation
from urllib import quote_plus
from menu import BlogMenu
from cgi import escape

class IBlogEntry(Interface):
    title = schema.TextLine(title=u'title')
    text = schema.Text(title=u'Body')

class BlogEntry(grok.Model):
    grok.implements(IBlogEntry)

    @property
    def htmltext(self):
        return escape(self.text).replace('\n', '<br/>');

class BlogIndex(layout.Page):
    grok.name('index')

class Add(layout.AddForm):
    navigation.sitemenuitem(BlogMenu, order=-1)
    grok.title('Add a Blog Entry')
    grok.context(grok.Application)
    form_fields = grok.Fields(IBlogEntry)

    @grok.action('Add entry')
    def Add(self, **data):
        entry = BlogEntry()
        self.applyData(entry, **data)
        grok.getSite()[quote_plus(entry.title)] = entry
        self.redirect(self.url(entry))

Create a directory called 'blog_templates and add 'blogindex.pt', containing

<h1 tal:content="context/title"></h1>
<p tal:content="structure context/htmltext"></p>

Restart the server and have a look. Add a few blog entries to check it out.

site_p2c.jpg

Well done, you implemented a YUI menu in Grok!

Of course, you can skin the menu all you like, but that's beyond the scope of this tutorial, just check the YUI docs on how to do that.

Introducing AJAX

Up until now we haven't done anything AJAXy, however. We've used some JavaScript to make the page look a bit more dynamic, but that's about it.

What is AJAX?

AJAX stands for Asynchronous JavaScript And XML. A non-AJAX page works synchronously, in the sense that you request a page, read it, click a link or submit a form, and the entire page is refreshed. Repeat ad infinitum. However, in most cases a large part of the new page is exactly the same as the previous page: headers, footers, menus and perhaps more. So the server is actually doing a lot of work for something that only changed partly. And a lot of network bandwidth is wasted too. If only you could just update the needed parts...

This is exactly what AJAX was invented for, and why it is said to be asynchronous: you do something on the page which triggers a request to the server that only returns some data or a part of the page, which is then dynamically inserted in the page. Multiple requests would be pending, and the responses need not arrive in the order of the requests. The request and the update are usually done using JavaScript, although technically other scripting languages could be used, too (including Python!).

That explains the A and the J, but what about XML? In the initial definition it was specified that XML was to be used for data interchange between client and server, and for that purpose the XMLHttpRequest object was introduced. Since then however, it has become clear that XML is not a requirement: you could as well send plain text, pre-formatted HTML or format your data as JSON (JavaScript Object Notation, see below); it all depends on how you handle the data. So the X stands for whatever rather than XML, nowadays.

A quick note on JSON: JSON is a very lightweight way of defining an object in plain text. It is much easier to decode or produce JSON data than XML data: e.g. calling eval('<json-text>') in JavaScript will result in a JavaScript object. JSON also very closely matches a text-representation of a Python dictionary (if there really IS a difference?).

Getting the AJAX feeling

The first thing we're going to do with AJAX is using the YUI TabView, which can use AJAX under the hood to load the contents of the tabs, so you can have a feeling of what AJAX can do without having to do too much 'plumbing' yourself. We'll change the way a blog entry is presented with tabs: one for viewing the entry, and one for viewing meta data (Dublin Core).

A TabView can be used to display contents that are in the HTML to start with, or as we want, it can load the contents from an 'external source': a separate view on our server. However, in that case it also needs the YUI Connection Manager to be loaded, so we shouldn't forget that!

We'll go for the pre-formatted HTML approach, so the current 'blogindex.pt' has to be displayed inside the View tab, but using the BlogIndex will cause the layout to be loaded too, so we'll have to make a separate view that doesn't show layout and change the original 'blogindex.pt' to display the TabView.

Copy 'blogindex.pt' to 'blogview.pt'. Create a grok View called BlogView in 'blog.py', and specify that BlogIndex now needs yui.tabview and yui.connection like this:

from megrok import resource
from hurry import yui

class BlogIndex(layout.Page):
    grok.name('index')
    resource.include(yui.tabview)
    resource.include(yui.connection)

class BlogView(grok.View):
    grok.name('view')

Note that we derive from grok.View, and not layout.Page!

We also used another way of specifying needed resources. Instead of using the need() function, you can also use megrok.resource's include directive. There's a catch, however: this directive only works with objects you actually traverse to: in this case we traverse over our app object to a BlogEntry object and finally to the BlogIndex view. In case of our Layout object, this won't work, because you never traverse to your layout, it is merely looked up in code when needed by a Page to which you did traverse. So be careful with this!

Now edit 'blogindex.pt'

<div id="blog-view" class="yui-navset">
    <ul class="yui-nav">
    </ul>
    <div class="yui-content">
    </div>
</div>

We need some JavaScript to hook it up and load the appropriate CSS. Add a script and stylesheet viewlet to the file 'blog.py':

from layout import Scripts, StyleSheets

class BlogIndexScript(grok.Viewlet):
    grok.view(BlogIndex)
    grok.viewletmanager(Scripts)
    grok.context(Interface)
    grok.template('script')

class BlogIndexStylesheet(grok.Viewlet):
    grok.viewletmanager(StyleSheets)
    grok.context(Interface)
    template = grok.PageTemplate('<link rel="stylesheet" type="text/css" '
         'tal:attributes="href '
                'context/++resource++yui/tabview/assets/skins/sam/tabview.css" />')

and the associated 'script.pt' template in the 'blog-templates' directory:

<tal:tag tal:replace='structure string:<script type="text/javascript">' />
        var myTabs = new YAHOO.widget.TabView("blog-view");
        myTabs.addTab(new YAHOO.widget.Tab(
                {label:'View',
                 dataSrc: '<tal:tag tal:replace="python:view.url(context, 'view')"/>',
                 active: true})
                    );
        myTabs.addTab(new YAHOO.widget.Tab(
                {label:'Meta Data',
                 dataSrc: '<tal:tag tal:replace="python:view.url(context, 'meta')"/>'})
                    );

<tal:tag tal:replace="structure string:</script>" />

You might be wondering why we didn't just use normal <script> elements. This is because we need some dynamic parts in the script: the urls to the views. ZPT will not parse anything that is placed in <script> (and <style>) elements because typically that is application code that should not be parsed. Maybe that wasn't such a good idea after all, but this is how ZPT works, so we have to deal with it. Another workaround would be to add a funtion to the viewlet that returns the JavaScript as a string and use the tal:content expression on the script tag. Or overwrite the render method and do it all by yourself.

The only thing left now is the meta-data view in 'blog.py'.

class MetaDataView(grok.View):
    grok.name('meta')

and 'metadataview.pt' in 'blog-templates'

<span tal:define="created context/zope:created;
                  modified context/zope:modified;
                  formatter python:request.locale.dates.getFormatter('dateTime')">
        <dl>
        <span tal:condition="created" > <dt>Created</dt>
            <dd tal:content="python:formatter.format(created)"></dd></span>
        <span tal:condition="modified" >        <dt>Modified</dt>
            <dd tal:content="python:formatter.format(modified)"></dd></span>
        </dl>
</span>

We're using the Zope implementation of Dublin Core, which is a de facto industry standard for meta-data in document-oriented systems. I won't go any further into that, as it has nothing to do with JavaScript or AJAX.

The Result

Now restart the server and check it out.

site_p3.jpg

Each time you switch tabs, the content is loaded from the server. You can also change the behaviour, such that the contents are only loaded once, by setting the 'cacheData' configuration option of the tab to true. But we specifically don't want that behaviour, or the rest of the tutorial won't work!

Return to sender

All we've done now is requesting data from the server. How about sending some to the server? We'll add an edit tab with a form that posts the data without reloading the entire page.

The EditForm

In 'blog.py' we'll need to define an edit form, but we can't just use a megrok.layout.EditForm, because we don't want the layout to appear again in our tab. We'll solve this here by using an ordinary grok.EditForm to start with; further below we'll see another trick, using layers and skins.

class Edit(grok.EditForm):
    grok.context(IBlogEntry)
    form_fields = grok.Fields(IBlogEntry)

    @grok.action('Save Entry')
    def Save(self, **data):
        self.applyData(self.context, **data)
        self.redirect(self.url(self.context))

This will work as expected, but of course it will post the data and refresh the entire page. We'll need to hack the form's onsubmit handler to stop doing the default, and do our bidding. There's one catch: the form doesn't exist when the page is loaded: you're looking at the View tab, and the form is only loaded when the Edit tab is selected, so we can only rewire the submit code once the contents of the Edit tab changed.

add this code within the <script> element of the blog's 'script.pt' (below the myTabs.addTab() function that adds the 'Meta Data' tab):

myTabs.addTab(new YAHOO.widget.Tab(
           {label:'Edit',
            dataSrc: '<tal:tag tal:replace="python:view.url(context, 'edit')"/>'})
                    );

var submitcallback = {
  success: function(o) {myTabs.selectTab(0);},
  failure: function(o) {editTab.content='Oops something went quite wrong...'}
};

var editTab = myTabs.getTab(2);
editTab.addListener('contentChange',
function(e)
{
                var formObject = document.forms[0];
                YAHOO.util.Event.on(formObject,'submit', function (e) {
                        YAHOO.util.Event.stopEvent(e);
                        YAHOO.util.Connect.setForm(formObject);
                        YAHOO.util.Connect.asyncRequest('POST', formObject.action, submitcallback);
                });
        }
);

We first added the new tab. We then defined a callback object containing functions in case of a success or failure. In case of success, we want to return to the view tab, otherwise we set the contents to an error message.

NOTE: we don't take a successful submit with invalid data into account here!

Then we attached a listener to the contentChange event of our Edit tab, which rewires the submit handler not to do only the default submit (YAHOO.util.Event.stopEvent(e)), but also loads the form data from the form and sends it with an asynchronous request.

Try it out!

Of course, this is a rather quick 'n dirty way to handle it, as there still are quite a lot of situations that aren't handled correctly, but it basically works, and that's what this tutorial is about.

It has so many layers!

We created an EditForm to be used inside the TabView. You can browse to it, but you won't have the layout, since it is not derived from megrok.layout.Page, but if we do that, it would render the entire layout inside the TabView again.

However, we can solve this with layers and skins! Remember that rendering a view derived from megrok.layout.Page will look up a megrok.layout.Layout for the current layer and render itself in it. I specifically mentioned this for this reason: we can override the layout in another skin to just render the page and no other html around it.

So let's define a layer and skin and a layout as part of this skin in 'layout.py':

from zope.publisher.interfaces.browser import IDefaultBrowserLayer

class IAJAXLayer(IDefaultBrowserLayer):
    grok.skin('ajax')

class AJAXLayout(layout.Layout):
    grok.layer(IAJAXLayer)
    grok.context(Interface)
    template=grok.PageTemplate('<tal:tag tal:replace="structure view/content"/>')

We derive the layer from IDefaultBrowserLayer and not from IBrowserRequest, so that all views can still be looked up in both skins, since Grok registers all views to IDefaultBrowserLayer (which is derived from IBrowserRequest) by default.

The template just renders the Page content and nothing else.

Edit 'blog.py' and make BlogView and MetaDataView derive from layout.Page and Edit from layout.EditForm.

Restart the server, and try the edit form with and without the skin: if your url was

http://localhost:8080/yuidemo/Well+hello+there/edit

before, which should return the edit form in the layout without the TabView, change it to

http://localhost:8080/++skin++ajax/yuidemo/Well+hello+there/edit

to activate the skin, and you'll get just the form markup as text.

If you now go back to the index page. You'll see the problem I mentioned before: the urls for the Tabs still refer to the default skin views and rerender the layout inside the Tab. There's only one problem: there is no generic way (yet?) to generate a url to a different skin! How can we know it's an AJAX request then? Well, AJAX requests will have the 'X-Requested-With' HTTP header set to 'XMLHttpRequest', and we can apply the skin based on that!

Add this to 'layout.py':

from zope.app.publication.interfaces import IBeforeTraverseEvent
from zope.interface import alsoProvides

@grok.subscribe(grok.Application, IBeforeTraverseEvent)
def handle(obj, event):
    if event.request.getHeader('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
        alsoProvides(event.request, IAJAXLayer)

Now the index page will look and behave again as it did before, but the separate pages will also work. To make it more clear, you can now make the other views available in the menubar in 'menu.py':

class ActionMenu(navigation.Menu):
    grok.name('actions-menu')

    id = 'action-menu'
    cssClass='yuimenu yuimenunav'
    cssItemClass='yuimenuitem'
    cssItemLabelClass='yuimenuitemlabel'

and also add

navigation.submenu('actions-menu', 'Actions', order=2)

to the MainMenu definition.

Finally, add navigation.menuitem directives to the view, meta-data and edit views in 'blog.py' like:

navigation.menuitem(ActionMenu, 'View', order=-1)

Don't forget to add

from menu import ActionMenu

Well, restart the server, and tell us: what do you think about that!?

JSON meets the Panel

Finally, we'll see how to use JSON for data interchange in Grok. We'll also see how to use a YUI Panel in the process.

Flash messages

To demonstrate JSON, we'll be using flash messages. Flash messages are short messages that you can 'send' to the user from somewhere inside your code -- typically when a new object was created or something similar -- and then show them on the page rendered next. See the z3c.flashmessage package on PyPI for more info.

We'll be periodically checking the server for new messages, and display them in a Panel that will popup.

Grok 1.0 uses z3c.flashmessage by default and registers a z3c.flashmessage.interfaces.IMessageSource utility A grok.View has a flash method for this purpose, however, in megrok.layout v0.9, megrok.layout.Page is not derived from grok.View, so it does not have the flash. So we will need to brew our own function for that.

We'll put all our message code in a 'message.py' file:

import grok
from zope.interface import Interface
from hurry import yui
from layout import StyleSheets, Scripts

if you're using Grok v1.0, also define this:

from zope import component
from z3c.flashmessage.interfaces import IMessageReceiver, IMessageSource

def flash( message, type='message'):
    source = component.getUtility(IMessageSource, name='session')
    source.send(message, type)

This is the flash function to be used whenever we see fit. It just gets the IMessageSource utility that is registered by Grok, and sends the message to it. You can specify a type to categorize your messages.

class MessagesScript(grok.Viewlet):
    grok.viewletmanager(Scripts)
    grok.context(Interface)
    grok.template('script')

    def update(self):
        yui.json.need()
        yui.container.need()
        yui.dragdrop.need()

    @property
    def messageurl(self):
        return self.view.url(grok.getSite(), 'messages')

class PanelStylesheet(grok.Viewlet):
    grok.viewletmanager(StyleSheets)
    grok.context(Interface)
    template = grok.PageTemplate('<link rel="stylesheet" type="text/css" '
            'tal:attributes="href '
            'context/++resource++yui/container/assets/skins/sam/container.css" />')

Here we loaded the script and tell hurry.yui that it needs to load JSON, Container and DragDrop from the YUI library. DragDrop is needed if we want our messagebox to be movable. There is also a messageurl property method that returns the url to our JSON view which we will define later.

We also loaded the container CSS file provided with YUI.

JSON

These are the contents of the 'script.pt' file (in the 'message_templates' dir):

<div id="messages">
        <div class="hd">Messages</div>
        <div class="bd"></div>
        <div class="ft"></div>
</div>
<tal:tag tal:replace="structure string:<script type='text/javascript'>" />
    messagePanel = new YAHOO.widget.Panel("messages",
                { constraintoviewport:true,
                  visible: false,
                  context: ['body', 'bl', 'bl']
                });
    messagePanel.render();
    function getMessages()
    {
                var callback = {
                  success: function(o) {
                                var messages = YAHOO.lang.JSON.parse(o.responseText);
                                for (i in messages)
                                {
                                        dl = document.createElement('dl');
                                        var msg = messages[i];
                                        dl.innerHTML='<dt>'+ msg.type+'</dt><dd>'+msg.message+'</dd>';
                                        o.argument.appendToBody(dl);
                                        o.argument.show();
                                }
                        },
                  timeout: 3000,
                  argument: this
                };
                YAHOO.util.Connect.asyncRequest('GET',
                                '<tal:tag tal:replace="viewlet/messageurl"/>',
                                callback,
                                null);
    }
    YAHOO.lang.later(5000, messagePanel, getMessages, null, true);
    messagePanel.hideEvent.subscribe(function(type, args, panel)
                    {
                        panel.setBody('')
                    },
                    messagePanel);
<tal:tag tal:replace="structure string:</script>" />

We first defined the HTML code that defines a default YUI Module (of which a Panel is a specilization). We then created a Panel object that refers to that code, and we set some configuration options, amongst which that it should be hidden at the start.

Then we defined the getMessages() function that queries the server for new messages, and if there are, we parse them into a JavaScript object using the YAHOO.lang.JSON.parse function, cast them into HTML and append them to the Panel body and then we show the Panel.

We then wired this function to be called every 5 seconds.

Finally, we attached an event handler to the hideEvent of the Panel, that clears the Panel's body.

Now we are still missing one bit: the JSON view:

class JSONMessages(grok.JSON):
    grok.context(grok.Application)

    def messages(self):
        receiver = component.getUtility(IMessageReceiver)
        return [{'message':m.message, 'type':m.type} for m in receiver.receive()]

A grok.JSON view makes all its (public) methods traversable, and those methods should return something that can be turned into JSON data with the simplejson Python package. In our case, it's an array of dictionaries containing the message data and the type of message.

Messaging

Now we have everything in place to display messages, but we don't send any. Let's edit 'blog.py':

for grok 1.0, add this

from message import flash

In the Add method of the AddForm, add this before the redirect:

self.flash("New blog entry '%s' added!" % entry.title, type='blog')

And in the Save method of the EditForm, add this before the redirect:

self.flash("Blog entry '%s' updated!" % self.context.title, type='blog')

for Grok 1.0, omit the 'self.'. So, whenever we edit or add a blog, a message will be sent.

Restart the server, refresh your page and have a blast!

site_p5.jpg

Conclusion

What have we learned?

By now you should have a rather thorough understanding on how you can use the YUI Library in a Grok project. The main focus of YUI is not AJAX itself, but providing user interface components that can be easily reused and that work on all browsers. It uses AJAX for some of its functionality, but it is only a small fraction of it.

There are other packages out there (JQuery is very popular one for instance) that perhaps are better suited for pure AJAX applications, but if you understand how to use YUI, you won't have a hard time incorporating other libraries into your projects.

Source code for the final project can be downloaded from http://github.com/jmichiel/YUIdemo