Tuesday, October 15, 2013

Squirrelly Javascript

Lately, I've been maintaining our (very old) Joomla website, which is running a (very old) version of RSForm!Pro. To be honest, RSForm!Pro seems like a pretty decent product and even the old version we're running has lots of hooks were I've been able to cobble together things. But I'm getting ahead of myself...

Build me a form...

So the powers that be wanted a form added to the website that would send an e-mail when it was submitted. This is exactly the sort of thing that RSForm is designed to do, so it was the obvious choice. If I had known then what I know now, I might have approached this differently, but anyway.

One of the requirements that became apparent was that we only wanted some fields to be visible when other fields were selected. This is reasonably easy to do, and I was able to quickly implement this with the built-in functionality of RSForm. Where it gets tricky is that it's hard to validate the input on the hidden fields, because there's no way to say that a value is required but only if the field is visible.

Custom validation rules

The way I accomplished this was to add to the "Scripts" section of the form a "called on form display" script that set up some custom javascript code:
$jscript = "<script type='text/javascript'>

function customValidation() {
    var ret = true;
    
    // Two radio buttons, at least one must be a non-default value
    if ( document.getElementById('radio0-0').checked &&
         document.getElementById('radio1-0').checked )
    {
        document.getElementById('component359').className='formError';
        ret = false;
    }
    else
    {
        document.getElementById('component359').className='formNoError';
    }

    return ret;
}
</script>";

$formLayout = $jscript . $formLayout;

This leverages RSForm's built in naming (component359 is the error div for the radio button, and the classes are already defined to display or not). Our Javascript code is "prefixed" onto the formLayout variable which already contains the HTML for the form. In this way, we get our Javascript function into the HTML page. Then, all we have to do is to get the submit button to call this code when pressed, by using the "Additional Attributes" that RSForm lets us define.
onclick="javascript:return customValidation();"


You can do -- MAGICK!

Another requirement from management was that the form accept an e-mail address for the submitter, but that it also CC two optional addresses in addition. RSForm, unfortunately, has an issue with this. While it's easy to create the fields, and easy to add them to a "To" or "CC" line, since they're optional, the commas that would normally separate them need to be present (or not) depending on whether the fields have values (or not).

So we added a hidden field (ccs) to the form and added to our "Additional Attributes" on the submit button:
document.getElementById('ccs').value=
 [document.getElementById('cc1').value,
  document.getElementById('cc2').value].filter(function(e)
  {
   return(e!=='');
  }).join(',');

This means that the CCs field is now suitable for use in the CC field of the e-mail message. Nothing to it.

Subject of controversy

I use gmail to read my work mail. I started using it years ago and it's just stuck. Being able to search nearly instantly, as well as having the unique threaded model, has worked out well for me. But it makes it hard to track these form submissions as unique events because they all have the same subject. So I needed a way to add some sort of unique identifier to the submissions. I ended up using a timestamp since it would have a reasonable value and might be useful in other circumstances. I used the same trick of adding a hidden field, having my custom form validation javascript code set the value of the field when "Submit" is pressed, and then including the field value in the Subject.
if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) { 
      return typeof args[number] != 'undefined'
        ? args[number]
        : match
      ;
    });
  };
}

if (!String.prototype.pad) {
  String.prototype.pad = function(width, z) {
    z = z || '0';
    return this.length >= width ? this : new Array(width - this.length + 1).join(z) + this;
  };
}

function validateGoFields() {
    var ret = true;

    var now=new Date(new Date().getTime() - 420 * 60000);
    document.getElementById('timestamp').value = 
        new String('{0}{1}{2}-{3}{4}{5}').format(
            now.getUTCFullYear().toString().pad(4),
            now.getUTCMonth().toString().pad(2),
            now.getUTCDate().toString().pad(2),
            now.getUTCHours().toString().pad(2),
            now.getUTCMinutes().toString().pad(2),
            now.getUTCSeconds().toString().pad(2)
        );
I, by the way, am not really that smart. A lot of that was stolen from StackExchange and massaged to work the way I wanted it to.

(It's worth pointing out that the 420 nonsense is not a social commentary, it's -7 hours * 60 min/hr to give our timestamp in [hopefully] local time for the managers in California.)

One day I plan to learn Javascript so I can do this better.