Skip to content

Instantly share code, notes, and snippets.

@salex
Created August 15, 2010 14:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save salex/525561 to your computer and use it in GitHub Desktop.
Save salex/525561 to your computer and use it in GitHub Desktop.

Client-Side Unobtrusive Javascript

As I keep trying to dig into Rails 3, I keep running into things that make me learn more than I want to -see "My brain is full!"

Javascript is another one of those languages that I have used a lot, but only understand the basics. Rails 3 implements Unobtrusive Javascript (UJS) using either Prototype or JQuery. I have done quit a bit with Prototype and when I ported a Rails 2 application to Rails 3, a validation.js library I was using stopped working. I think because of a Prototype version upgrade that broke the library. This was one of those generic validation libraries that checked: empty, email, number, etc. The only thing I used was empty - and then I had to modify it to take care of radio and checkbox inputs in a sane manner.

Anyhow I decided to roll my own. My first attempt was to steal parts of rails.js and write kludges to suit my needs. It worked, but I was not sure I liked it (or understood).

I then started looking at UJS posts and found this post where someone tried the same thing that I did, but then discovered behaviors.

I copied the code and tried to implement my own behaviors. As my unanswered post on rails talk points out, I could get a simple toggle JS function working, a replacement for onclick="toggleOther(this)", but I was having trouble with my onsubmit replacement (which is what validate.js did by creating an observer).

I finally went back and tried to understand the basic add behavior script:

// An Unobtrusive Javascript (UJS) driver based on explicit behavior definitions. Just
// put a "data-behaviors" attribute on your view elements, and then assign callbacks
// for those named behaviors via Behaviors.add.

var Behaviors = {
  add: function(trigger, behavior, handler) {
    document.observe(trigger, function(event) {
      var element = event.findElement("*[data-behaviors~=" + behavior + "]");
      if (element) handler(element, event);
    });
  }
};

Now I've copied and used these type of routines a number of times, but I adhered to "Information Hiding", I didn't need to know how it worked - just how to use it. Now this code should be obvious to anyone who started with Fortran, Bacic and 6502 assembler! - but it wasn't. After looking and tracing in Safari JS debugger I have my own version of what this type of code is.

  • Behaviors is a hash variable of functions. I think they call them anonymous functions - which many times contain other anonymous functions.
  • The "add" function adds functions with arguments trigger, behavior and handler
  • The call to handler(element,event) calls a function "handler" and passes arguments element and event.

What I was missing is that when I wrote my call to Behaviors.add:

Behaviors.add("submit", "validate", function(element) {
	...
});

Is that arguments element, and event are passed to it. I was trying to stop the event, but didn't know to get to the event and kept trying to find out how to find the event from the form element. I already has it and all I needed was:

if(!valid){event.stop()}; 

I'm posting this gist, just in case anyone else is trying to do some client-side UJS and gets stuck.

My entire assmnts.js file contents is below as an example. Remember all I was trying to do is: 1) make sure all the form elements were filled in (behavior validate) and annotating any missing elements, and 2) toggle some display:none div's if an input was checked (behavior toggleOther). These are two separate non-related behaviors.

The code is not generic, but suited to my needs. If you need to do the same types of things, you should be able to figure it out.

Steve

// An Unobtrusive Javascript (UJS) driver based on explicit behavior definitions. Just
// put a "data-behaviors" attribute on your view elements, and then assign callbacks
// for those named behaviors via Behaviors.add.

var Behaviors = {
  add: function(trigger, behavior, handler) {
    document.observe(trigger, function(event) {
      var element = event.findElement("*[data-behaviors~=" + behavior + "]");
      if (element) handler(element, event);
    });
  }
};

