Ajax progress indicator with prototype
Courtenay : March 26th, 2008
Today I'm going to show you how I combine my old-school Javascript with the latest Prototype has to offer. I hope you learn a little about the basic use and misuse of "bind". Hopefully someone will chime into the comments and give a better way to do this code!
You may know that you can add a global ajax responder to your application:
Ajax.Responders.register({
onCreate: function() {
Ajax.activeRequestCount++;
},
onComplete: function() {
Ajax.activeRequestCount--;
}
});
But I'm not going to cover that here, because you can't get the originating object. For example, there's no way here for a global ajax responder to get the anchor object.
<a class="add" href="#" onclick="new Ajax.Request('/products/3/categorizations/18', {asynchronous:true, evalScripts:true, method:'put'}); return false;">add</a>
Here's the rails generator code:
<%= link_to_remote "add", { :url => product_categorization_path(@product, category), :method => :put } %>
So, let's add some code to prevent the user from clicking twice.
To keep the view clean, I'll implement it as a simple view helper in application_helper.rb
<%= link_to_remote "add", { :url => product_categorization_path(@product, category), :before => ajax_progress, :method => :put } %>
def ajax_progress
"setTimeout(function(){ this.innerHTML = '...' }.bind(this), 100)"
end
SetTimeout takes either a function name or an anonymous function. I'm binding the anonymous function to "this", which in this context refers to the 'A' anchor tag. Because it's bound, I can refer to "this" inside the function and get the anchor tag.
The setTimeout is useful because it lets us modify the tag (even remove it from the DOM) without messing with the ajax request. You can use setTimeout on a form to change the action, so it can't be submitted twice (for important forms)
I'm going to step it up one more, because this code belongs in a library. Time to open up application.js and create a ghetto pseudoclass singleton thingy.
MyApp = {
/* do this while we're waiting for ajax responses */
ajaxing: function(){
this.innerHTML = '...';
this.onclick = FacetApp.nothing;
},
/* don't do anything. useful for descriptive declarations like $('a').onclick = FacetApp.nothing */
nothing: function() {
return false
}
}
This will prevent the link from doing anything if the user clicks it twice.
Now, to modify the ajax_progress helper to simple beauty.
def ajax_progress
"setTimeout(MyApp.ajaxing.bind(this), 100)"
end
Finally, let's show the user a GMail-style notice after 5 seconds, just to let them know that we're running slow or have just plain died.
MyApp = {
ajaxing: function(){
this.innerHTML = '...';
this.onclick = FacetApp.nothing;
setTimeout(MyApp.slooow.bind(this), 5000);
},
/* shows a ? symbol. useful for showing progress on an ajax action */
slooow: function(){
this.innerHTML = ".?."
/* show a warning message in the ui somewhere */
}
}
Got a better way of doing this?
safari 3.1 breaks your app
Courtenay : March 21st, 2008
If you're doing anything like
$('foo').getElementsByClassName("monkey").each(Element.hide)
Safari 3.1 out this week breaks this, because it now natively implements getElementsByClassName.
So, pending a fix from the Prototype guys, one of whom has a desk next to mine, be aware that your apps may be broken for your Safari users.
You can easily change that code to
$$("#foo .monkey").each(Element.hide)
implement sweet sparklines with plotr
Courtenay : May 6th, 2007
requires: plotr.
You have a bunch of data you want to plot in a sparkish fashion. Lets say they are sparse, one or two every 4 hours. So, lets pull some data clustered in 4-hour increments and spark it up.
Create the data like thusly:
distance = 4.hours # how much we are clustering the results
@sparks = @store.orders.find(:all,
:select => "count(id) as count, round(UNIX_TIMESTAMP(created_at)/#{distance}) as timestamp",
:group => "round(UNIX_TIMESTAMP(created_at) / #{distance})")
Install plotr javascripts, include them, then in your view dump the placeholder:
<div><canvas id="spark" width="510" height="50"></canvas></div>
And write some javascript
<% baseline = sparks[0]['timestamp'].to_i %>
<script type="text/javascript">
var dataset = { 'spark': [ <% @sparks.each do |spark| %>
[<%= spark['timestamp'].to_i - baseline %>, <%= spark.count %>],<% end %>
[]
]};
var spark = new Plotr.BarChart('spark', { colorScheme:'blue', yNumberOfTicks:0,xNumberOfTicks:0,padding:{left:0,right:30,top:0,bottom:10}});
spark.addDataset(dataset);
spark.render();
</script>
Mine looked something like this:

