javascripteventspolymerdom-eventspolymer-2.x

Polymer 2 TypeError on Local DOM element when Event triggered


I have a search input field to receive user input and a variable-state button that responds to user input. Notice the on-input event on the search input.

<dom-module id="app-search">
    <template>
        
        <input type="search" id="searchInput" on-input="_onInput" />

        <!-- NOTE: button is set to 'disabled' when there is no value of search field -->
        <paper-button disabled>Done</paper-button>

    </template>
</dom-module>

In the Polymer ready() definition I get a handle to the paper-button element (The ready() function is called after everything, including after the element and its properties have been initialized, so it's a good time to query the local DOM).

ready() {
    super.ready(); // must call this for Polymer to work

    // get handle to paper-button element
    this._doneBtn = Polymer.dom(this.root).querySelector('paper-button');
}

(By the way, I know using this.$ can be used as a syntax short-cut for Polymer.dom(this.root).querySelector() but that syntax only seems to work for targeting elements in the local DOM that have an id, e.g: this.$.searchInput will return a handle to the element with id="searchInput". Does anyone know of a shorthand for targeting non-id, regular elements without having to type Polymer.dom(this.root)...?)

I have a function that detects input events on the search field. If there is a value in the search field, enable the button.

_onInput() {
    // if search input has value
    if (Boolean(this.$.searchInput.value)) {
        // remove disabled attr from button
        this._doneBtn.removeAttribute('disabled');
    } else {
        this._disableBtn();
    }
}

_disableBtn() {
    this._doneBtn.setAttribute('disabled', true);
}

Up to now, this works so that when a user starts typing, the button becomes enabled; when there is no value the button becomes disabled.

However, by convention, users can also delete the input value by clicking the little 'x' that appears on the right-hand-side of the search input. Developers can detect that event by attaching a search event to the input element:

ready() {
    super.ready(); // must call this for Polymer to work

    // get handle to paper-button element
    this._doneBtn = Polymer.dom(this.root).querySelector('paper-button');

    // attach 'search' event listener to search input field
    // call the element's '_disableBtn' function
    this.$.searchInput.addEventListener('search', this._disableBtn);
}

The problem is when I trigger the event by clicking the 'x' that appears when the search field has a value, the this._disableBtn function fires, but this._doneBtn inside the function returns undefined: Uncaught TypeError: Cannot read property 'setAttribute' of undefined.

Assuming this might have to do with an improper type definition, I tried declaring the _doneBtn property in the Polymer properties getter:

static get properties() {
    return {
        _doneBtn: Object // also tried 'String'
    }
}

I also tried querying the DOM from inside the _disabledBtn function again and trying to re-declare the property but I still get the same error:

_disableBtn() {
    if (!this._doneBtn) {
        this._doneBtn = Polymer.dom(this.root).querySelector('paper-button');
    }
    this._doneBtn.setAttribute('disabled', true);
}

Can anyone understand what's happening here? This seems to have something to do with the event listener. Perhaps the DOM is not fully rendered before it's parsed although switching the order of the declarations in the ready() call doesn't make a difference? It could also have something to do with this.

Interestingly, when I console.log(this) inside _disableBtn, the console returns two different this instances, one for the host element (<app-search>) and one for the target element that fired the event: two elements point to 'this'. Also noteworthy is the order that this is printed.

I'm hoping someone wiser than me can help solve what's going on here.


Solution

  • After reading @softjake's response, I was able to solve the problem.

    First, let's revisit the addEventListener() setup that was added in the ready() function:

    ready() {
        super.ready();
    
        // handle to toggle button element
        this._doneBtn = Polymer.dom(this.root).querySelector('paper-button');
    
        // previous method === no worky
        //this.$.searchInput.addEventListener('search', this._disableBtn);
    
        // changed it to:
        this.$.searchInput.addEventListener('search', this._disableBtn.bind(this._doneBtn));
    }
    

    The key here is that I'm using .bind(this._doneBtn) to make sure this, inside the scope of the _disableBtn function refers to this._doneBtn and not some other this (like the parent element or document/window).

    Finally, adjust the _disableBtn function slightly:

    _disableBtn() {
        // previous (also no worky)
        //this._doneBtn.setAttribute('disabled', true);
    
        // changed to:
        this.setAttribute('disabled', true);
    }
    

    Because this already refers to this._doneBtn, we can simply use this.setAttribute(...).