Changing the Auto-Suggest Behaviour in WooCommerce

A request came in this week from a client with a WooCommerce store:

I just placed an order through our website and had an issue with the state. The customer lives in Oregon, so I typed “O” hoping Oregon would auto populate, but it didn’t. I was stuck having to scroll through all 50 states to find Oregon.

An animated Gif showing WooCommerce's default autosuggest behaviour on select boxes.
Confusing for most people.

I tried it, and it’s true. Typing “o” doesn’t narrow the list down much, and the way it does narrow it down doesn’t make much sense in this context: The autosuggest on the State field matches the search phrase anywhere in one of the options, so typing “o” gets you a list of all states with the letter “o” in the state name, ordered alphabetically by state name. Go ahead and try it out in a WooCommerce demo shop.

Out of the box WooCommerce uses SelectWoo, which is essentially Select2 with more accessibility. Select2’s default behaviour of matching the typed text to any part of an option makes sense in many places, especially when searching a store or for categories, but for selecting states, (and countries, although slightly less), it’s confusing.

Nothing’s filterable, but we can set a default

Select2 lets us set the callback function used for matching, so we can customize the matching behaviour, but there’s a problem, the settings for SelectWoo are set in WooCommerce’s country-select.js file, and there’s no way to filter them, so we can’t change the matching behaviour on initialization. It turns out that once a Select2/SelectWoo instance is initialized there’s no way to change the settings either.

When I got to here I thought we were out of luck – either I’d be creating a child theme and re-implementing the checkout, or we were going to live with SelectWoo’s default filtering behaviour. Luckily, I found one way to change the matching callback: It’s possible to set the default SelectWoo options callback before any instances are initialized. So that’s what we’re going to do.

Server-Side

First we need to add a bit of Javascript to our checkout page, so we enqueue a small script that’s dependent on the selectWoo script.


namespace JB\WooAutosuggest;



function enqueue_frontend_scripts() {
    
    if( function_exists('is_checkout') && is_checkout() ) {

        wp_enqueue_script( 'jb-was-checkout-autosuggest', plugins_url( '../assets/checkout-autosuggest.js', __FILE__), array('selectWoo') );

    }
}

add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_frontend_scripts', 20 );

And that’s all the PHP we need! Because our solution changes the default matching behaviour we are careful to only enqueue this script on the Checkout page using WooCommerce’s is_checkout() function.

Client-Side

The enqueued Javascript file is pretty simple as well:

(function($) {
    // Based on custom matcher here: https://select2.org/searching#customizing-how-results-are-matched
    function start_matcher(params, data ) {
        
        // If there are no search terms, return all of the data
        if ($.trim(params.term) === '') {
          return data;
        }

        // Do not display the item if there is no 'text' property
        if (typeof data.text === 'undefined') {
          return null;
        }

        // `params.term` should be the term that is used for searching
        // `data.text` is the text that is displayed for the data object
        // Make sure to compare in the same case! 
        if ( data.text.toLowerCase().indexOf(params.term.toLowerCase() ) === 0 ) {
            return data;
        }
        // Return `null` if the term should not be displayed
        return null;
    }
    $.fn.select2.defaults.set('matcher', start_matcher );
})(jQuery);

There’s a closure so we don’t pollute the global namespace, then we define a new matcher function that matches the beginning of the phrase, and set it as the default matcher. This code runs as soon as it’s ready – it doesn’t wait for a DOMReady event, because by the time DOMReady fires the SelectWoo boxes have probably been initialized already.

Conclusion

I wish we didn’t have to set the defaults for the whole page, but that’s what we’re stuck with at the moment. We could apply different matching patterns to different pages by adding a few more conditions to the PHP block, and a bit more Javascript. This works to make the checkout page work as expected, though.

All the code is up on Github as a WordPress plugin. If it’s useful let me know!

An animated Gif showing WooCommerce's autosuggest matching the beginning of each option.
This makes more sense!

Vanilla Speed

Me: I wonder how to make this cool new HTML5 feature interact with Javascript properly.