Ajax File Upload
Courtenay : April 2nd, 2007
Here’s how to upload without leaving the page. It’s not ajax, but it feels like it. Sorry for the misleading heading :)
First, install the respondsto_parent plugin. Read more about respondsto_parent This plugin lets you fake rjs from an iframe response. Doesn’t make sense? Don’t worry, it will later.
script/plugin install http://sean.treadway.info/svn/plugins/responds_to_parent
Now, in your page, let’s say it’s an article editing form.
<form method="post" action="/articles">
Title: <input type="text" name="article[name]" />
…
Forget about this form.
Add a new form somewhere like the sidebar, looks like this:
<div id="asset_list">
<%= render :partial => ‘asset’, :collection => @assets %>
</div>
<form method="post" action="/assets.js" target="background-uploader" enctype="multipart/form-data" id="background-upload-form">
File: <input type="file" name="asset[uploaded_data]" />
<input type="submit" value="Upload" />
</form>
<iframe width="1" height="1" name="background-uploader" src="about:blank"></iframe>
So, we have a multipart form that points to assets.js with a target, a 1x1 iframe, and a file field. These are all required. Optional, the div with the asset list, but it’s helpful for the user to see all the assets, or perhaps the most recent.
Open up your asset controller and modify the create action.
def create
@asset = Image.create! params[:asset]
respond_to do |wants|
wants.html { redirect_to asset_path(@asset) }
wants.js do
responds_to_parent do
render :update do |page|
page.insert_html :top, ‘asset_list’, render(:partial => ‘articles/asset’, :object => @asset)
page.visual_effect :highlight, “asset_#{@asset.to_param}”
page[‘background-upload-form’].reset
end # render
end # responds_to_parent
end # wants
end # respond_to
rescue ActiveRecord::RecordInvalid
# eep! you’re screwed!
end
What does this do? Well.. first, it creates the asset. Remember how we pointed the form to “assets.js”? This tells rails that we wants.js and rails sets the content-type accordingly. This has the benefit of allowing us to create assets with the same action regardless of iframe or as before.
Unfortunately, since this isn’t ajax, it’s going to dumping its contents as text in the iframe and it won’t work. Enter the responds_to_parent plugin, which does some magic and makes our rjs response activate as expected.
Inside the responds_to_parent block we insert the new asset partial in the top of the list, flash it so the user is aware, and reset the upload form.
And that’s it. Error checking is left to the reader :)
Ajax File Upload
Courtenay : April 2nd, 2007
Here’s how to upload without leaving the page. It’s not ajax, but it feels like it. Sorry for the misleading heading :)
First, install the responds-to-parent plugin. Read more about responds_to_parent This plugin lets you fake rjs from an iframe response. Doesn’t make sense? Don’t worry, it will later.
script/plugin install http://sean.treadway.info/svn/plugins/responds_to_parent
Now, in your page, let’s say it’s an article editing form.
<form method="post" action="/articles">
Title: <input type="text" name="article[name]" />
…
Forget about this form.
Add a new form somewhere like the sidebar, looks like this:
<div id="asset_list">
<%= render :partial => ‘asset’, :collection => @assets %>
</div>
<form method="post" action="/assets.js" target="background-uploader" enctype="multipart/form-data" id="background-upload-form">
File: <input type="file" name="asset[uploaded_data]" />
<input type="submit" value="Upload" />
</form>
<iframe width="1" height="1" name="background-uploader" src="about:blank"></iframe>
So, we have a multipart form that points to assets.js with a target, a 1x1 iframe, and a file field. These are all required. Optional, the div with the asset list, but it’s helpful for the user to see all the assets, or perhaps the most recent.
Open up your asset controller and modify the create action.
def create
@asset = Image.create! params[:asset]
respond_to do |wants|
wants.html { redirect_to asset_path(@asset) }
wants.js do
responds_to_parent do
render :update do |page|
page.insert_html :top, ‘asset_list’, render(:partial => ‘articles/asset’, :object => @asset)
page.visual_effect :highlight, “asset_#{@asset.to_param}”
page[‘background-upload-form’].reset
end # render
end # responds_to_parent
end # wants
end # respond_to
rescue ActiveRecord::RecordInvalid
# eep! you’re screwed!
end
What does this do? Well.. first, it creates the asset. Remember how we pointed the form to “assets.js”? This tells rails that we wants.js and rails sets the content-type accordingly. This has the benefit of allowing us to create assets with the same action regardless of iframe or as before.
Unfortunately, since this isn’t ajax, it’s going to dumping its contents as text in the iframe and it won’t work. Enter the responds_to_parent plugin, which does some magic and makes our rjs response activate as expected.
Inside the responds_to_parent block we insert the new asset partial in the top of the list, flash it so the user is aware, and reset the upload form.
And that’s it. Error checking is left to the reader :)
plugin watch: acts_as_dismissible, name nanny
Courtenay : February 21st, 2007
Mark Daggett, the very epitome of the artist-programmer, is the ultimate yak-shaver. You can set him onto a task and three days later, you’ll have four awesome plugins and half the task finished. He’s currently studying for his PHD in some arty computer thing, but in the meantime keeps putting out some quality code.
name nanny
Mark also likes coming up with silly names for his implementations, so you call name nanny in your model like
validates_wholesomeness_of :login
The plugin looks for any ‘bad’ words (which you define) and replaces them with the word, ‘bleep’. Or, with the word, ‘smurf’. Which is much more fun.
acts_as_dismissible
This useful plugin, which I originally spec’d for a shared client, gives users a box with a “dismiss” button, so they never need to see a particular message again, unless they clear their cookies.
Go check it out! http://locusfoc.us
optimizing prototype.js
Courtenay : June 8th, 2006
Object.extend(Array.prototype, {
_each: function(iterator) {
var iter = 0;
var len = this.length;
var i = len % 8;
if (i>0) do {
iterator(this[iter++]);
} while (--i);
i = parseInt(len >> 3);
if (i>0) do {
iterator(this[iter++]);
iterator(this[iter++]);
iterator(this[iter++]);
iterator(this[iter++]);
iterator(this[iter++]);
iterator(this[iter++]);
iterator(this[iter++]);
iterator(this[iter++]);
} while (--i);
//for (var i=0; i
You can test it with something like
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18].each(function(i) { print(i) } );
integrate rails modules with javascript objects
Courtenay : February 23rd, 2006
A Better Twisty
obie : February 23rd, 2006
1 2 3 4 5 6 7 8 9 10 11 |
<div class="Story Node" id="story_105"> <span class="Twisty">►</span> <div class="Name"><div>Node</div></div> <div class="Collapsible Details" style="display:none"> <div class="LastUpdated">Updated 7 minutes ago</div> <div class="Body"><div><p>Description</p></div></div> <div class="Story Node" id="story_106"> ... you get the picture I hope </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 |
var goal = { rules: { '.Collapsible' : function(details) { var twisty = details.parentNode.getElementsByTagName('span')[0]; twisty.onclick = function() { toggleTwisty(twisty, details); return false; } } } } Behaviour.register(goal.rules); |
function toggleTwisty(twisty, target) {
twisty.innerHTML = (target.style.display == 'none') ? '▼' : '►';
new Effect[Element.visible(target) ? 'BlindUp' : 'BlindDown'](target,
{ duration:.2, fps:50, queue:'end' })
}
The "dynamic method invocation using square brackets and a conditional" idiom of JS just blows my mind. Cryptic, but cool.
Assert yourself, man! -- redirecting with RJS
Courtenay : February 19th, 2006
def new
if request.xhr?
render :update { |page| page.redirect_to :action => 'create' }
else
redirect_to(:action => 'create')
end
end
render :update is the infamous RJS call, wherein it returns a document with text/javascript content-type. If you look into prototype.js, it automagically evals such documents. The response body will be pure javascript, baby, and it'll look something like 'window.location.href = "/whatever/do/5"'
Here's the test helper to test if your ajax response is actually called. I'd love for someone to rewrite this, write some tests, and submit it as a patch to rails.. Hi, lazyweb!
In the mean time, you can just dump it in test/test_helper.rb
def assert_js_redirected_to(options={}, message=nil)
clean_backtrace do
assert_response(:success, message)
assert_equal 'text/javascript', @response.headers['Content-Type'], 'Response should be Javascript content-type';
js_regexp = %r{(\w+://)?.*?(/|$|\\\?)(.*)}
url_regexp = %r{^window\.location\.href [=] ['"]#{js_regexp}['"][;]$}
redirected_to = @response.body.match(url_regexp)
assert_not_nil(redirected_to, message)
redirected_to = redirected_to[3]
msg = build_message(message, "expected a JS redirect to >, found one to >", options, redirected_to)
if options.is_a?(String)
assert_equal(options.gsub(/^\//, ''), redirected_to, message)
else
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is >)", redirected_to)
assert_equal(@controller.url_for(options).match(js_regexp)[3], redirected_to, msg)
end
end
end
Strip the ^ and $ from that regex if you're returning more than just a URL in your RJS update call.
*update*
fixed the method so it actually takes a hash. testing is not just for sissies!
more cracked-out scaffolding: Element.twisty
Courtenay : February 2nd, 2006
disable all your form elements
Courtenay : January 31st, 2006
Selectors and Effects Queues : Squirrel Domination
Liquid error: undefined method `login' for nil:NilClass : January 25th, 2006
Scaffolding on Crack (sortable, filtered scaffolds)
Courtenay : January 23rd, 2006
jiggery-pokery with switch/case and %W
Courtenay : December 20th, 2005
irb> %W' one two three '
=> ['one','two','three']
javascript> function $W(str) { return $A(str.split(/\s+/)); }
then, case
ruby:
case foo:
when 1,2,3:
# stuff
when 'four','five','six':
else:
# other stuff
end
javascript:
function stuff(arg) {
switch(true) {
case ($W('one two three').include(arg)):
stuff;
break;
case ($W('four five six').include(arg)):
other stuff;
break;
default:
break;
}
}
Wow, this is incredibly haphazard. Hope it /(helps|confuses)/ someone.