Recently, I've been playing with
CoffeeScript and have found it to be a lot of fun. I'm a big fan of JavaScript and CoffeeScript has only added to the enjoyment of writing client-side code by taking away a few of the "
bad parts" and reducing syntactic noise. To help learn the language, I took a few of the jQuery UI examples and re-wrote them in CoffeeScript. You can find the full solution (F# + ASP.NET MVC (Razor) + CoffeeScript) at
https://github.com/dmohl/FsCoffeeScriptjQueryUIExample. (Note: You will need to install the
Mindscape Web Workbench Visual Studio 2010 extension to make the CoffeeScript aspects work correctly. Visit
this post by Scott Hanselman for information on getting started with this extension).
Here are the before and after examples:
This simple Portlets example comes from the jQuery UI demo found at
http://jqueryui.com/demos/sortable/#portlets.
The JavaScript Version:
(function (portlets, undefined) {
portlets.init = function() {
$(".column").sortable({
connectWith: ".column"
});
$(".portlet")
.addClass("ui-widget ui-widget-content ui-helper-clearfix ui-corner-all")
.find(".portlet-header").addClass("ui-widget-header ui-corner-all")
.prepend("<span class='ui-icon ui-icon-minusthick'></span>")
.end().find(".portlet-content");
$(".portlet-header .ui-icon").click(function () {
$(this).toggleClass("ui-icon-minusthick").toggleClass("ui-icon-plusthick");
$(this).parents(".portlet:first").find(".portlet-content").toggle();
});
$(".column").disableSelection();
};
} (window.portlets = window.portlets || {}));
The CoffeeScript Version:
((portlets) ->
portlets.init = ->
$(".column").sortable(connectWith: ".column").disableSelection()
$(".portlet")
.addClass("ui-widget ui-widget-content ui-helper-clearfix ui-corner-all")
.find(".portlet-header").addClass("ui-widget-header ui-corner-all")
.prepend("<span class='ui-icon ui-icon-minusthick'></span>")
.end().find ".portlet-content"
$(".portlet-header .ui-icon").click ->
$(this).toggleClass("ui-icon-minusthick").toggleClass "ui-icon-plusthick"
$(this).parents(".portlet:first").find(".portlet-content").toggle()
) window.portlets = window.portlets or {}
This simple Photo Manager example comes from the jQuery UI demo found at
http://jqueryui.com/demos/droppable/#photo-manager.
The JavaScript Version:
(function (pictureManager, undefined) {
pictureManager.init = function() {
var $gallery = $("#gallery"),
$trash = $("#trash");
$( "li", $gallery ).draggable({
cancel: "a.ui-icon",
revert: "invalid",
containment: $( "#demo-frame" ).length ? "#demo-frame" : "document",
helper: "clone",
cursor: "move"
});
$trash.droppable({
accept: "#gallery > li",
activeClass: "ui-state-highlight",
drop: function( event, ui ) {
deleteImage( ui.draggable );
}
});
$gallery.droppable({
accept: "#trash li",
activeClass: "custom-state-active",
drop: function( event, ui ) {
recycleImage( ui.draggable );
}
});
$("ul.gallery > li").click(function (event) {
var $item = $(this), $target = $(event.target);
if ($target.is("a.ui-icon-trash")) {
deleteImage($item);
} else if ($target.is("a.ui-icon-zoomin")) {
viewLargerImage($target);
} else if ($target.is("a.ui-icon-refresh")) {
recycleImage($item);
}
return false;
});
var recycle_icon = "<a href='link/to/recycle/script/when/we/have/js/off' title='Recycle this image' class='ui-icon ui-icon-refresh'>Recycle image</a>";
function deleteImage($item) {
$item.fadeOut(function () {
var $list = $("ul", $trash).length ?
$("ul", $trash) :
$("<ul class='gallery ui-helper-reset'/>").appendTo($trash);
$item.find("a.ui-icon-trash").remove();
$item.append(recycle_icon).appendTo($list).fadeIn(function () {
$item
.animate({ width: "48px" })
.find("img")
.animate({ height: "36px" });
});
});
}
var trash_icon = "<a href='link/to/trash/script/when/we/have/js/off' title='Delete this image' class='ui-icon ui-icon-trash'>Delete image</a>";
function recycleImage($item) {
$item.fadeOut(function () {
$item
.find("a.ui-icon-refresh")
.remove()
.end()
.css("width", "96px")
.append(trash_icon)
.find("img")
.css("height", "72px")
.end()
.appendTo($gallery)
.fadeIn();
});
}
function viewLargerImage($link) {
var src = $link.attr("href"),
title = $link.siblings("img").attr("alt"),
$modal = $("img[src$='" + src + "']");
if ($modal.length) {
$modal.dialog("open");
} else {
var img = $("<img alt='" + title + "' width='384' height='288' style='display: none; padding: 8px;' />")
.attr("src", src).appendTo("body");
setTimeout(function () {
img.dialog({
title: title,
width: 400,
modal: true
});
}, 1);
}
}
};
} (window.pictureManager = window.pictureManager || {}));
The CoffeeScript Version:
((pictureManager) ->
pictureManager.init = ->
$gallery = $("#gallery")
$trash = $("#trash")
recycle_icon = "<a href='link/to/recycle/script/when/we/have/js/off'
title='Recycle this image'
class='ui-icon ui-icon-refresh'>Recycle image</a>"
trash_icon = "<a href='link/to/trash/script/when/we/have/js/off'
title='Delete this image'
class='ui-icon ui-icon-trash'>Delete image</a>"
deleteImage = ($item) ->
$item.fadeOut ->
$list = if $("ul", $trash).length then $("ul", $trash) else $("<ul class='gallery ui-helper-reset'/>").appendTo $trash
$item.find("a.ui-icon-trash").remove()
$item.append(recycle_icon).appendTo($list).fadeIn ->
$item.animate(width: "48px").find("img").animate height: "36px"
recycleImage = ($item) ->
$item.fadeOut ->
$item.find("a.ui-icon-refresh").remove().end()
.css("width", "96px").append(trash_icon).find("img")
.css("height", "72px").end().appendTo($gallery).fadeIn()
viewLargerImage = ($link) ->
src = $link.attr "href"
title = $link.siblings("img").attr "alt"
$modal = $("img[src$='#{src}']")
if $modal.length
$modal.dialog "open"
else
img = $("<img alt='#{title}' width='384' height='288'
style='display: none; padding: 8px;' />")
.attr("src", src).appendTo "body"
setTimeout (->
img.dialog
title: title
width: 400
modal: true
), 1
$("li", $gallery).draggable
cancel: "a.ui-icon"
revert: "invalid"
containment: if $("#demo-frame").length then "#demo-frame" else "document"
helper: "clone"
cursor: "move"
$trash.droppable
accept: "#gallery > li"
activeClass: "ui-state-highlight"
drop: (event, ui) ->
deleteImage ui.draggable
$gallery.droppable
accept: "#trash li"
activeClass: "custom-state-active"
drop: (event, ui) ->
recycleImage ui.draggable
$("ul.gallery > li").click (event) ->
$item = $(this)
$target = $(event.target)
if $target.is "a.ui-icon-trash"
deleteImage $item
else if $target.is "a.ui-icon-zoomin"
viewLargerImage $target
else recycleImage $item if $target.is "a.ui-icon-refresh"
false
) window.pictureManager = window.pictureManager or {}
This simple User Manager example comes from the jQuery UI demo found at
http://jqueryui.com/demos/dialog/#modal-form.
The JavaScript Version:
(function (userManager, undefined) {
userManager.init = function () {
$("#dialog:ui-dialog").dialog("destroy");
var name = $("#name"),
email = $("#email"),
password = $("#password"),
allFields = $([]).add(name).add(email).add(password),
tips = $(".validateTips");
function updateTips(t) {
tips
.text(t)
.addClass("ui-state-highlight");
setTimeout(function () {
tips.removeClass("ui-state-highlight", 1500);
}, 500);
}
function checkLength(o, n, min, max) {
if (o.val().length > max || o.val().length < min) {
o.addClass("ui-state-error");
updateTips("Length of " + n + " must be between " +
min + " and " + max + ".");
return false;
} else {
return true;
}
}
function checkRegexp(o, regexp, n) {
if (!(regexp.test(o.val()))) {
o.addClass("ui-state-error");
updateTips(n);
return false;
} else {
return true;
}
}
$("#dialog-form").dialog({
autoOpen: false,
height: 300,
width: 350,
modal: true,
buttons: {
"Create an account": function () {
var bValid = true;
allFields.removeClass("ui-state-error");
bValid = bValid && checkLength(name, "username", 3, 16);
bValid = bValid && checkLength(email, "email", 6, 80);
bValid = bValid && checkLength(password, "password", 5, 16);
bValid = bValid && checkRegexp(name, /^[a-z]([0-9a-z_])+$/i, "Username may consist of a-z, 0-9, underscores, begin with a letter.");
bValid = bValid && checkRegexp(email, /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i, "eg. ui@jquery.com");
bValid = bValid && checkRegexp(password, /^([0-9a-zA-Z])+$/, "Password field only allow : a-z 0-9");
if (bValid) {
$("#users tbody").append("<tr>" +
"<td>" + name.val() + "</td>" +
"<td>" + email.val() + "</td>" +
"<td>" + password.val() + "</td>" +
"</tr>");
$(this).dialog("close");
}
},
Cancel: function () {
$(this).dialog("close");
}
},
close: function () {
allFields.val("").removeClass("ui-state-error");
}
});
$("#create-user")
.button()
.click(function () {
$("#dialog-form").dialog("open");
});
};
} (window.userManager = window.userManager || {}));
The CoffeeScript Version:
((userManager) ->
userManager.init = ->
name = $("#name")
email = $("#email")
password = $("#password")
allFields = $([]).add(name).add(email).add password
tips = $(".validateTips")
updateTips = (t) ->
tips.text(t).addClass "ui-state-highlight"
setTimeout (->
tips.removeClass "ui-state-highlight", 1500
), 500
checkLength = (o, n, min, max) ->
if o.val().length > max or o.val().length < min
o.addClass "ui-state-error"
updateTips "Length of #{n} must be between #{min} and #{max}."
false
else true
checkRegexp = (o, regexp, n) ->
unless regexp.test o.val()
o.addClass "ui-state-error"
updateTips n
false
else true
$("#dialog-form").dialog
autoOpen: false
height: 300
width: 350
modal: true
buttons:
"Create an account": ->
bValid = true
allFields.removeClass "ui-state-error"
bValid = bValid and checkLength name, "username", 3, 16
bValid = bValid and checkLength email, "email", 6, 80
bValid = bValid and checkLength password, "password", 5, 16
bValid = bValid and checkRegexp name, /^[a-z]([0-9a-z_])+$/i, "Username may consist of a-z, 0-9, underscores, begin with a letter."
bValid = bValid and checkRegexp email, /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i, "eg. ui@jquery.com"
bValid = bValid and checkRegexp password, /^([0-9a-zA-Z])+$/, "Password field only allow : a-z 0-9"
if bValid
$("#users tbody").append "<tr>" + "<td>" + name.val() + "</td>" + "<td>" + email.val() + "</td>" + "<td>" + password.val() + "</td>" + "</tr>"
$(this).dialog "close"
Cancel: ->
$(this).dialog "close"
close: ->
allFields.val("").removeClass "ui-state-error"
$("#dialog:ui-dialog").dialog "destroy"
$("#create-user").button().click ->
$("#dialog-form").dialog "open"
) window.userManager = window.userManager or {}