Googles…

Stack Overflow Answer #1: Use this great jQuery function!

Stack Overflow Answer #2: Underscore.js is better, us it! You only need 1/2 a character of code.

Smacks desk in frustration.

I’m working on a major overhaul of an existing website that focuses on speed and a great mobile experience that has never used Javascript libraries, (it was originally written in 2007 before they were a thing), and we’re not changing that. For this website a library is a bunch of extra code to download, execute, and take up memory, which adds extra function calls between the code I write and the things that happen in the browser – more CPU cycles, more battery drain, more waiting, and no benefit to our users and customers.

If we were using jQuery I wouldn’t have spent some time looking up the differences in event creation between browsers, and I might not have spent the morning figuring out why a datalist wasn’t appearing properly in Firefox, or maybe I would have had to do that bit of debugging anyway. One thing that doesn’t help me move the project along at all is other people’s over-reliance on Javascript libraries that is so prevalent. In 2007 when I had a problem I could Google it, or post to a forum, and find an answer, (assuming it had been done before), today I experience the sequence of events at the top of this post several times every day. This is why the trio of blog posts last week, (Marco Arment’s post about PPK’s post about John Gruber’s take on Facebook Instant), have really resonated with me. PPK is right, we need to stop relying on libraries, whether it’s jQuery, underscore, or whatever. There are times when a library is the right tool, but with modern Javascript APIs, HTML5, and CSS3, and good browser support for all but the absolute bleeding-edge.

To use a Javascript library is to externalize the cost of development onto your customers and users.

Despite my frustration with the “use some library” responses on Stack Overflow questions using plain vanilla JavaScript has turned out to be an enjoyable learning experience. I’ve been able to make everything work, (so far), with way less code than was needed in 2007. Of course, if your browser can’t handle the project’s Javascript requirements the JS is not loaded at all and you get the plain HTML experience, which isn’t so bad either.

For more on choosing not to burden your users and customers with unneeded slowness, see the ALA article Choosing Vanilla Javascript, which is 15 months old now, and things have just gotten better since it was published!

Really Purging a YouTube Iframe in Internet Explorer

I’ve been working on a website where we display YouTube videos in a modal overlay. This seems simple: absolutely position a div and centre YouTube’s iframe embed within it. When the user tires of the video remove the iframe and hide or remove the div. It works great, then I fired up Internet Explorer for testing.

In IE this is what appears in the Javascript console,  after the iframe is removed:

Errors in the IE Javascript Error Console.
These errors appear after html5player.js shouldn’t exist anymore.

With the iframe gone html5player.js and its code shouldn’t  exist anymore and therefore should not throw errors. It turns out that when an iframe is removed from a web page IE doesn’t completely purge the contents of the iframe from memory right away, if ever.

How do we fix it?

The errors occurred when using jQuery to set the innerHTML of the containing div to an empty string, so I started trying to explicitly remove the iframe’s DOM node using pure Javascript. Element.removeChild() the node instead of destroying it, so that didn’t kill the errors. The docs for IE’s removeNode() don’t specify if the node is held in memory, but the errors continued with it as well.

Some Googling turned up, (I forget where), that setting the iframe’s src attribute to an empty string might help. This makes sense because it’s the equivalent of navigating the iframe to a new page, which should unload any Javascript. Trying to set src=” worked, most of the time. When it didn’t work IE went crazy and became unusable.

In the end I took the approach of trying to wipe out the contents of the iframe as thoroughly as possible, first by changing the iframe’s src to an empty string, then by accessing the iframe’s DOM with Javascript and setting its document element to null. This did the trick. Here’s the function:

// note: modal is the div that contains the YouTube iframe
// jquery is used for some selecting, but the guts of the removal are pure Javascript

