Logic’s Last Stand

June 5, 2010

Accessible Forms with PHP and jQuery

Filed under: Computers, Freeware — Tags: , , , , — Zurahn @ 8:30 pm

A primary challenge of recent web-development is how to make use of the great new dynamic tools provided to us in using libraries such as jQuery, while still providing an accessible website without the use of JavaScript. While there’s more to accessibility than making a site work without JavaScript, it’s a fundamental start. This task looks arduous, but it doesn’t have to be — approach it right from the start and it may actually be trivial.

Let’s start from the no-JavaScript version and work up. Normally your form without JavaScript would look something like this

<form method="post" action="example.php">
    Field: <input type="text" name="field" /><br />
    <input type="submit" value="Submit" />
</form>

To have JavaScript handle the post, we’ll add an onsubmit function to the form.

<form method="post" action="example.php" onsubmit="return formSubmit(this)">
    Field: <input type="text" name="field" /><br />
    <input type="submit" value="Submit" />
</form>

When the onsubmit function returns false, the form does not submit. So by having the formSubmit function return false, we can have the page handle the post via AJAX instead of having to refresh the page. Let’s look at the formSubmit function.

function formSubmit(obj)
{
    var form = $(obj);
    $.post(obj.action, form.serialize());
    return false;
}

The .serialize() function takes the form elements and converts them to query string parameters so they can be passed through post. By using this, we can reuse the same generic formSubmit function regardless of the form — all we have to add is the onsubmit attribute to the form.

Now you may also want to have error and success messages return. A good way to handle this is via JSON objects. JSON is a standard by which objects can be represented in string form, so we can pass a string from PHP to JavaScript, which can then be handled as an object. Let’s update out formSubmit function to handle this behaviour (the script will assume that there are hidden divs with the ID “error” and “success”.

function formSubmit(obj)
{
    var form = $(obj);
    $.post(obj.action, form.serialize(), function(data)
    {
        // Return data is JSON object string, so eval to get object
        var message = eval("("+data+")");
        showErrors(message['errors']);
        showSuccesses(message['successes']);
    });
    return false;
}

function showErrors(messages)
{
    if(typeof messages != "undefined")
    {
        $('#success').css('display', 'none');
        var error = $('#error');
        error.css('display', 'none');
        error.html(getMessageList(messages));
        error.fadeIn();
    }
}

function showSuccesses(messages)
{
    if(typeof messages != "undefined")
    {
        $('#error').css('display', 'none');
        var success = $('#success');
        success.css('display', 'none');
        success.html(getMessageList(messages));
        success.fadeIn();
    }
}

function getMessageList(messages)
{
    var output = '<ul>';
    // iterate through the object properties
    for(i in messages)
        output += '<li>'+messages[i]+'</li>';
    output += '</ul>';
    return output;
}

Now we need to construct the JSON object on the PHP side. The error will be echoed, but remember that we want this to work even if it’s not an AJAX post, so we need different behaviour depending on whether or not it was an AJAX post — echo error/success messages if AJAX, redirect back if it’s not. Let’s go to example.php; the script will assume that the value $_SESSION[‘page’] holds the value of $_SERVER[‘PHP_SELF’] before the post.

<?php
session_start();
$field = $_POST['field'];

if(!isset($field) || $field === "")
    Reporting::setError("Name cannot be blank");

if(!Reporting::hasErrors())
{
    /* Do something with $field */
    Reporting::setSuccess("Operation with <em>$field</em> completed successfully");
}

Reporting::endDo();


class Reporting
{
    public function __construct()
    {

    }

    public static function endDo()
    {
        if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
        {
            if(self::hasErrors())
                echo self::getJsonErrors();
            else if(self::hasSuccesses())
                echo self::getJsonSuccesses();
        }
        else
            header('Location: '.$_SESSION['page']);
    }

    public static function hasErrors()
    {
        return isset($_SESSION['errors'][0]);
    }

    public static function hasSuccesses()
    {
        return isset($_SESSION['successes'][0]);
    }

    public static function setError($message)
    {
        $_SESSION['errors'][] = $message;
    }

    public static function setSuccess($message)
    {
        $_SESSION['successes'][] = $message;
    }
    
    public static function getJsonErrors($clear=true)
    {
        return self::getJsonMessages('errors', $clear);
    }

    public static function getJsonSuccesses($clear=true)
    {
        return self::getJsonMessages('successes', $clear);
    }

    public static function showErrors($clear=true)
    {
        return self::showMessages('errors', $clear);
    }

    public static function showSuccesses($clear=true)
    {
        return self::showMessages('successes', $clear);
    }

    private static function showMessages($type, $clear)
    {
        $output = '<ul>';
        foreach($_SESSION[$type] as $val)
            $output .= "<li>$val</li>";
        $output .= '</ul>';
        if($clear)
            $_SESSION[$type] = array();
        return $output;
    }

    private static function getJsonMessages($type, $clear)
    {
        $output = '{ '.$type.': { ';
        $comma = '';
        foreach($_SESSION[$type] as $key => $val)
        {
            $output .= $comma.$key.': "'.$val.'"';
            $comma = ', ';
        }
        $output .= ' } }';
        if($clear)
            $_SESSION[$type] = array();
        return $output;
    }
}
?>

Looks like a lot of work, but with that, we’re all done. Everything from now on is handled identically between jQuery and non-JavaScript versions of the site, and all you have to do is add onsubmit=”return formSubmit(this)” to each form, and in the processing script, close with Reporting::endDo(). Everything else takes care of itself.

Blog at WordPress.com.