Behaviors.add("submit", "validate", function(element) {
  var formElements = element.select([".required", ".required-one"]);
  var valid = true;
  formElements.each(function(elm) {
      var type = elm.type.toLowerCase();
      var node = elm.nodeName.toLowerCase();
      var elm_valid = true
      if (node == 'input') {
          var type = elm.type.toLowerCase();
          if (type == 'text' || type == 'password') {
              elm_valid = !isEmpty(elm)
              elm_valid ? clearError(elm) : setError(elm)
              valid = valid && elm_valid
          } else if (type == 'radio' || type == 'checkbox') {
              elm_valid = isChecked(elm)
              elm_valid ? clearError(elm) : setError(elm)
              valid = valid && elm_valid
          }
      } else if (node == 'textarea') {
          elm_valid = !isEmpty(elm)
          elm_valid ? clearError(elm) : setError(elm)
          valid = valid && elm_valid
      } else if (node == 'select') {
          elm_valid = isSelected(elm)
          elm_valid ? clearError(elm) : setError(elm)
          valid = valid && elm_valid
      } else {
		//nothing?
      }

  });
  if (!valid) {
    event.stop()
    };
});

Behaviors.add("click", "toggleOther", function(element) {
  var id = element.id;
  var other_id = id + "_other"
  var other_text = other_id + "_text"
  var input_type = $(id).type.toLowerCase()
  if (input_type == 'radio') {
      var input_name = $(id).name
      var input_form = $(id).form
      var radio_group = Form.getInputs(input_form, 'radio', input_name)
      //alert(radio_group)
      for (var i = 0; i < radio_group.length; i++) {
          //alert(radio_group[i].id)
          other_id = radio_group[i].id + "_other"
          other_text = radio_group[i].id + "_other_text"
          if ($(other_id)) {
              if ($(radio_group[i].id).checked) {
                  if ($(other_id)) {
                      $(other_id).style.display = "block"
                      $(other_text).disabled = false
                  };
              } else {
                  if ($(other_id)) {
                      $(other_id).style.display = "none"
                      $(other_text).value = ""
                      $(other_text).disabled = true
                  };
              };
          };
      };

  } else {

      if ($(id).checked) {
          if ($(other_id)) {
              $(other_id).style.display = "block"
              $(other_text).disabled = false
          };
      } else {
          if ($(other_id)) {
              $(other_id).style.display = "none"
              $(other_text).value = ""
              $(other_text).disabled = true
          };
      };
  };
});


function isEmpty(e) {
    var v = e.value;
	// The code below check unique to my applications in that all text inputs
	// produce an array, the first element is an id to a question and the second being the text.
	// there can also be an array of these two element text fields.
    var siblings = document.getElementsByName(e.name);
    if (siblings.length > 2) {
        var ok = false;

        for (var i = 0; i < siblings.length; i++) {
            if (siblings[i].type.toLowerCase() == "text") {
                v = siblings[i].value
                empty = ((v == null) || (v.length == 0))
                ok = ok || empty
            }
        };

        return ok;
    } else {
        return ((v == null) || (v.length == 0));
    };
}

function isSelected(e) {
    return e.options ? e.selectedIndex > 0: false;
}

function isChecked(e) {
    var siblings = document.getElementsByName(e.name);
    return $A(siblings).any(function(elm) {
        return $F(elm);
    });

}

function setError(e) {
    var elemID = e.id
    if (elemID) {
		// id in form "qa_36_161", enclosing div id in form "q_36"
        var chunks = elemID.split("_")
        if (chunks.length != 3) {
            return
        };
        var qid = $("q_" + chunks[1])
        if ($(qid)) {
            $(qid).addClassName('validation-error')
            errID = $(qid).id + "err"
            span = '<span class="validation-advice" id="' + errID + '">Your forgot something!</span>'
            if (!$(errID)) {
                $(qid).insert(span)
            }
        }
    }
}

function clearError(e) {
    var elemID = e.id
    if (elemID) {
		// id in form "qa_36_161", enclosing div id in form "q_36"
        var chunks = elemID.split("_")
        if (chunks.length != 3) {
            return
        };
        var qid = $("q_" + chunks[1])
        if ($(qid)) {
            $(qid).removeClassName('validation-error')
            errID = $(qid).id + "err"
            if ($(errID)) {
                $(errID).remove()
            }
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment