jQuery read only elements

There is a business requirement on a project I have at work to only allow a certain number of properties editable at a certain stage of the domain object’s life cycle. And like always, the properties defined to be editable could change in the future.

I already have a page that allows the user to edit all properties of the domain object, I really do not wish to duplicate that code. Please note that if you set an input element on a form as disabled, the value associated with the element will NOT be submitted via form post, that certainly is NOT what I’m looking for. I want the values of the *read only* fields still be submitted by the form but I just don’t want the user to edit the values. So I looked into using the readonly attribute in HTML. It’s complete crap.

For faqs.org:

It’s important to understand that READONLY merely prevents the user from changing the value of the field, not from interacting with the field. In checkboxes, for example, you can check them on or off (thus setting the CHECKED state) but you don’t change the value of the field.

Basically it works half-hearted and most browsers (IE, FF) do not indicate (by default) that a field is readonly. So the users will probably be left wondering why they cannot edit the field.

I searched around for an alternative and found this wonderful jQuery plugin readonly. Even though I couldn’t get it to work right for my application, the idea behind it is genius. You basically put overlay layers on top of any input fields you wish to make read only. The plugin seems a bit outdated. Perhaps it doesn’t work well with the newer version of jQuery but the idea still works well. Therefore I implemented my own.

This implementation automatically makes all input fields read only unless the input field has the class excludeMeFromReadOnly. This is just my implementation for demonstration. I do not provide any support for this code. Use where you see fit.

CSS

.readOnlyOverlay {
	position: absolute;
	background-color: #666;
	opacity: 0.3;
	filter: alpha(opacity=30);
	padding: 0 !IMPORTANT;
	margin: 0 !IMPORTANT;
}

javascript function generateOverlay

function generateOverlay(element) {
	var dimension = getDimensions($(element));
	//console.log('top=' + dimension.top + ' left=' + dimension.left +' width='+ dimension.width + ' height=' + dimension.height);

	// disassociate corresponding label attributes so the value of the element cannot be changed by clicking on the labels
	var id = $(element).attr('id');
	var label = $('label[for="'+id+'"]');
	$(label).removeAttr('for');

	// set my tabindex to -1 so tabs will ignore me
	$(element).attr('tabIndex', -1);
	$(label).attr('tabIndex', -1);

	// create a div overlay
	var overlay = $('</pre>
<div class="readOnlyOverlay"></div>
<pre>
').appendTo('body');
	$(overlay).css('top', dimension.top).css('left', dimension.left).css('width', dimension.width).css('height', dimension.height);
}

javascript function getDimensions

function getDimensions(element){
	var ret = {};

	// The multiple acquisitions of the CSS styles are required to cover any border and padding the elements may have.
	// The Ternary (parseInt(...) || 0) statements fix a bug in IE6 where it returns NaN,
	//  which doesn't play nicely when adding to numbers...
	ret.width = $(element).width()
	  + (parseInt($(element).css('borderLeftWidth')) || 0)
	  + (parseInt($(element).css('borderRightWidth')) || 0)
	  + (parseInt($(element).css('padding-left')) || 0)
	  + (parseInt($(element).css('padding-right')) || 0);
	ret.height = $(element).height()
	  + (parseInt($(element).css('borderTopWidth')) || 0)
	  + (parseInt($(element).css('borderBottomWidth')) || 0)
	  + (parseInt($(element).css('padding-bottom')) || 0)
	  + (parseInt($(element).css('padding-bottom')) || 0);
	var offsets = $(element).offset();
	ret.left = offsets.left;
	ret.top = offsets.top;

	return ret;
}

jQuery selector

// select all input,select elements. I'd wrap my form in a div.
$('div#formContent input, div#formContent select, div#formContent textarea').each(function(i, element) {
	// if the element doesn't have the class named excludeMeFromReadOnly, overlay it to make it look like it's read only
	if(!$(element).hasClass('excludeMeFromReadOnly')) {
		generateOverlay($(element));
	}
});

Please note, the plugin contains a lot more logic including IE hacks etc. Fortunately for me, I really don’t care about IE prior to version 8 in my particular scenario. Therefore I don’t need all of those hacks.

—>Example code (example at jsFiddle)<—

jQuery.each vs Javascript for loop

Being a big jQuery fan, I use jQuery.each method a lot in my Javascript code. Until recently I didn’t think too hard what jQuery.each really is.

Its description says it’s an iterator but it certainly is NOT a true iterator.

For example:

Let me know what you expect the code below to return.

var myVars = ['foo1', 'foo2', 'foo3'];

function containsValue(myValue, myCollection) {
	jQuery.each(myCollection, function(i, val) {
		if(val == myValue) {
			return true;
		}
	});
	return false;
}

$(document).ready(function() {
	console.log(containsValue('foo2', myVars));
});

Before I know better, I’d expect it to return true. Since myCollection DOES CONTAIN the value ‘foo2′. However the function containsValue WILL ALWAYS RETURN FALSE. That’s because when you return out of jQuery.each, it simply exits out of jQuery.each but not the containing function. In fact, whether you do anything in the callback function at all, jQuery.each ALWAYS RETURNS the collection you pass in.

e.g.

var returnedVar = jQuery.each(myVars, function(i, val) {});
console.log(returnedVar === myVars); // evaluates to true

In my opinion, jQuery.each acts more like a closure than an iterator. Sure you may use it as an iterator as long as you not returning anything. If you are just changing behaviors or collecting information, it will mimic an iterator. But you need to know, it really is not an iterator.

Personally I’m going to start to use Javascript’s native for..in statement instead of jQuery.each for Javascript collection variables. There is also an argument that jQuery.each may never perform faster than the native support for an iterator. Therefore below I will rewrite the function above using for..in.

function containsValueWithFor(myValue, myCollection) {
	for (index in myCollection) {
		if(myCollection[index] == myValue) {
			return true;
		}
	}
	return false;
}

console.log(containsValueWithFor('foo2', myVars)); // true

var myMap = {'lala':'foo1', '2':'foo2', 'b':'foo3'};

console.log(containsValueWithFor('foo2', myMap)); // true as well for an object/map

—>Example code<—