
Traditionally information is gathered on a website with an HTML form. In some cases we may want to respond quickly to user input, requiring a lighter weight process than traditional form submission. With a little JavaScript & Ajax we can do away with the form to create highly responsive websites. This article explains how to make HTML elements clickable, editable, and savable with the Prototype JavaScript library. Learn how to gather user input and give immediate feedback without submitting a form.
Part one of this article describes how to transform HTML elements so they can be modified by users directly on the page. Part two will describe how to store those changes in a database with Ajax and give example uses of this code.
Written by Jen on October 12, 2009
The Prototype library is a lightweight JavaScript library that streamlines the behavior of JavaScript in all browsers, making DOM manipulation and Ajax requests a piece of cake. Prototype is used heavily throughout this example but if you've never used Prototype, don't worry. This tutorial will walk you through it and provide links to documentation for more information on various methods and modules.
To get started, download a copy of Prototype from http://www.prototypejs.org/download. Create two files, editor.js and index.html, and place them in a folder along with prototype.js. Open index.html in your favorite text editor and add the following code, which includes links to prototype.js and editor.js:
<!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"/>
<script src="prototype.js" type="text/javascript"></script>
<script src="editor.js" type="text/javascript"></script>
<title>untitled</title>
</head>
<body>
</body>
</html>
In order to make elements editable, we need a way to find them from within our JavaScript. We are going to identify editable elements by setting their class attribute to editable. Then we'll use Prototype to collect them into an array. Add the following lines to index.html:
<!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"/>
<script src="prototype.js" type="text/javascript"></script>
<script src="editor.js" type="text/javascript"></script>
<title>untitled</title>
</head>
<body>
<h1 class="editable">Hello world!</h1>
<h2 class="editable">Here is a sub-header</h2>
<p class="editable">Paragraph one</p>
<p class="editable">Paragraph two</p>
</body>
</html>
Add the following code to editor.js:
// when the page finishes loading, execute the findEditables function
Event.observe(window, 'load', findEditables);
function findEditables(event) {
// collect the elements with class="editable" into an array
var elements = $$('.editable');
alert(elements);
}
Take a look at $$('.editable'). I've made use of Prototype's $$( ) utility method, which, as the documentation states, takes an arbitrary number of CSS selectors (strings) and returns a document-order array of extended DOM elements that match any of them. I've used it to find all DOM elements with class attribute equal to editable.
Open index.html in your web browser. If we've done everything correctly, an alert window will pop up with a list of elements that have the editable class. Remove the alert line from the findEditables function.
Now that we have a way to find editable elements from within our JavaScript we will create an Editable class to hold the data and methods for an editable element. We will include a constructor called initialize, a createEditor method that will create and return a form textarea, and edit and save methods that will be used as click event handlers on editable elements. We will create instances of the Editable class from within the findEditables function. Add the skeleton for the Editable class to editor.js:
// when the page finishes loading, execute the findEditables function
Event.observe(window, 'load', findEditables);
function findEditables(event) {
// collect the elements with class="editable" into an array
var elements = $$('.editable');
// create a new Editable object for each element
elements.each(function(el){
new Editable(el);
});
}
/*
The Editable class transforms an html element so that
it can be edited when clicked.
*/
var Editable = Class.create({
/* constructor */
initialize: function(element) {
},
createEditor: function() {
},
edit: function(event) {
},
save: function(event){
}
});
We use Prototype's Class.create method to create the Editable class. When we call new Editable(el) from our findEditables function, the class's constructor initialize is fired. See the Class.create documentation for more details.
Also, you may have noticed the elements.each function in findEditables. When we used the $$( ) Prototype utility to gather the editable elements into an array, Prototype very conveniently extended that array to include methods from its Enumerable module. Enumerable includes a large set of methods that are used to manipulate collections such as arrays. The each function in the Enumerable module allows us to enumerate over all the elements in our array and apply a function to each one. We can quickly create an Editable object for every element in our Array. See the Enumerable documentation for more details.
Now add the body to the constructor:
// when the page finishes loading, execute the findEditables function
Event.observe(window, 'load', findEditables);
function findEditables(event) {
// collect the elements with class="editable" into an array
var elements = $$('.editable');
// create a new Editable object for each element
elements.each(function(el){
new Editable(el);
});
}
/*
The Editable class transforms an html element so that
it can be edited when clicked.
*/
var Editable = Class.create({
/* constructor */
initialize: function(element) {
this.element = $(element);
this.editor = this.createEditor();
this.boundEdit = this.edit.bind(this);
this.boundSave = this.save.bind(this);
this.element.observe('click', this.boundEdit);
},
createEditor: function() {
},
edit: function(event) {
},
save: function(event){
}
});
Inside the constructor we create several instance variables, bind the two event handlers to this instance of the Editable class, and set a click event handler on the editable element. In line 1 the editable element is stored in the instance variable this.element. In line 2 the createEditor method is called, which will return a form textarea that will be used to edit the element (more to come on that in a minute). Lines 3 and 4 bind this instance of the Editable class to the event handlers edit and save. If we did not use binding then within the edit and save methods, the this keyword would refer to the element that the event handler is set on, i.e. the editable element. Since our handlers are methods on an instance of the Editable class, we want this to refer to the Editable instance. If you are new to Prototype and Binding this can be a bit difficult to understand. For a more in depth explanation, look at the Event.observe documentation and scroll down to the section titled "The tricky case of methods that need this".
Add the body of the createEditor method:
// when the page finishes loading, execute the findEditables function
Event.observe(window, 'load', findEditables);
function findEditables(event) {
// collect the elements with class="editable" into an array
var elements = $$('.editable');
// create a new Editable object for each element
elements.each(function(el){
new Editable(el);
});
}
/*
The Editable class transforms an html element so that
it can be edited when clicked.
*/
var Editable = Class.create({
/* constructor */
initialize: function(element) {
this.element = $(element);
this.editor = this.createEditor();
this.boundEdit = this.edit.bind(this);
this.boundSave = this.save.bind(this);
this.element.observe('click', this.boundEdit);
},
createEditor: function() {
// find computed width and height of element we are editing
var dimensions = this.element.getDimensions();
var editor = new Element('textarea');
editor.setStyle({
width: dimensions.width+'px',
height: dimensions.height+'px'
});
return editor;
},
edit: function(event) {
},
save: function(event){
}
});
The createEditor method computes the dimensions of the element we are editing, creates a textarea with the same dimensions and returns it. This method is called from within the initialize constructor and assigns the return value (the textarea) to this.editor.
Add the body to the edit method:
// when the page finishes loading, execute the findEditables function
Event.observe(window, 'load', findEditables);
function findEditables(event) {
// collect the elements with class="editable" into an array
var elements = $$('.editable');
// create a new Editable object for each element
elements.each(function(el){
new Editable(el);
});
}
/*
The Editable class transforms an html element so that
it can be edited when clicked.
*/
var Editable = Class.create({
/* constructor */
initialize: function(element) {
this.element = $(element);
this.editor = this.createEditor();
this.boundEdit = this.edit.bind(this);
this.boundSave = this.save.bind(this);
this.element.observe('click', this.boundEdit);
},
createEditor: function() {
// find computed width and height of element we are editing
var dimensions = this.element.getDimensions();
var editor = new Element('textarea');
editor.setStyle({
width: dimensions.width+'px',
height: dimensions.height+'px'
});
return editor;
},
edit: function(event) {
this.element.stopObserving('click', this.boundEdit);
var content = this.element.innerHTML;
this.editor.value = content;
this.element.update(this.editor);
this.editor.focus();
Event.observe(document, 'click', this.boundSave);
},
save: function(event){
}
});
First, the click event handler is temporarily removed from the element to prevent the edit function from being called again. Next, the value of the editor (the textarea we created) is set to the content of the editable element. Then, the editor is placed inside the element and keyboard focus is set to the editor. If the editable element was a paragraph for example, it would now look like this:
<p class="editable">
<textarea>This is a paragraph</textarea>
</p>
Finally, the save method is set as an event handler on the Document object so that when the user clicks anywhere on the page, the save method will execute.
Next, add the body to the save method:
// when the page finishes loading, execute the findEditables function
Event.observe(window, 'load', findEditables);
function findEditables(event) {
// collect the elements with class="editable" into an array
var elements = $$('.editable');
// create a new Editable object for each element
elements.each(function(el){
new Editable(el);
});
}
/*
The Editable class transforms an html element so that
it can be edited when clicked.
*/
var Editable = Class.create({
/* constructor */
initialize: function(element) {
this.element = $(element);
this.editor = this.createEditor();
this.boundEdit = this.edit.bind(this);
this.boundSave = this.save.bind(this);
this.element.observe('click', this.boundEdit);
},
createEditor: function() {
// find computed width and height of element we are editing
var dimensions = this.element.getDimensions();
var editor = new Element('textarea');
editor.setStyle({
width: dimensions.width+'px',
height: dimensions.height+'px'
});
return editor;
},
edit: function(event) {
this.element.stopObserving('click', this.boundEdit);
var content = this.element.innerHTML;
this.editor.value = content;
this.element.update(this.editor);
Event.observe(document, 'click', this.boundSave);
},
save: function(event){
var eventElement = event.element();
if(eventElement.descendantOf(this.element) || eventElement == this.element) return;
Event.stopObserving(document, 'click', this.boundSave);
var content = $F(this.editor);
content = content.gsub(/[\r\n]/, '');
this.element.update(content);
this.element.observe('click', this.boundEdit);
}
});
In line 1 of the save method, we retrieve the element that the event occurred on, i.e. the element that the user clicked on. In line 2, we check to see if that element is the editable element or is a descendant of the editable element. If it is, we return because we don't want to execute the save method unless the user clicks outside of the element they are editing.
In line 3 the click event handler is removed from the Document object. In line 4, the content of the editor is stored in a local variable. $F( ) is another handy utility method provided by Prototype that returns the value of a form control (see the documentation for getValue). In our case it returns the value of the editor textarea. In line 5, we use a regular expression to remove new line characters from the content. Finally, the content of the editable element is updated with the new value and the click event handler is reset on the editable element. Reload index.html in your web browser and click on one of the editable elements. You can change the content and when you click away, the element is updated with the new content.
The Editable class is almost complete. Whenever input is collected on a web page we need to consider security. We don't want users to enter HTML into our editable elements because they could break the page layout or worse, execute malicious JavaScript on our page. To see the potential harm, load index.html in your browser and enter some HTML into an editable element. Depending on what you enter, you can break the editable elements and even the whole page. Now enter the following JavaScript into an editable element:
<script>
alert('Hello world');
</script>
When the element is saved the JavaScript executes on the page. To avoid these two scenarios we use Prototype's String.escapeHTML and String.unescapeHTML functions. Modify the save method as follows:
// when the page finishes loading, execute the findEditables function
Event.observe(window, 'load', findEditables);
function findEditables(event) {
// collect the elements with class="editable" into an array
var elements = $$('.editable');
// create a new Editable object for each element
elements.each(function(el){
new Editable(el);
});
}
/*
The Editable class transforms an html element so that
it can be edited when clicked.
*/
var Editable = Class.create({
/* constructor */
initialize: function(element) {
this.element = $(element);
this.editor = this.createEditor();
this.boundEdit = this.edit.bind(this);
this.boundSave = this.save.bind(this);
this.element.observe('click', this.boundEdit);
},
createEditor: function() {
// find computed width and height of element we are editing
var dimensions = this.element.getDimensions();
var editor = new Element('textarea');
editor.setStyle({
width: dimensions.width+'px',
height: dimensions.height+'px'
});
return editor;
},
edit: function(event) {
this.element.stopObserving('click', this.boundEdit);
var content = this.element.innerHTML;
content = content.unescapeHTML();
this.editor.value = content;
this.element.update(this.editor);
Event.observe(document, 'click', this.boundSave);
},
save: function(event){
var eventElement = event.element();
if(eventElement.descendantOf(this.element) || eventElement == this.element) return;
Event.stopObserving(document, 'click', this.boundSave);
var content = $F(this.editor);
content = content.gsub(/[\r\n]/, '');
content = content.escapeHTML();
this.element.update(content);
this.element.observe('click', this.boundEdit);
}
});
Now our web page can't be hacked with HTML or JavaScript and the Editable class is complete. Part 2 of the tutorial will describe how to use Ajax to store the new values in a database and provide some example uses of this code.