Return to sender
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!?

