Fork me on GitHub
Showing posts with label webkit. Show all posts
Showing posts with label webkit. Show all posts

25 Mar 2011

Styling with HTML5 form validation

HTML5 specifies a number of enhancements to form inputs that are gradually being implemented by newer browsers. Among the enhancements is support for some level of automatic form validation. When inputs have invalid values the browser may refuse to submit the form. Currently Opera, Safari, Chrome and Firefox 4 have some support for this. IE8 does not. I haven’t experimented with IE9 yet.

Let’s look at an example. A simple registration form has inputs for the name, email and website of a prospective user. Of these name and email are required. The input elements are as follows:

<input type="text" name="name" required>
<input type="email" name="email" required>
<input type="url" name="website">

The required attribute is new in HTML5 as are the input types email and url. All of these have implications for validation. Additionally the new input types, whilst they appear just like regular text inputs in desktop browsers are great for mobile users as they are given optimised virtual keyboard layouts.

CSS3 has :invalid, :required and :optional pseudo-classes that are supported by current versions of Firefox, Chrome, Safari and Opera. Some browsers also refuse to submit the form if a required field is not filled in or invalid values are entered in url or email type fields. Obviously without appropriate feedback this could be a pretty disastrous user experience and thankfully even the Webkit browsers which until recently were blocking form submission without feedback are now handling things better.

  • Firefox 4, Opera 11.01 and Chrome 10.0.648.204 all block form submission if there is an empty required field or invalid value and display a message next to the first such field.
  • Safari 5.0.4 will apply :invalid CSS rules but allows the form to be submitted.

Applying styles to invalid fields is pretty easy. For example to highlight invalid fields with a red border and background:

input {
    border: 1px solid #ccc;
}

input:invalid {
    background: #fff3f3;
    border-color: #f66;
}

You can also combine the :invalid pseudo-class in combination with others. For example to highlight focused fields with a fancy CSS3 box-shadow:

input:focus {
    border-color: #99f;
    outline: none;
     -moz-box-shadow: 0 0 0.25em #99f; 
  -webkit-box-shadow: 0 0 0.25em #99f; 
          box-shadow: 0 0 0.25em #99f; 
}

input:focus:invalid {
     -moz-box-shadow: 0 0 0.25em #f66; 
  -webkit-box-shadow: 0 0 0.25em #f66; 
          box-shadow: 0 0 0.25em #f66;
}

It’s worth noting that Firefox 4 automatically adds a red box-shadow to invalid fields (and not just focused ones). To turn it off you can specify:

input:invalid {
    -moz-box-shadow: none;
}

Of course, the browsers that display messages when they refuse to submit a form do so with different text and different visual effects so creating a consistent cross-browser look and feel with both client and server side validation messages is going to be an interesting challenge.

22 Feb 2011

JavaScript's history object, pushState and the back button

I’m not sure if it’s the immaturity of the browser support or my general uselessness but I’ve been having some trouble with the JavaScript history API.

I won’t try to explain the history API here, it’s pretty well covered at Mozilla Developer Network and W3. The basics are simple enough:

  • The API provides two methods; pushState which allows you to add a new entry to the browser history and replaceState which modifies the current history entry.
  • New entries added to history using pushState can be navigated via the browser’s back and forward buttons and a popstate event is fired on the window object when this happens.
  • Both methods allow you to attach arbitrary data to the history entry that you can use to reconstruct the appropriate page state when the user uses the back or forward buttons.

I imagine a pretty typical use-case is what I’ve been trying to do with the pagination and sorting on the list pages of Grails scaffolding. Instead of pagination links and column headers causing a full page reload when clicked I intercept the click event and send an AJAX request getting back a page fragment I can use to update the list in the page. Easy enough, however without the history API it will break the back and forward buttons and make deep linking impossible. This isn’t an acceptable option so in true progressive enhancement style I’ve used Modernizr and only apply the AJAX behaviour if the browser supports the history API.

The essence of the script involved is this:

var links =     //... pagination and sorting links
var container = //... the region of the page that will be updated with AJAX

// override clicks on the links
links.live('click', function() {
    // grab the link's URL
    var url = $(this).attr('href');
    // add a new history entry
    history.pushState({ path: url }, '', url);
    // load the page fragment into the container with AJAX
    container.load(url);
    // prevent the link click bubbling
    return false;
});

// handle the back and forward buttons
$(window).bind('popstate', function(event) {
    // if the event has our history data on it, load the page fragment with AJAX
    var state = event.originalEvent.state;
    if (state) {
        container.load(state.path);
    }
});

// when the page first loads update the history entry with the URL
// needed to recreate the 'first' page with AJAX
history.replaceState({ path: window.location.href }, '');

At first glance this works pretty nicely. In browsers that support history (right now that’s just Chrome, Safari and its mobile variants) paginating and sorting the list does not refresh the entire page but the browser’s location bar is updated so copy-pasting or bookmarking the URL will give a valid link to the current page. What’s more, the back and forward buttons can be used to step back through the list pages just as if we reloaded the whole page. In non-history-compliant browsers the list page behaves just like it always did; the links reload the entire page.

Unfortunately there’s a problem that was reported to me on GitHub shortly after I uploaded a demo of my scaffolding work. Where everything falls down is when you paginate the list, follow a link off the page (or just use a bookmark or type in a URL), then use the back button to return to it. In Chrome and iPad/iPhone variants of Safari the browser displays just the page fragment from the last AJAX call, losing all the surrounding page along with styling, scripts, etc.

Where things get very odd is that adding a Cache-Control: no-cache header to the AJAX responses makes the problem disappear, presumably because the browser then doesn’t cache the AJAX response and has to use the historical URL to reload the entire page. Remember, in good progressive enhancement style the URL for the full page or the fragment is the same. The server uses the X-Requested-With header to decide whether to return a fragment or the whole page. Obviously, forgoing caching is hardly an acceptable compromise but it’s interesting in that it reveals what the browser is doing. It can’t, surely, be right for the browser to treat a page fragment the same as a full document!

Curiously in desktop Safari this doesn’t happen and the full page is loaded as you would hope. Looking at the User-Agent header it looks like Safari is using an older version of WebKit (533.19.4 rather than 534.13).

You can see the behaviour in my scaffolding sample app. I also ran into the exact same issue today at work whilst trying to add history navigation to this page (which is a good example of why you’d want to use history in the first place). I don’t think it’s just me, either. The same problem can be seen with the demo linked from this post about the history API.

If there are any JavaScript experts out there who can point out my obvious incompetence here, that would be great. Otherwise I guess I’ll have to wait for updates to WebKit to hopefully fix the issue before I can start applying this technique with confidence.