function purgeVideo() {
    // purge video must be called while the video is still displayed on the page, 
    // so doing a jQuery fadeOut(purgeVideo) may throw errors, (I think because 
    // fadeOut sets display:none before calling its callback).

    if( modal ) {
        // get the iframe, declare variables
        var ifr = $('iframe', modal), rifr;
        
        // if the iframe is found get the underlying DOM node from jQuery
        if ( ifr.length > 0 ) {
            rifr = ifr.get(0);
        }

        // make extra sure the DOM node exists before starting to work on it.
        if( rifr ) {
            // set src to '' to start removing YouTube and get around cross-domain access restrictions
            rifr.src = '';

            // now set the document element of the iframe to null, (I think this is IE-specific)
            if ( rifr.documentWindow ) {
                rifr.documentWindow.document = null;
                rifr.documentWindow = null;
            }

            // delete the iframe, (also removes it from DOM).
            delete rifr;
        }

        // this won't throw an error, even if the iframe is gone, (we're back to jQuery here).
        ifr.remove();

        // remove anything else, (titles, etc), from the modal overlay
        modal.html('');
    }

}

Starting a Side Project

There’s a side project that I’ve had in my head literally for years. It will solve a minor annoyance I have that comes up a couple of times per year, and an extremely basic version should be quite quick to put together. Even though I’m snowed under with work, one of the annual events that the project will help with is coming up very quickly, so I’m going to get a rough version online to use, with my family, and see what works and what doesn’t. I also intend to blog about the process, and track the time spent on the project.

The nature of the project will be kept secret for a while.

Why now, After so many Years?

TL;DR: I think that web development has progressed to a point that I can get a lot of value out of the project for a reasonable amount of work.

For years this was “the project that I would learn Rails with,” back when Rails was the cool kid on the block. Now PHP, which I’ve worked with for years, has grown up and I don’t feel the same need to learn Rails. This is a chance to try some modern PHP techniques that are difficult to push into legacy projects, in an environment where I’m the one who will suffer if I screw up.

There’s a heavy mobile component, so I plan to use a Responsive design, which I haven’t gone all-in on before, and I recently learned a bunch of performance tricks, so stay tuned for some greased lightning.

SASS, and Grunt build routines for SASS, Grunt, and images have made their way into legacy projects but I’ve never started a project with these in place, and I’m excited to see how it goes.

Finally, by building on the old-fashioned LAMP stack I can host the project on existing servers at no additional cost so the only cost will be my time.

Source, Open and Closed

This unlike a couple of other projects, this one will be largely closed-source. I think it may be a viable business so I intend to find out before giving it away for free. This means I got to have adventures in setting up a Git server instead of relying on GitHub.

I will be sharing things that are not core to the business such as interesting techniques, and maybe my Gruntfile. If the project doesn’t become a business, I may release the source, only time will tell.

Generating Form Elements with Javascript in IE 10

There’s a DOM manipulation gotcha in IE10 that just got me, and Google didn’t help much, so I’m giving Google something to show anyone who has this problem in the future.

When you dynamically generate a form input in Internet Explorer 10 the you must set the type attribute before doing anything else to the element, otherwise any values you set will be ignored when submitting the form, and in some cases will not be displayed. When inspecting elements using IE’s developer tools, however, the correct value appears in the generated document tree.

So this works as expected:

var sub = document.createElement('input');
sub.setAttribute('type', 'submit');
sub.setAttribute('value', 'Submit Generated Button');


But this doesn't:

var sub = document.createElement('input');
sub.setAttribute('value', 'Submit Generated Button');
sub.setAttribute('type', 'submit');

This is particularly hard to catch with radio buttons and checkboxes, (this is what got me). Their default value is “on” which doesn’t tell me much, especially if you’re submitting an array of them.

Here’s a demo. The first button shows its value, (and submits its value), and the second shows, (and submits) the default of “Submit Query.”

The only documentation I found on this behaviour is a passing sentence in the createElement documentation at MSDN. When they say “then set the type property to the appropriate value in the next line of code” they’re serious about the next line of code.

Also, who chooses “Submit Query” as a default value for a submit button in 2013 anyway? Are they trying to confuse people? Shouldn’t it just be “Submit”?