James Garbutt

Software engineer. Front-end @crispthinking. JavaScript & HTML5 expert.

Polymer Tips & Tricks 1

September 25, 2016

Edit Page

This is the first in a series of posts about the various problems I have tackled while using Polymer.

Over the last several months, I have been working hard on a Polymer-based iOS app, using Cordova. I have met many, many bumps in the road along the way and this will be essentially a log of my solutions to the problems I encountered.

Sorting/Filtering an iron-list

Have you ever tried to sort or filter an iron-list? Not such a simple thing to do, and most solutions are rather inefficient.

In my app, I have a large list of cards which are filtered based on the category they should appear for and sorted based on date. This means I can hold one single array of all cards in memory (from Firebase) and simply filter it when the user changes category.

Easy in dom-repeat:

    <template is="dom-repeat" items="{{cards}}" sort="_sortFn" filter="_filterFn">
        <my-card item="{{item}}"></my-card>
    </template>

However, this is horribly inefficient and resulted in my app using ~300MB of memory on an iPad. Why? Because all cards exist in view at once (even if below the fold) and must have their layout re-calculated on several occasions when actions elsewhere occur.

Anyhow, the clear solution is to use an iron-list:

    <iron-list items="{{cards}}">
        <my-card item="{{item}}"></my-card>
    </iron-list>

But, we have no way to filter or sort the cards like in a dom-repeat (there is an issue open for this).

The solution most people, including myself, seem to come up with is a computed binding:

    <iron-list items="{{_sortAndFilter(cards)}}">

Where _sortAndFilter returns the sorted and filtered array. Here is a list of each binding I tried and why it was a bad idea:

  • _sortAndFilter(cards): It is computed only when the entire array changes. Splices and sub-property changes don’t propagate.
  • _sortAndFilter(cards.splices): Sub-property changes don’t propagate. Array is fully recomputed each time a child is moved/removed/added.
  • _sortAndFilter(cards.*): Array is fully recomputed each time any change occurs.

In all these cases, the array is fully recomputed on any change, so iron-list is forced to do a full refresh. This means you’ll likely lose your scroll position, too, a horrible experience for users.

Solution

My solution to this, which you can find here, is an element which essentially holds a copy of the initial array with any sorts and filters applied.

    <array-filter items="{{cards}}" filtered="{{_cards}}" filter="_filterFn" sort="_sortFn"></array-filter>
    <iron-list items="{{_cards}}"

The way this works internally is:

  • Observe items.*
  • Use linkPaths to link each item’s change paths in source and filtered array, so sub-property changes propagate
  • Sort/filter splices and splice them into the filtered array in order, rather than recomputing the whole array

dom-if inside an iron-list

Before I explain this, I do not recommend using a dom-if to filter out iron-list items. Please use the solution in the previous section.

    <iron-list items="{{items}}">
        <template is="dom-if" if="[[item.enabled]]">
            <div>[[item.id]]</div>
        </template>
    </iron-list>

Many of you have tried the above before and, soon enough, found that it doesn’t work and causes iron-list to flip.

The reason for this is because iron-list will assume the template elements are children, it will try position them like any other child. To solve this, wrap it like so:

    <iron-list items="{{items}}">
        <div>
            <template is="dom-if" if="[[item.enabled]]">
                <div>[[item.id]]</div>
            </template>
        </div>
    </iron-list>

This also has its issues though. If item.enabled changes at some point or simply isn’t immediately available, the size of your item will change and confuse iron-list. So you must have something like:

    <template is="dom-if" if="[[item.enabled]]" on-dom-change="_onDomIfChange">

Inside _onDomIfChange, simply do something like:

    _onDomIfChange: function(e) {
        var item = this.$.list.modelForElement(e.currentTarget.parentElement).item;
        this.$.list.updateSizeForItem(item);
    }

Preventing duplicate iron-a11y-keys events

When using iron-a11y-keys with multiple key combinations, it is possible that one combination may contain the other:

    <iron-a11y-keys keys="shift+enter enter" ...></iron-a11y-keys>

If you listen for the keys-pressed event, you’ll soon find that when you press shift+enter, you get 2 events: one for shift+enter, one for enter.

To stop any further events firing, it turns out you must preventDefault() on the keyboardEvent, not on the event you are given:

    _onKeysPressed: function(e) {
        e.detail.keyboardEvent.preventDefault(); // works
        e.preventDefault(); // does not work
    }

You can see this in a demo here.

Thanks to ergo for finding this.