WikiGardening required — This document is out of date. Rails now includes facilities to greatily simplify this kind of feature. These include the Prototype Javascript library and a number of ajax helper methods.
XmlHTTPRequest, or “ajax”, is a great way to perform simple database actions without refreshing the whole page.
For this example let’s assume we have a scaffold-like list of items ("Animals") in a table like so:
| id | name | |
|---|---|---|
| 01 | monkey | remove |
| 02 | giraffe | remove |
| 03 | elephant | remove |
In the Animals view, you’d see something like this for the remove link:
<%= link_to 'remove',
:controller=>'animals',
:action=>'destroy',
:id=>animal.id %>
Note that you the Animals controller should use authentication, and the ‘destroy’ link should be shown only to authenticated users, otherwise TheGoogleSpider will come and delete your entire database! (it happens)
What we’d like to do is replace this code with some javascript so that you can just click “remove” and it is all done in the background.
The function calls the request, but because it takes a little time (or a lot of time, if you’re on WEBrick), there is a lag between clicking ‘remove’ and getting a response. There are three important points here.
We use callbacks for the first problem, and, while the request is active, change the relevant item’s text to “Removing…”
Here’s the javascript that you need to include in your /public/javascripts directory (note that there are many different ways of implementing XmlHTTPRequest, this is just one of them!).
The upcoming version of Rails(0.11) will include some nifty functions to handle a lot of this Ajaxy goodness, but until then, you’ll have to roll your own code. However, if you’re rolling on the trunk rails, you will be able to use the inbuilt helpers, so read this: How to use the Ajax helpers
There are four key functions:
function xmlRequest(url, obj, func)
{
if (!url) return false;
if (window.XMLHttpRequest)
var http = new XMLHttpRequest();
else if (window.ActiveXObject)
try { http = new <span class="newWikiWord">ActiveXObject<a href="http://wiki.rubyonrails.org/rails/pages/ActiveXObject">?</a></span>("Msxml2.XMLHTTP"); }
catch(e) { http = new <span class="newWikiWord">ActiveXObject<a href="http://wiki.rubyonrails.org/rails/pages/ActiveXObject">?</a></span>("Microsoft.XMLHTTP"); }
if (!http) return false;
if (func)
http.onreadystatechange = function(){
if (http.readyState != 4) return;
func(obj);
};
else
http.onreadystatechange = function() { return; }
http.open('GET', url, true);
http.send(null);
if (func) {} else return http.responseText;
return false;
}
function destroy(me)
{
if (!document.getElementById) return true;
// check for compatability
if (!me) return true;
// make sure you've passed something
if (!me.href) return true;
// make sure there's a link associated with 'me'
if (!confirm("Are you sure you want to remove this item?"))
return false;
// you can remove this line if you don't want confirmation.
//now, find the table row
parent = findParent(me, 'tr');
if (!parent) return true;
parent.cells[1].innerHTML = 'Removing..' + parent.cells[1].innerHTML;
xmlRequest(me.href, parent, hide);
}
function findParent(obj, tag)
{
if (!obj || !tag) return false;
while (!obj.tagName.match(/body|html/i)
// make sure we don't iterate too far
obj = obj.parentNode;
if (obj.tagName.toLowerCase() == tag.toLowerCase())
return obj; // found!
}
return false;
}
function hide(obj)
{
obj.style.display = 'none';
}
So there you have the javascript. Now, back in the Animals view, we want to change that link.
<%= link_to 'remove',
:controller=>'animals',
:action=>'destroy',
:id=>animal.id %>
To add in our javascript..
<%= link_to 'remove',
{ :controller=>'animals',
:action=>'destroy',
:id=>animal.id},
'onclick'=>'return destroy(this);' %>
Now, what if your data is not in a table, but in paragraphs.. or divs? You can easily change the findParent(me, ’tr’) to findParent(me, ’div’). Or, for extra points, provide findParent with a regular expression of possible tags.
Extending the function
This destroy script is a bit like a cruise missile— it’s fire-and-forget. There’s no checking for success, if the record still (or actually) exists, or if the file works.
To change this, modify the xmlRequest function like so:
if (func)
http.onreadystatechange = function(){
if (http.readyState != 4) return;
if (http.status == 200)
func(obj);
else
alert("An error occurred" + http.status);
};
Extending some more
You can actually return the HTML from the httprequest, and set some text in your application. For example, you might want to set the table row to “Item deleted.”
In the View, set your output with render_text, or, set the text in the .rhtml file. Make sure you have set the layout to nil, or your table row is going to be pretty messed up.
Then, modify the xmlRequest function again:
if (func)
http.onreadystatechange = function(){
if (http.readyState != 4) return;
if (http.status == 200)
func(obj, http.responseText);
else
alert("An error occurred" + http.status);
};
Then in the hide function:
function hide(obj, txt)
{
obj.cells[1].innerHTML = txt;
}
Stay tuned to find out how to add items to a database using the same functions!
Nice stuff, good explanation. One minor point though: isn’t it a bit dangerous having the original ‘destroy’ link as a normal hyperlink (i.e. using GET)? If a robot gets to the page it could potentially follow all the destroy links on the page and wipe out all your data! Better to use a POST and some kind of mini form in my opinion. Anything that changes data on the server should use a POST. — Goynang
Well, if your authentication is set up properly, it shouldn’t matter. Also, if you want to mimic \TaDa list functionality, you’d swap Destroy with your own function ("Done") —Court3nay
Please note that there are already some functions for this in the source trunk (like @link_to_remote@). Could be nicely mixed with this; maybe with a helper. – madrobby
WikiGardening required — This document is out of date. Rails now includes facilities to greatily simplify this kind of feature. These include the Prototype Javascript library and a number of ajax helper methods.
XmlHTTPRequest, or “ajax”, is a great way to perform simple database actions without refreshing the whole page.
For this example let’s assume we have a scaffold-like list of items ("Animals") in a table like so:
| id | name | |
|---|---|---|
| 01 | monkey | remove |
| 02 | giraffe | remove |
| 03 | elephant | remove |
In the Animals view, you’d see something like this for the remove link:
<%= link_to 'remove',
:controller=>'animals',
:action=>'destroy',
:id=>animal.id %>
Note that you the Animals controller should use authentication, and the ‘destroy’ link should be shown only to authenticated users, otherwise TheGoogleSpider will come and delete your entire database! (it happens)
What we’d like to do is replace this code with some javascript so that you can just click “remove” and it is all done in the background.
The function calls the request, but because it takes a little time (or a lot of time, if you’re on WEBrick), there is a lag between clicking ‘remove’ and getting a response. There are three important points here.
We use callbacks for the first problem, and, while the request is active, change the relevant item’s text to “Removing…”
Here’s the javascript that you need to include in your /public/javascripts directory (note that there are many different ways of implementing XmlHTTPRequest, this is just one of them!).
The upcoming version of Rails(0.11) will include some nifty functions to handle a lot of this Ajaxy goodness, but until then, you’ll have to roll your own code. However, if you’re rolling on the trunk rails, you will be able to use the inbuilt helpers, so read this: How to use the Ajax helpers
There are four key functions:
function xmlRequest(url, obj, func)
{
if (!url) return false;
if (window.XMLHttpRequest)
var http = new XMLHttpRequest();
else if (window.ActiveXObject)
try { http = new <span class="newWikiWord">ActiveXObject<a href="http://wiki.rubyonrails.org/rails/pages/ActiveXObject">?</a></span>("Msxml2.XMLHTTP"); }
catch(e) { http = new <span class="newWikiWord">ActiveXObject<a href="http://wiki.rubyonrails.org/rails/pages/ActiveXObject">?</a></span>("Microsoft.XMLHTTP"); }
if (!http) return false;
if (func)
http.onreadystatechange = function(){
if (http.readyState != 4) return;
func(obj);
};
else
http.onreadystatechange = function() { return; }
http.open('GET', url, true);
http.send(null);
if (func) {} else return http.responseText;
return false;
}
function destroy(me)
{
if (!document.getElementById) return true;
// check for compatability
if (!me) return true;
// make sure you've passed something
if (!me.href) return true;
// make sure there's a link associated with 'me'
if (!confirm("Are you sure you want to remove this item?"))
return false;
// you can remove this line if you don't want confirmation.
//now, find the table row
parent = findParent(me, 'tr');
if (!parent) return true;
parent.cells[1].innerHTML = 'Removing..' + parent.cells[1].innerHTML;
xmlRequest(me.href, parent, hide);
}
function findParent(obj, tag)
{
if (!obj || !tag) return false;
while (!obj.tagName.match(/body|html/i)
// make sure we don't iterate too far
obj = obj.parentNode;
if (obj.tagName.toLowerCase() == tag.toLowerCase())
return obj; // found!
}
return false;
}
function hide(obj)
{
obj.style.display = 'none';
}
So there you have the javascript. Now, back in the Animals view, we want to change that link.
<%= link_to 'remove',
:controller=>'animals',
:action=>'destroy',
:id=>animal.id %>
To add in our javascript..
<%= link_to 'remove',
{ :controller=>'animals',
:action=>'destroy',
:id=>animal.id},
'onclick'=>'return destroy(this);' %>
Now, what if your data is not in a table, but in paragraphs.. or divs? You can easily change the findParent(me, ’tr’) to findParent(me, ’div’). Or, for extra points, provide findParent with a regular expression of possible tags.
Extending the function
This destroy script is a bit like a cruise missile— it’s fire-and-forget. There’s no checking for success, if the record still (or actually) exists, or if the file works.
To change this, modify the xmlRequest function like so:
if (func)
http.onreadystatechange = function(){
if (http.readyState != 4) return;
if (http.status == 200)
func(obj);
else
alert("An error occurred" + http.status);
};
Extending some more
You can actually return the HTML from the httprequest, and set some text in your application. For example, you might want to set the table row to “Item deleted.”
In the View, set your output with render_text, or, set the text in the .rhtml file. Make sure you have set the layout to nil, or your table row is going to be pretty messed up.
Then, modify the xmlRequest function again:
if (func)
http.onreadystatechange = function(){
if (http.readyState != 4) return;
if (http.status == 200)
func(obj, http.responseText);
else
alert("An error occurred" + http.status);
};
Then in the hide function:
function hide(obj, txt)
{
obj.cells[1].innerHTML = txt;
}
Stay tuned to find out how to add items to a database using the same functions!
Nice stuff, good explanation. One minor point though: isn’t it a bit dangerous having the original ‘destroy’ link as a normal hyperlink (i.e. using GET)? If a robot gets to the page it could potentially follow all the destroy links on the page and wipe out all your data! Better to use a POST and some kind of mini form in my opinion. Anything that changes data on the server should use a POST. — Goynang
Well, if your authentication is set up properly, it shouldn’t matter. Also, if you want to mimic \TaDa list functionality, you’d swap Destroy with your own function ("Done") —Court3nay
Please note that there are already some functions for this in the source trunk (like @link_to_remote@). Could be nicely mixed with this; maybe with a helper. – madrobby