Search code examples
javascriptprototypejs

Prototype.js 1.7 uncaught exception: Syntax error, unrecognized expression: [object HTMLInputElement]


This code worked for Prototype.js 1.6, but since upgrading to 1.7 i get an error: uncaught exception: Syntax error, unrecognized expression: [object HTMLInputElement]

document.observe('dom:loaded', function() {
    $$('.validate-length').each(function(elem) {
        var note = elem.next('.note');
        var counter = new Element('span');
        note.insert(counter);

        var curLen = $(elem).getValue().length;
        var maxLen = elem.className.match(/maximum-length-(\d+)/)[1];
        var count  = maxLen - curLen;

        if (curLen >= maxLen) {
            counter.update(' (-' + count + ')').setStyle({'color': 'red'});
        } else {
            counter.update(' (+' + count + ')').setStyle({'color': 'green'});
        }

        $$(elem).invoke('observe', 'keyup', function() {
            var curLen = $(elem).getValue().length;
            var count  = maxLen - curLen;
            if (curLen >= maxLen) {
                counter.update(' (-' + count + ')').setStyle({'color': 'red'});
            } else {
                counter.update(' (+' + count + ')').setStyle({'color': 'green'});
            }
        });
    });
});

Seems something is wrong here: $$(elem).invoke('observe', 'keyup', function() {

Appreciate any help.


Solution

  • When you are inside an enumerator, looping over a collection, your instance variable (elem) is a singular object, not a collection. So you want to use elem.observe('keyup', function() { ... }); to instantiate your observer.

    The "double-dollar" function finds a collection of DOM elements that match the CSS selector passed as its first argument. But you've already done that at the top of the script, so now each member of that collection is already "found" (and extended with all of Prototype's methods), and does not need any further wrapping in order to be acted upon.

    There's some optimizations available to you in this as well. You should create a single function to do the countdown, maybe using the addMethods factory to chain it on to the input's prototype, so you don't need to create a new anonymous function for each of your textareas. Next, if you have more than a very few of these inputs on each page, you might want to look at the "deferred observer" pattern to allow you to write one observer for the whole page, rather than creating a separate one for each input.

    Examples, as requested:

    Element.addMethods({
      // setup once, memoize the counter element and maxLen
      prepare_for_countdown: function(element){
        var elm = $(element);
        // even if you call it multiple times, it only works once
        if(!elm.retrieve('counter')){
          var counter = new Element('span');
          elm.next('.note').insert(counter);
          elm.store('counter', counter);
          var maxLen = elm.readAttribute('data-max-length');
          elm.store('maxLen', maxLen);
        }
        return elm; // so you can chain
      },
      // display the value, run once at load and on each observed keyup
      countdown: function(element){
        var elm = $(element);
        var curLen = elm.getValue().length;
        var maxLen = elm.retrieve('maxLen');
        var count  = maxLen - curLen;
        var counter = elm.retrieve('counter');
        if (curLen >= maxLen) {
          counter.update(' (' + count + ')').setStyle({'color': 'red'});
        } else {
          counter.update(' (+' + count + ')').setStyle({'color': 'green'});
        }
        return elm;
      }
    });
    
    // run setup and call countdown once outside of listener to initialize
    $$('.validate-length').invoke('prepare_for_countdown').invoke('countdown');
    
    // deferred listener, only responds to keyups that issue from a matching element
    document.on('keyup', '.validate-length', function(evt, elm){
      elm.countdown();
    });