Polymer Tips & Tricks 1

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.

: 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.

: 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

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.