Integrating Minitest with Shippable
I know, everyone uses Travis. I have nothing against it. But in case you want to test and/or …
A simple tutorial explaining how to made a simple coffeescript that can automagically show and hide a loading spin while doing a ajax call.
I just built a simple and non intrusive AJAX status indicator with spin.js and CoffeeScript, and I think that you people may like it :)
By simple, I meant that the code itself is simple. You don’t need to know JS/Coffee like a ninja.
By non intrusive, I meant that the indicator does not block user interaction with the app, and you will see that you don’t need to change your code to fit in my indicator ~rules~, so, it’s also code-non-intrusive. (LOL)
Let’s do it!
Basically, the only dependency is the spin.js library. You probably use something like jquery-ujs
, turbolinks
or even the jQuery
API itself to made ajax calls.
In my case, I use all of them.
My Gemfile
looks like this:
(...)
group :assets do
gem 'sass-rails', '~> 3.2.3'
gem 'coffee-rails', '~> 3.2.1'
gem 'therubyracer', :platforms => :ruby
gem 'uglifier', '>= 1.0.3'
gem 'turbolinks'
gem 'jquery-turbolinks'
gem 'spinjs-rails'
end
# asset related
gem 'jquery-rails'
(...)
You will also need to import them in you application.js
.
We all know that something about events triggered by those APIS:
jQuery.{ajax,post,get}
: ajax{Start,Stop,Complete,Error,Success}
turbolinks
: page:{fetch,load,restore,change}
jQuery-ujs
: ajax:{beforeSend,success,error,complete,aborted:required,aborted:file}
There’s a lot of events to deal, uh? Not really. According to my tests, basically, all jQuery
events will fire ajaxStart
and ajaxComplete
at the start and end of request repectivelly. In the same way, tubolinks
will trigger page:fetch
and page:change
, an at last jQuery-ujs
will fire ajax:beforeSend
and ajax:complete
. So, we will have something like:
$(document).on 'ajax:before ajaxStart page:fetch', ->
# show spin
$(document).on 'ajax:complete ajaxComplete page:change', ->
# hide spin
But we also have another problem: if some user interaction fire multiple ajax calls? In my app, I don’t need to worry too much about that for now, because I don’t need to do more than one $.get
(for example) in the same user interaction. I only have some $.get
(for example) callbacks that call Turbolinks.visit
, so, imagine something like:
ajax:beforeSend--------------ajax:complete------------------>
-------------------page:fetch------------------page:change-->
The spin will shows up in beforeSend
, and hide in complete
, but there’s another ajax going on in page:fetch
. To solve that, I just cache in a variable the last “event type” (ajax or page), and only hide when this call ends.
To fix the issue about multiple ajax calls for the same type, we can made a dirty hack to store in a hash or something the count of events that should end to spin disapear. Is pretty simple, but I’ll let this for you guys :)
The most “difficult” part of the solution is already explained, so, get the code, read it, and use the comment box if you have any doubts =)
ajax.spin.js.coffee
code:
opts = {
lines: 7, # The number of lines to draw
length: 6, # The length of each line
width: 3, # The line thickness
radius: 5, # The radius of the inner circle
corners: 1, # Corner roundness (0..1)
rotate: 0, # The rotation offset
color: '#000', # #rgb or #rrggbb
speed: 1.1, # Rounds per second
trail: 100, # Afterglow percentage
shadow: false, # Whether to render a shadow
hwaccel: false, # Whether to use hardware acceleration
className: 'spinner', # The CSS class to assign to the spinner
zIndex: 2e9, # The z-index (defaults to 2000000000)
top: 'auto', # Top position relative to parent in px
left: 'auto' # Left position relative to parent in px
}
# save the lastEvent type that was called
lastEvent = undefined
# the element where the spinner should appear
$n = undefined
# I can only pop the $n var when document is ready
$(document).ready ->
$n = $('.navbar.navbar-static-top')
# get the event type, ex: a "page:change" will return only 'page'
eventType = (event) ->
return false if not event
type = event.type
if type.indexOf(':') > -1
type.split(':')[0]
else
type.match(/[A-Z]?[a-z]+|[0-9]+/g)[0]
# show the spinner
loadState = (event) ->
lastEvent = eventType event
$n.spin opts
# hide the spinner
doneState = (event) ->
if eventType(event) == lastEvent
lastEvent = undefined
$n.spin false
# bind some states (will see if it is more needed)
$(document).on 'ajax:before ajaxStart page:fetch', (event) ->
loadState event
$(document).on 'ajax:complete ajaxComplete page:change', (event) ->
doneState event
The compiled ajax.spin.js
file, for those wo don’t use CoffeeScript:
(function() {
var $n, doneState, eventType, lastEvent, loadState, opts;
opts = {
lines: 7,
length: 6,
width: 3,
radius: 5,
corners: 1,
rotate: 0,
color: "#000",
speed: 1.1,
trail: 100,
shadow: false,
hwaccel: false,
className: "spinner",
zIndex: 2e9,
top: "auto",
left: "auto"
};
lastEvent = void 0;
$n = void 0;
$(document).ready(function() {
return ($n = $(".navbar.navbar-static-top"));
});
eventType = function(event) {
var type;
if (!event) {
return false;
}
type = event.type;
if (type.indexOf(":") > -1) {
return type.split(":")[0];
} else {
return type.match(/[A-Z]?[a-z]+|[0-9]+/g)[0];
}
};
loadState = function(event) {
lastEvent = eventType(event);
return $n.spin(opts);
};
doneState = function(event) {
if (eventType(event) === lastEvent) {
lastEvent = void 0;
return $n.spin(false);
}
};
$(document).on("ajax:before ajaxStart page:fetch", function(event) {
return loadState(event);
});
$(document).on("ajax:complete ajaxComplete page:change", function(event) {
return doneState(event);
});
}.call(this));
Hope it help somebody, enjoy.
Cheers