Edit-in-Place with Ajax
Back on day one we looked at using the Prototype library to take all the hard work out of making a simple Ajax call. While that was fun and all, it didn’t go that far towards implementing something really practical. We dipped our toes in, but haven’t learned to swim yet.
So here is swimming lesson number one. Anyone who’s used Flickr to publish their photos will be familiar with the edit-in-place system used for quickly amending titles and descriptions on photographs. Hovering over an item turns its background yellow to indicate it is editable. A simple click loads the text into an edit box, right there on the page.
Prototype includes all sorts of useful methods to help reproduce something like this for our own projects. As well as the simple Ajax GETs we learned how to do last time, we can also do POSTs (which we’ll need here) and a whole bunch of manipulations to the user interface – all through simple library calls. Here’s what we’re building, so let’s do it.
Getting Started
There are two major components to this process; the user interface manipulation and the Ajax call itself. Our set-up is much the same as last time (you may wish to read the first article if you’ve not already done so). We have a basic HTML page which links in the prototype.js
file and our own editinplace.js
. Here’s what Santa dropped down my chimney:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Edit-in-Place with Ajax</title>
<link href="editinplace.css" rel="Stylesheet" type="text/css" />
<script src="prototype.js" type="text/javascript"></script>
<script src="editinplace.js" type="text/javascript"></script>
</head>
<body>
<h1>Edit-in-place</h1>
<p id="desc">Dashing through the snow on a one horse open sleigh.</p>
</body>
</html>
So that’s our page. The editable item is going to be the <p>
called desc
. The process goes something like this:
- Highlight the area
onMouseOver
- Clear the highlight
onMouseOut
- If the user clicks, hide the area and replace with a
<textarea>
and buttons - Remove all of the above if the user cancels the operation
- When the Save button is clicked, make an Ajax POST and show that something’s happening
- When the Ajax call comes back, update the page with the new content
Events and Highlighting
The first step is to offer feedback to the user that the item is editable. This is done by shading the background colour when the user mouses over. Of course, the CSS :hover
pseudo class is a straightforward way to do this, but for three reasons, I’m using JavaScript to switch class names.
:hover
isn’t supported on many elements in Internet Explorer for Windows- I want to keep control over when the highlight switches off after an update, regardless of mouse position
- If JavaScript isn’t available we don’t want to end up with the CSS suggesting it might be
With this in mind, here’s how editinplace.js
starts:
Event.observe(window, 'load', init, false);
function init(){
makeEditable('desc');
}
function makeEditable(id){
Event.observe(id, 'click', function(){edit($(id))}, false);
Event.observe(id, 'mouseover', function(){showAsEditable($(id))}, false);
Event.observe(id, 'mouseout', function(){showAsEditable($(id), true)}, false);
}
function showAsEditable(obj, clear){
if (!clear){
Element.addClassName(obj, 'editable');
}else{
Element.removeClassName(obj, 'editable');
}
}
The first line attaches an onLoad
event to the window, so that the function init()
gets called once the page has loaded. In turn, init()
sets up all the items on the page that we want to make editable. In this example I’ve just got one, but you can add as many as you like.
The function madeEditable()
attaches the mouseover, mouseout and click events to the item we’re making editable. All showAsEditable
does is add and remove the class name editable
from the object. This uses the particularly cunning methods Element.addClassName()
and Element.removeClassName()
which enable you to cleanly add and remove effects without affecting any styling the object may otherwise have.
Oh, remember to add a rule for .editable
to your style sheet:
.editable{
color: #000;
background-color: #ffffd3;
}
The Switch
As you can see above, when the user clicks on an editable item, a call is made to the function edit()
. This is where we switch out the static item for a nice editable textarea. Here’s how that function looks.
function edit(obj){
Element.hide(obj);
var textarea ='<div id="' + obj.id + '_editor">
<textarea id="' + obj.id + '_edit" name="' + obj.id + '" rows="4" cols="60">'
+ obj.innerHTML + '</textarea>';
var button = '<input id="' + obj.id + '_save" type="button" value="SAVE" /> OR
<input id="' + obj.id + '_cancel" type="button" value="CANCEL" /></div>';
new Insertion.After(obj, textarea+button);
Event.observe(obj.id+'_save', 'click', function(){saveChanges(obj)}, false);
Event.observe(obj.id+'_cancel', 'click', function(){cleanUp(obj)}, false);
}
The first thing to do is to hide the object. Prototype comes to the rescue with Element.hide()
(and of course, Element.show()
too). Following that, we build up the textarea and buttons as a string, and then use Insertion.After()
to place our new editor underneath the (now hidden) editable object.
The last thing to do before we leave the user to edit is it attach listeners to the Save and Cancel buttons to call either the saveChanges()
function, or to cleanUp()
after a cancel.
In the event of a cancel, we can clean up behind ourselves like so:
function cleanUp(obj, keepEditable){
Element.remove(obj.id+'_editor');
Element.show(obj);
if (!keepEditable) showAsEditable(obj, true);
}
Saving the Changes
This is where all the Ajax fun occurs. Whilst the previous article introduced Ajax.Updater()
for simple Ajax calls, in this case we need a little bit more control over what happens once the response is received. For this purpose, Ajax.Request()
is perfect. We can use the onSuccess
and onFailure
parameters to register functions to handle the response.
function saveChanges(obj){
var new_content = escape($F(obj.id+'_edit'));
obj.innerHTML = "Saving...";
cleanUp(obj, true);
var success = function(t){editComplete(t, obj);}
var failure = function(t){editFailed(t, obj);}
var url = 'edit.php';
var pars = 'id=' + obj.id + '&content=' + new_content;
var myAjax = new Ajax.Request(url, {method:'post',
postBody:pars, onSuccess:success, onFailure:failure});
}
function editComplete(t, obj){
obj.innerHTML = t.responseText;
showAsEditable(obj, true);
}
function editFailed(t, obj){
obj.innerHTML = 'Sorry, the update failed.';
cleanUp(obj);
}
As you can see, we first grab in the contents of the textarea into the variable new_content
. We then remove the editor, set the content of the original object to “Saving…” to show that an update is occurring, and make the Ajax POST.
If the Ajax fails, editFailed()
sets the contents of the object to “Sorry, the update failed.” Admittedly, that’s not a very helpful way to handle the error but I have to limit the scope of this article somewhere. It might be a good idea to stow away the original contents of the object (obj.preUpdate = obj.innerHTML
) for later retrieval before setting the content to “Saving…”. No one likes a failure – especially a messy one.
If the Ajax call is successful, the server-side script returns the edited content, which we then place back inside the object from editComplete
, and tidy up.
Meanwhile, back at the server
The missing piece of the puzzle is the server-side script for committing the changes to your database. Obviously, any solution I provide here is not going to fit your particular application. For the purposes of getting a functional demo going, here’s what I have in PHP.
<?php
$id = $_POST['id'];
$content = $_POST['content'];
echo htmlspecialchars($content);
?>
Not exactly rocket science is it? I’m just catching the content
item from the POST and echoing it back. For your application to be useful, however, you’ll need to know exactly which record you should be updating. I’m passing in the ID of my <div>
, which is not a fat lot of use. You can modify saveChanges()
to post back whatever information your app needs to know in order to process the update.
You should also check the user’s credentials to make sure they have permission to edit whatever it is they’re editing. Basically the same rules apply as with any script in your application.
Limitations
There are a few bits and bobs that in an ideal world I would tidy up. The first is the error handling, as I’ve already mentioned. The second is that from an idealistic standpoint, I’d rather not be using innerHTML
. However, the reality is that it’s presently the most efficient way of making large changes to the document. If you’re serving as XML, remember that you’ll need to replace these with proper DOM nodes.
It’s also important to note that it’s quite difficult to make something like this universally accessible. Whenever you start updating large chunks of a document based on user interaction, a lot of non-traditional devices don’t cope well. The benefit of this technique, though, is that if JavaScript is unavailable none of the functionality gets implemented at all – it fails silently. It is for this reason that this shouldn’t be used as a complete replacement for a traditional, universally accessible edit form. It’s a great time-saver for those with the ability to use it, but it’s no replacement.
See it in action
I’ve put together an example page using the inert PHP script above. That is to say, your edits aren’t committed to a database, so the example is reset when the page is reloaded.
About the author
Drew McLellan is a developer and content management consultant from Bristol, England. He’s the lead developer for the popular Perch and Perch Runway content management systems, and public speaking portfolio site Notist. Drew was formerly Group Lead at the Web Standards Project, and a Search Innovation engineer at Yahoo!. When not publishing 24 ways, he keeps a personal site about web development, takes photos, tweets a lot and tries to stay upright on his bicycle.