Plone does great at in-place editing: navigate to the thing you want to edit, then click the button and edit it. However, this paradigm breaks apart as soon as there is a need for a page to have multiple editable areas—such as for a homepage or section landing page.
At Groundwire, we used to deal with this problem by creating an ongoing series of very similar hacky one-off templates: the sort of template that would have have several areas which each pulled in the content from some item in a hidden folder of page components. Unfortunately this approach did not scale very well: it was tedious for us to set up new templates, and it was cumbersome for editors to remember how everything was set up in order to successfully make changes.
Last year we worked on the Net Impact website which has a different multi-part layout for each section landing page, and we realized that we needed to come up with a better solution. The requirements:
- Someone writing a template should be able to define an editable area of that template very simply, by just adding a line to the template that specifies the name of the area.
- There should be support for different types of editable areas; each type may have different settings when editing the area.
- Editing an area should be triggered by a pencil icon that shows up while hovering over the area for users who have permission to edit the area,
- All this should be done in a way that is simple to reuse for new sites.
Tiles to the rescue
We realized right away that our requirements were very similar to the functionality provided by the Deco project's implementation of "tiles." Deco is an ambitious project to provide drag-and-drop layout capabilities within Plone. Deco as a whole was not mature enough for us to feel comfortable using it, but I knew that the tile rendering was one of the older and more mature parts of Deco, and we realized that it would not take a lot of effort to use tile rendering without the rest of Deco.
A tile is a snippet that can be inserted into a template as a div with a data-tile attribute, like this:
<div data-tile="/Plone/@@mytile" />
Then some machinery in the publisher provided by plone.app.blocks performs the following steps:
- It finds all the divs with a data-tile attribute (let's call them tile placeholders).
- For each one, it performs a subrequest to fetch the contents of the tile. Using a URI makes tiles very flexible: a tile could be a browser view, or it could come from some external system.
- The tile placeholder is replaced with the contents of the tile's body tag. If the tile has a head tag, its contents will be appended to the head of the page that includes the tile.
That's a great start! As it turns out, other parts of the tiles implementation also help support our use case:
- plone.tiles has a tile implementation which supports having multiple tile types. A tile turns out to basically be a browser view that also happens to have some associated data. (This is a lot like a portlet renderer, but one that can be added anywhere with a line in a template rather than needing to mess around with portlet managers.) Each type of tile can specify a different schema for its data, and that data can be persisted in different ways.
- plone.app.tiles provides an edit form that takes care of editing the data for a particular instance of a tile.
In practice: adding a rich text tile
So let's see how this plays out in practice. We are going to:
- Set up the basic tile rendering machinery.
- Implement a rich text tile that can be added anywhere, and that stores its contents in an annotation of the context where it is added.
- Make sure that editors see a pencil icon that brings up an modal overlay to edit the tile.
The basics
Okay, let's get the basics set up.
- Create a package that declares dependencies on: lxml, plone.app.blocks, plone.app.textfield, plone.app.tiles, and plone.tiles.
- At the time of this writing, you need trunk checkouts of plone.app.blocks, plone.app.tiles, and plone.tiles.
- Make sure that your configure.zcml includes
<includeDependencies package="."/>.
- Make sure the metadata.xml in your package's GenericSetup profile runs the default profiles from plone.app.blocks and plone.app.tiles as dependencies.
- Install your package.
The tile
Add a tile.py with the following:
from zope.interface import Interface from plone import tiles from zope.schema import Text from plone.app.textfield import RichText from plone.app.textfield.interfaces import ITransformer class IRichTextTileData(Interface): text = RichText(title=u'Text') class RichTextTile(tiles.PersistentTile): def __call__(self): text = '' if self.data['text']: transformer = ITransformer(self.context, None) if transformer is not None: text = transformer(self.data['text'], 'text/x-html-safe') return '%s' % text
In configure.zcml, add:
<plone:tile name="groundwire.tiles.richtext" title="Groundwire rich text tile" description="A tile containing rich text" add_permission="cmf.ModifyPortalContent" schema=".tile.IRichTextTileData" class=".tile.RichTextTile" permission="zope2.View" for="*" />
This defines a new tile type, called groundwire.tiles.richtext. This tile type has a schema with a single rich text field, and when it is rendered the tile will run the configured text through the safe HTML transform to make sure it is safe.
Wiring in the edit form
Now we just need to make sure that editors will have a way to access the edit interface for tiles.
Add the following javascript. Make sure you put a condition on it like "python:object.portal_membership.checkPermission('Modify portal content', object)" so that it will only run and add the edit links for users who have permission to edit.
jQuery(function($) { $('div[data-tile]').each(function() { $(this).addClass('tile-editable'); var href = $(this).attr('data-tile'); var edithref = href.replace(/@@/, '@@edit-tile/'); $('<a class="tile-edit-link" href="' + edithref + '"><img height="16" src="pencil_icon.png" width="16" />') .appendTo($(this)) .prepOverlay({ subtype: 'iframe', config: { onClose: function() { location.reload(); } } }); }); // Check if tiledata is available and valid if (typeof(tiledata) !== 'undefined') { // Check action if (tiledata.action === 'cancel' || tiledata.action === 'save') { // Close dialog window.parent.jQuery('.link-overlay').each(function() { try { window.parent.jQuery(this).overlay({api: true}).close(); } catch(e) { } }); } } });
This adds an edit link to all the divs that have data-tile attributes. It also handles the "tiledata" which is how the plone.app.tiles edit form controls when the overlay it appears in should close.
And finally we need a bit of CSS to style the tiles and edit links:
.tile-editable { position: relative; outline: 2px dashed #e8e8e8; min-height: 1.5em; } .tile-editable:hover { outline: 2px dashed #b8b8b8; } .tile-edit-link { display: none !important; position: absolute; right: 1px; bottom: 1px; z-index: 500; } .tile-editable:hover .tile-edit-link { display: block !important; }
Adding a tile
Okay, now let's add one of these to a template. Pick your favorite template and add:
<div tal:attributes="data-tile string:${context/absolute_url}/@@groundwire.tiles.richtext/hello-world" />
Here's what it looks like in my instance (I added it to the document_view template):
And here's the editing interface that shows up when I click on the pencil:
(If you want to see how this code all comes together, look at the code in https://groundwire.devguard.com/svn/public/groundwire.tiles/branches/davisagli-blocks)
In conclusion
We are very happy with the way the tile approach turned out for the Net Impact site. Once we had mastered the basic technique for landing pages, we soon realized that tiles provided a useful way to add user-editable content areas anywhere in the site. Site needs a doormat in the footer? Use a tile with the Plone site as its context so it appears the same throughout the site and the client can edit the links. Client wants a block of text they can edit on the login form to promote registering for the site? No problem, just add a tile. Client is repeatedly asking for minor edits to the text introducing a custom form? No problem, we turned it into a tile and told them how to edit it. Since the presentation of tiles in the UI is simple and consistent, the barrier to entry for the client to learn how to edit a new tile was very low.
The approach as described here isn't perfect. One thing that needs some care is cache invalidation. In our case, we wrote an ObjectModified event handler for tiles that updates the modified time of the page on which the tile appears. Another limitation is that text in tiles won't be included in the fulltext index unless you go to extra lengths. Whether that's a feature or a bug depends on your use case.
Overall though, we love the technique and have also started using tiles in other sites. I know that Six Feet Up has also successfully used tiles with at least one client. If you want to expand your Plone layout repertoire without using experimental technology like Deco or removing control over content from your clients, I encourage you to give it a try!