Polymer Expressions – Filtering

polymer-logo

Polymer is a great library. I use it every day and I’m often finding hidden gems that make life so much easier. Today I wanted to touch on some goodness of Polymer Expressions. In particular setting up and using filters.

It used to be that setting up filters in Polymer was a little bit of a task, having to create a new object which implemented or extended some kind of filter object. I don’t recall now the details, just that I recall using it was very inconvenient. I’m not sure when exactly it changed, as I gave up on filters and transformers for quite a while. However looking at them now, they have become supremely easy to use.

Filters can be used on single items or even in repeat templates. To show a couple small examples on how filters can be used very easily we’ll create a list which allows for a very, very, simple fuzzy search. In order to do this we’ll create a our own polymer component called fuzzy-list. The goal is to have a list of items which will filter based on characters typed into a text box. All items in the list should also be entirely uppercase, just to take the contrived example from the Polymer Expressions Pub package.

First we’ll setup the simple stuff for our component. Below is the initial code we’ll use, and which we’ll add to.

<link rel="import" href="packages/polymer/polymer.html">

<polymer-element name="fuzzy-list">
  <template>
    <div>
      <ul>
        <template repeat="{{ item in list }}">
          <li>{{ item }}</li>
        </template>
      </ul>
    </div>
  </template>
  <script type="application/dart" src="fuzzy_list.dart"></script>
</polymer-element>

And here’s our class:

import 'package:polymer/polymer.dart';

@CustomTag('fuzzy-list')
class FuzzyList extends PolymerElement {
  @observable List list;
  
  FuzzyList.created() : super.created() {
    list = toObservable(['apple','banana','orange', 'pear', 
                         'grape', 'pineapple',
                         'strawberry', 'peach']);
  }
}

Now filters in Polymer Expressions must be a one-argument function. To start with we’ll create the simple uppercase filter. We add the following one-line function to our FuzzyList class.

uppercase(String str) => str.toUpperCase();

It’s really that simple. Now any String which is passed to the uppercase filter will be returned in uppercase. To see this, change line 8 of our HTML file to be:

<li>{{item | uppercase}}</li>

That’s great, now we have a filter in place to capitalize our list. Note that this affects only the display and it does not change the contents of our original list. Now we need to be able to filter the list contents itself. Our filter should accept an argument which is the text to apply for the filter, and the list we want to filter. But Polymer Expression filters can only accept one argument, how can we pass two? First lets see the code then we’ll look at what’s going on.

First A couple of additions to the HTML. First line appears above the unordered list to allow us to specify the filter. The second line replaces the existing template repeat.

<label>Filter: <input type="text" value="{{filterStr}}"></label>
<!-- ... -->
<template repeat="{{ item in list | filter(filterStr) }}">

Next we update our class to add the filterStr variable and add the filter function

  @observable String filterStr = '';
  // ...
  filter(String str) => (List l) {
    if(str == null || str.trim().isEmpty) return l;
    return l.where((el) {
      for(var i = 0; i < str.length; i++) {
        if(!el.contains(str[i])) return false;
      }
      return true;
    });
  };

We bind the inputs value to filterStr which is an observable, and as such will receive updates as we type and cause any expression using it to be re-evaluated. Secondly we pass the list in our template repeat to the filter filter. The filter function is also passed our filterStr. You'll notice the filter function accepts a string, not a list, and returns a function, rather than a list. The reasoning for this is actually a little complex. If we just passed the filter without the arguments, our list would not be automatically updated when the value of filterStr changed.

Instead what happens is that when the value of filterStr changes, it causes filter(filterStr) to be re-evaluated. When that evaluates, it returns a new function to which we pass the list. Essentially, filter is not actually our filter as used by Polymer Expressions, rather the function it returns is the filter. Using Dart's closures, we essentially generate a new function enclosing the value passed to filter within. This is then applied to the list we pass.

The anonymous closure itself is pretty straight forward. It first checks if the string passed when creating the closure was null or empty, if so it just returns the list. If not, it returns a sub-list (well iterable on the list) of elements which contain all the characters contained in the string which was passed. If an element does not contain all the characters, then it's not included in the iterable returned. See the API documents for List.where for more information.

A few things to note, this fuzzy search is extremely simple. For instance it ignores duplicates, searching for "pp" will give the same results of just "p". Secondly it doesn't verify order that characters appears. For instance searching for "ap" will return "pear" and "peach" even though the letters appear in different order. This was just intended as a simple example and would need a lot of updates to be functional as most people would expect it to be. I leave it as an exercise for the reader to implement a better fuzzy search algorithm.

This entry was posted in Dart and tagged , , . Bookmark the permalink.

Have Something To Add?

Loading Facebook Comments ...
Loading Disqus Comments ...