Bartosz Bąbol

Software engineering

Reactive Autocomplete With Polymer and RxJS

I. Intro

In my first blog post I will try to make basic introduction to Polymer and RxJs.

A) What is polymer?

Polymer is library for creating web components. Web components are reusable elements, containers with their own isolated api, templates and style. You may think about them like about AngularJs directives. Difference between angularjs and polymer is fundamental. Polymer is a library which targets one task when angular is a framework for building whole apps. Moreover it is built on top of web components. You can of course use polymer inside angular project and you can build app based only on web components but those tools are targeting different problems in web development.

B) What is Rx?

Rx is, according to main rx page, “an API for asynchronous programming with observable streams”.

C) Assumptions

In following example we will try to create component for searching. After typing we should get list of best suggestions. We would like it to be reusable, it should have his own encapsulated style and what most important API. This is why we have chosen Polymer. Search elements seems simple but they have some interesting corner cases. Some of them:

  • User types something, component send request only if user provides long enough text to search.

  • User types very quickly some phrase for example: ‘polymer tutorial’. Our component should be intelligent enough to know that it should send only one request to server, with param ‘polymer tutorial’, and not sending requests after each typed by user letter. This seems simple. Maybe we need some timeout. Probably we don’t need to incorporate external library like Rx to achieve this.

  • User types some phrase: ‘polymer tutorial’, then request goes to a server, then(before or after first request finishes, that’s not important) user add more letters to phrase for example he adds new words like ‘for beginners’ but in a moment he realises that he is polymer king and he doesn’t need this ‘for beginners’ part, so he quickly deletes it. Our search component should know that people make mistakes and it should not make another request since it would be with same param: ‘polymer tutorial’.

  • User types some phrase: ‘polymer’, request goes to a server, then(before first request finishes) he adds new phrase to existing one: ‘tutorial’. How we could guarantee that the second request with param: ‘polymer tutorial’ will finish after the first one(with param ‘polymer’) and user will get expected suggestions?

Especially last case seems to be non trivial. And this is case where RX fits very well, as we see later, concept of observables(maybe more intuitive name is ‘stream’) gives us abstraction for dealing with such problems.

II. Setup

I assume that you have git, Node, and Bower installed.

A) Clone from github

Before cloning project install polyserve- it will run localhost server for us:

1
npm install -g polyserve

Then clone the project:

1
git clone -b initial-setup https://github.com/BBartosz/polymer-rx-tutorial.git

And finally run:

1
bower install

In you search-component directory run:

1
polyserve

You should have started project at port 8080, so go to http://localhost:8080/components/search-component/. We can start fun part now.

B) Create project from scratch

Other option to start is to create directory named ‘search-component’, then download and run seed-element which is default template for building polymer web components. Follow instructions in Polymer docs till this section: https://www.polymer-project.org/1.0/docs/start/reusableelements.html#develop-and-test I encourage you to preview this template and after running (in /search-component directory where you should place unzipped seed-element files)

1
bower install

and then:

1
polyserve

you should have server on port 8080 started. When you go to http://localhost:8080/components/seed-element/ you should see default docs and demo page for seed-element component. You can read how should be use, how its api looks like and preview in action.

So after review, we can now safely delete seed-element.html, and create new file search-component.html. It should look like this:

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<link rel="import" href="../polymer/polymer.html">

<!--
An element for searching.

Example:

    <search-component></search-component>

@demo
-->
<script>
    Polymer({
        is: 'search-component',
        properties: {},

        // Element Lifecycle

        ready: function() {}
    });
</script>

You should also rename occurences of seed-element and change it to search-component then go to http://localhost:8080/components/search-component/ to check if you did it right. Restart server to have guarantee.

Run project

In you search-component directory run:

1
polyserve

You should have started project at port 8080. We can start fun part now.

III. Closer look at polymer component

First of all we have to register our new component.

search-component.html
1
2
3
4
5
6
7
8
9
10
11
<link rel="import" href="../polymer/polymer.html">

<script>
    Polymer({
        is: 'search-component',
        properties: {},
        ready: function() {
            alert("Component loaded!");
        }
    });
</script>

You can find these info in docs but I will repeat them anyway. Registering an element associates name of the element with a prototype so you can add properties and methods to your component. As you see function Polymer(arg) takes as argument object that defines your element’s prototype. We need to import polymer, then in script we can initialize our component. We do this by specifying its name, must contain “-”. This is because HTML5 specification which says that custom components should have “-” and native html components don’t have to.

Next we can see ready which is lifecycle callback and it means that when your component is loaded then function from ready will be fired. To see this component in action go to demo/index.html and look how it is initialized.

So we have got our component but it is invisible. We can add local DOM which will be encapsulated. To do this we have to wrap it inside <dom-module> with id the same as name defined inside Polymer(args). Inside dom-module we can have <script> tags and <template> tags as well. In <script> we will define logic and behavior and in <template> we will define…html template.

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<link rel="import" href="../polymer/polymer.html">
<!--
An element for searching.

Example:

    <search-component></search-component>

@demo
-->
<dom-module id="search-component">
    <template>
        <input type="text" placeholder="Search something">
    </template>

    <script>
        Polymer({
            is: 'search-component',
            properties: {},

            ready: function() {

            }
        });
    </script>
</dom-module>

Before we go any further we can review another feature of components: properties. One of the tasks of properties is to make your component generic. They are values you can pass to your component when you initialize your component on page. You can set type of property, its default value and even you can observe changes of it. Properties can be used also for data-binding as you will see later.

In our example we can extract already some property, we could parameterize placeholder in input tag. So this is how you define your property:

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
    <script>
        Polymer({
            is: 'search-component',
            properties: {
                inputPlaceholder: {
                    type: String,
                    value: "Default placeholder text"
                }
            },

            ready: function() {

            }
        });
    </script>
</dom-module>

As you can see we set the type (String) in declaration of inputPlaceholder. This tells polymer how to deserialize passed value. For us it is hint how to pass values to component. Value attribute specifies default value if none would be passed to inputPlaceholder.

Now we have to place somehow this property inside DOM:

search-component.html
1
2
3
4
5
6
7
<dom-module id="search-component">
    <template>
        <input type="text"
                       id="searchInput"
                       placeholder="[[inputPlaceholder]]"
                       autofocus>
    ...

We added also autofocus to have this input focused after page refresh.

[[somePropertyName]] creates one-way data binding

{{somePropertyName}} creates one-way or two-way data binding, depending whether property calles somePropertyName is confugured for two way data binding.

More about data binding: https://www.polymer-project.org/1.0/docs/devguide/data-binding.html

Providing attribute value to component looks like following:

demo/index.html
1
2
3
4
5
6
7
...
    <body>
        <search-component input-placeholder="Reactive search">

        </search-component>
    </body>
...

As you can see attribute name passed to component, when it is with dash, is converted to camel case:

input-placeholder => inputPlaceholder

If you would pass argument with camel case to component it would be converted to lowercase.

someOtherAttribute => someotherattribute

More about declaring properties you can find in docs: https://www.polymer-project.org/1.0/docs/devguide/properties.html

Next we need to declare property for keeping all elements which we will possibly get as a result of search. I will give that property name searchResults, its type would be an array, and default value- array with 3 elements just to simulate some results.

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
    <script>
        Polymer({
            is: 'search-component',
            properties: {
                inputPlaceholder: {
                    type: String,
                    value: "Default placeholder text"
                },
                searchResults: {
                    type: Array,
                    value: ["result1", "result2", "result3"]
                }
            },

            ready: function() {

            }
        });
    </script>
</dom-module>

The we would like to show those results somehow. To do it we have to use template tag which we specify as ‘reapeatable’ part of template. This is the way to iterate over collection in polymer. You specify template tag, telling that it is dom-repeat element, you pass collection to items property and then you can do with item what you want.

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
<dom-module id="search-component">
    <template>
        <input type="text"
               id="searchInput"
               placeholder="[[inputPlaceholder]]"
               autofocus>

        <template id="resultList" is="dom-repeat" items="">
            <li></li>
        </template>
    </template>
    ...

So now previewing our component you should see simple input with static, hardcoded 3 options.

It’s time to get some real data. Clean default value of property searchResuls:

search-component.html
1
2
3
4
5
6
...
    searchResults: {
         type: Array,
         value: []
    }
...

We need to install jQuery and rxjs in our project, to do this:

1
2
bower install jquery --save
bower install rxjs --save

This commend will install both libraries (–save flag will add dependency to bower.json)

Import those 2 libraries to our component.

search-component.html
1
2
3
4
<link rel="import" href="../polymer/polymer.html">
<script src="../jquery/dist/jquery.js"></script>
<script src="../rxjs/dist/rx.lite.js"></script>
...

IV. Using RxJs to get search result

Bread and butter of our component. Rxjs!

Ok so lets explain it line by line.

search-component.html
1
2
3
4
5
...
ready: function() {
    var self = this;
    var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
...

Here we are creating observable(or stream as you wish). Observable is mix of 2 design patterns known from software engineering: iterator and observer. Why we need this? In age of big data, big doesn’t only mean huge amount. It means also different sources of data. You might treat big file as data, database with tables and rows as data, social media notifications and events created by user as data. It would be great to treat those data in similar way, to have some abstraction which would help us dealing with such big amount/vast sources of data.We know already something what could be useful: Iterator is simple design pattern: take a collection(no matter what collection) and give me the next element of it as long as it has next element. This is nice, we want to use something like iterator for examples of data mentioned above, without worring about kind of data, that’s great abstraction. You might think about Iterable collection as data producer and you asking for next element as consumer. There is one big problem: iterator doesn’t know about notion of time. It works only for ‘synchronous’ collections. This is unacceptable since we are dealing with web related problems. Moreover iterator throws an error if sth unpredictable or unacceptable happens. This is not ‘happy path’, where we could forget about tedious problems.

But there is observer pattern, pretty similar thing. You give a callback to data producer and it calls you, observes changes. And this repeats after…after what? This is one of the missing factors, there is no automatic way to saying producer: no more data, no more observations! Moreover observer pattern has a lot of downsides, it breaks good software engineering principles like encapsulation and so on. Those 2 patterns are about the same thing: sending data to consumer. And data means: whatever data. List of ints, collection of events, chunks of some big file. But they were not connected with each other. They were not sufficient with they basic form(iterator only for synchronous collections). And this is why observable was created. To mix both observator and iterator, and use it for asynchronous data. In observable we can get the next element. Elements in observable may occur asynchronously. Producer can say: that’s all I have and then method onComplete(), implemented by us to match our needs will be fired. Same with onError(). We can think about Observable as a timeline with max 3 things: event, error and completion. On diagram below there is no error, completion is marked with vertical line, events are letters marbles and there is error marked with X.

—-e—e–e—–e-x-|—>

So how it is working: user is typing something for example ‘polymer’, how would our observable look like?

—e—e—e—e—e—e—e—>

Where ‘e’ is event binded to our stream. Now when we have got some abstraction like observable which should have main feature of Iterator pattern: give me next element!

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  ...
          var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
          observable.subscribe(
              function (event) {
                  console.log(event);
              },
              function (error) {
                  console.log("Something gone wrong: " + error);
              },
              function (e) {
                  console.log("Finally completed!");
              }
          );
  ...

subscribe() subscribes observer to an observable. This is way to mix iterator with observer. As you see it might take 3 parameters. First is function(in this case look for docs, link below) which is fired when taking next element, second is fired when something goes wrong, and last is function to complete. Subscribe() has an alias: forEach(). Look for docs for more explicit explanation: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/subscribe.md For our purposes we can get rid of onError and onComplete functions.

search-component.html
1
2
3
4
5
6
7
8
...
     var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
     observable.subscribe(
         function (event) {
             console.log(event);
         }
     );
...

Now we see in our console that we have some events as results of typing on keyboard, and here we discover great feature of observable: composability. Before we would take next element of observable, we would like to prepare it somehow to fit our needs. We need to map our stream:

search-component.html
1
2
3
4
5
6
7
8
9
...
   var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
   var subscription = observable.map(function (e) {
       return e.target.value;
   }).subscribe(function (event) {
        console.log(event);
      }
   );
...

So map takes a function as parameter and it applies it to each element in observable.

We are returning return e.target.value in passed to map function because it is way to get value input after pushing key. Now our observable looks like this:

—p—po—pol—poly—polym—polyme—polymer—>

Why is that? User pushes ‘p’ so observable looks like this:

—p—>

After transformation it looks the same because in this moment value of input is one letter- p:

—p—>

User types another letter ‘o’ so our transformed observable looks like this:

—p—po–>

Because when user typed ‘o’ value of input was ‘po’, and so on.

Now lets add feature that our search would not hit requests when text is shorter than 2.

search-component.html
1
2
3
4
5
6
7
8
9
10
...
  var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
  var subscription = observable.map(function (e) {
      return e.target.value;
  }).filter(function (inputText) { return inputText.length > 2 })
    .subscribe(function (event) {
       console.log(event);
     }
  );
...

If you are familiar with any functional programming api this is probably obvious for you. If you are not familiar with any of those apis, then probably it is obvious too ;). Filter takes as parameter function. This function must return Boolean and it takes as parameter each element of our stream. If result is true it lets this element go to new filtered observable, else the element is popped out from the observable. So we managed first problem, mentioned on beginning of this text.

Now lets solve one of our initial problems with search. User types very quickly ‘polymer’, we don’t want to call server for each time as we would do in existing solution. When typing, you can see in console that each typed letter is taken in subscribe().

Lets use debounce() method:

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
...
 ready: function() {
    var self = this;
    var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
    var subscription = observable.map(function (e) {
       return e.target.value;
    }).filter(function (inputText) { return inputText.length > 2 })
    .debounce(300)
    .subscribe(function (event) {
       console.log(event);
    }
);
...

How is it working? After map(), and filter, when user types ‘polymer’ very quickly we have observable (and dash ‘-’ means 100ms in our timeline):

–po-pol-poly-polym-polyme-polymer- - ->

debounce(ms) says: ‘I will go further if time in ms specified in my parameter will pass, after event in observable occurs’. After ‘po’ there were 100ms, after ‘pol’ the same, and so on, so debounce is saying ‘Hold your horses! When specified time will pass I will take current value from observable.’

After typing ‘polymer’ 300ms passed so resulting observable after debounce looks like this:

—-polymer—>

This is nice, working as expected, check it out in console.

Now look how we specified our observable:

search-component.html
1
2
3
4
5
6
7
...
 ready: function() {
    var self = this;
    var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
    ...
);
...

It is bounded to an event ‘keyup’, so when uses types arrow it will be added to resulting observable. It would be useless for our purposes since we are interested only in changes in input field. We don’t want this kind of data, we are interested only when text in input field actually changes. Moreover think about situation described above in paragraph C). Users types ‘polymer’ and request go to server then he adds ‘for beginners’, but before request goes (so before 300ms specified in debounce), user deletes this phrase ‘for beginners’ and he is left with ‘polymer’ as before typing. So he already made a request with term ‘polymer’ we don’t want to make it one more time, it would be not necessary. Rxjs gives us a function distinctUntilChanged() which will filter out element of an observable if is is the same as previous element. It will solve those 2 problems described in this paragraph.

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
...
    ready: function() {
        var self = this;
        var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
        var subscription = observable.map(function (e) {
            return e.target.value;
        }).filter(function (inputText) { return inputText.length > 2 })
          .debounce(500)
          .distinctUntilChanged()
          .subscribe(function(e) {
            console.log(e)
          });
...

Its time to write a function which will call the wikipedia server and invoke it when needed.

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
 searchWikipedia: function searchWikipedia(term) {
     return $.ajax({
         url: 'http://en.wikipedia.org/w/api.php',
         dataType: 'jsonp',
         data: {
             action: 'opensearch',
             format: 'json',
             search: term
         }
     }).promise();
 },

 ready: function() {
    var self = this;
    var observable = Rx.Observable.fromEvent(this.$.searchInput, 'keyup');
    var subscription = observable.map(function (e) {
       return e.target.value;
    }).filter(function (inputText) { return inputText.length > 2 })
    .debounce(500)
    .distinctUntilChanged()
    .flatMapLatest(self.searchWikipedia)
    .subscribe(function (event) {
       console.log(event);
    }
);
...

How flatMap() works? Similarly to map(). It applies a function to each element in observable. This function returns new observable or Promise(this is our case) then it fires it, and merge/flatten resulted elements to final/resulted observable. To show simple result try this code, create simple script file or even use JsBin fo this demo. We will use interval() method:

https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/interval.md

someScriptJs.js
1
2
3
4
var observable = Rx.Observable.interval(200).take(10);
observable.subscribe(function (x) {
   return console.log(x);
});

After 200ms observable will produce next number, till number 9. We can now map this observable

someScriptJs.js
1
2
3
4
var observable = Rx.Observable.interval(200).take(10).map(function (x) {return x+1;});
observable.subscribe(function (x) {
   return console.log(x.toString());
});

Now we would transform original observable, and for each number we will add 1. In diagram:

0–1–2–3–4—5–6–7–8–9|>

After mapping: 1–2–3–4—5–6–7–8–9–10|>

Nothing new, but what if we would like to map using asynchronous function. Let’s try it:

someScriptJs.js
1
2
3
4
5
6
7
var source = Rx.Observable.interval(100).take(10).flatMap(function (x) {
   return Rx.Observable.interval(10)
});

source.subscribe(function (x) {
   return console.log(x.toString());
});

And result is console is “[object Object]”. We can now use method mergeAll() to ‘unpack’ this nested object.

someScriptJs.js
1
2
3
4
5
6
7
var source = Rx.Observable.interval(100).take(10).map(function (x) {
   return Rx.Observable.interval(10).take(1)
}).mergeAll();

source.subscribe(function (x) {
   return console.log(x.toString());
});

You might consider flatMap() as shorthand for this mix of map() and mergeAll(), used mostly for many asynchronous objects nested in your observable. Solution with flatMap looks like this:

someScriptJs.js
1
2
3
4
5
6
7
var source = Rx.Observable.interval(100).take(10).flatMap(function (x) {
   return Rx.Observable.interval(10).take(1)
});

source.subscribe(function (x) {
   return console.log(x.toString());
});

Back to our autocomplete. We used flatMapLatest() here. Why? Because one of the biggest problem of our autocomplete is that we don’t have guarantee that result of first sent request will return before result of request sent little later. flatMapLatest() works pretty similar to flatMap() except when new item is emitted by original observable (source), Observable that was generated from the previously-emitted item will disappear, flatMapLatest() will unsubscribe from it and it will begin only mirroring the current one, the latest. As you see non trivial problem solved with one liner. Try commenting debounce() and takeUntilCHanged() and play with flatMap and flatMapLatest(), you will get the difference for sure.

This is mostly end of using RxJs in our example. I encourage you to look at the docs, they are great source of knowledge, written in easy to understand way. There is also a lot of other examples which would help you understand concept of observables.

V. Back to Polymer again

Main RxJs part is finished, now lets polish this component a little bit and make it useful.

In definitions of properties we declared value ‘searchResult’ which is an array. We should populate it with results, after typing by user new phrases. To do this we need to use polymers ‘set’ method. Btw Polymer has its own api for updating properties which are arrays and you can find those methods in docs:

https://www.polymer-project.org/1.0/docs/devguide/properties.html#array-mutation

search-component.html
1
2
3
4
5
6
...
.flatMapLatest(self.searchWikipedia)
.subscribe(function(e) {
    self.set('searchResults', e[1]);
});
...

this.set(‘nameOfProperty’, value) sets value of sets result to second value of resulted observable element, why second value? Because wikipedia sends back response in this form, where 2. value is array of resulted elements. So now you should see how our search input works. BTW You can also find methods for array mutation: https://www.polymer-project.org/1.0/docs/devguide/properties.html#array-mutation

Now we should parameterize it a little bit. So for example debounce method can take parameter from outside. Value of debounce could be provided by user who will use our component. We will call it ‘timeout’ because debounce is reserved name in polymer, btw It is name of method which does similar thing.

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
<script>
Polymer({
    is: 'search-component',
    properties: {
        inputPlaceholder: {
            type: String,
            value: "Default placeholder text"
        },
        searchResults: {
            type: Array,
            value: []
        },
        timeout: {
            type: Number,
            value: 500
        }
    },
...
        .filter(function (inputText) { return inputText.length > 2 })
        .debounce(self.timeout)
        .distinctUntilChanged()

Default value would be set to 500ms. Let’s use it:

demo/index.html
1
2
3
4
...
<body>
    <search-component input-placeholder="Reactive search" timeout="100">
...

Now we can add some polymer sugar to our component. Lets make it more ‘material design’. Go to https://elements.polymer-project.org/ and browse the available components. We will use paper-input. Download it:

1
bower install --save PolymerElements/paper-input#^1.0.0

And import it to our component

search-component.html
1
2
3
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../paper-input/paper-input.html">
...

And use it similarly as input. Everything you need to know is in docs page, from where you downloaded component. I changed placeholder to label to have fancy animation.

search-component.html
1
2
3
4
5
6
7
8
<dom-module id="search-component">
    <template>
        <paper-input type="text"
               id="searchInput"
               value=""
               label="[[inputPlaceholder]]"
               autofocus>
        </paper-input>

Now there is a time to add better dropdown we will make it using following components: paper-button, paper-material, iron-collapse, paper-item. Check those out in elements catalog.

1
bower install --save PolymerElements/iron-collapse#^1.0.0 PolymerElements/paper-button#^1.0.0 PolymerElements/paper-item#^1.0.0 PolymerElements/paper-material#^1.0.0

Now add this dropdown to our component:

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
        ...
        </paper-input>
        <iron-collapse id="collapse">
            <paper-material>
                <div class="collapse-content">
                    <template id="resultList" is="dom-repeat" items="">
                        <paper-item>
                            <paper-button value=""></paper-button>
                        </paper-item>
                    </template>
                </div>
            </paper-material>
        </iron-collapse>
        ...

Set observer function to searchResults property:

search-component.html
1
2
3
4
5
6
7
        ...
        searchResults: {
            type: Array,
            value: [],
            observer: "_resultsChanged"
        },
        ...

and define this function which will collapse the dropdown when it will be needed:

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        ...
             timeout: {
                type: Number,
                value: 500
            }
        },
        _resultsChanged: function(results) {
            var collapse = this.$.collapse;
            if (results.length > 0 && !collapse.opened) {
                this.$.resultList.render();
                collapse.toggle()
            } else if (results.length == 0 && collapse.opened) {
                collapse.toggle()
            }
        },
        searchWikipedia: function searchWikipedia(term) {
        ...

we can add one more condition to our observable because we want our collection to change when input length will decrease to 0:

search-component.html
1
2
3
4
5
6
        ...
            var subscription = observable.map(function (e) {
               return e.target.value;
            }).filter(function (inputText) { return inputText.length > 2 || inputText.length == 0;})

        ...

This should be pretty straightforward. Let’s define function which will do something when we choose result from dropdown.

First of all use _chooseItem() on tap:

search-component.html
1
2
3
4
5
        ...
            <paper-item>
                <paper-button on-tap="_chooseItem" value=""></paper-button>
            </paper-item>
        ...

And now define _chooseItem():

search-component.html
1
2
3
4
5
6
7
        ...
            _chooseItem: function(event, sender) {
                var clickedButtonValue = event.path[1].value;
                this.set('searchTerm', clickedButtonValue);
                this.$.collapse.toggle();
            },
        ...

You can parameterize this component more, add minLength:

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
...
    timeout: {
        type: Number,
        value: 500
    },
    minLength: {
        type: Number,
        value: 2
    }
},
_resultsChanged: function(results) {
...

Use it in our observable filter():

search-component.html
1
2
3
4
5
...
    var subscription = observable.map(function (e) {
        return e.target.value;
    }).filter(function (inputText) { return inputText.length > self.minLength || inputText.length == 0;})
...

And parameterize this ajax call:

search-component.html
1
2
3
4
5
6
7
8
9
...
    minLength: {
        type: Number,
        value: 2
    },
    getRemoteSuggestions: {
        type: Object
    }
...

It will be parse as object. Use it in our observable transformations:

search-component.html
1
2
3
4
5
6
7
8
...
    var subscription = observable.map(function (e) {
        return e.target.value;
    }).filter(function (inputText) { return inputText.length > self.minLength || inputText.length == 0;})
    .debounce(self.timeout)
    .distinctUntilChanged()
    .flatMapLatest(self.getRemoteSuggestions)
...

To pass function from outside you need to wrap your component inside a <template> tag.

demo/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
    <body>
        <template id="wrapperTemplate" is="dom-bind">
          <search-component
                  input-placeholder="Wikipedia search"
                  get-remote-suggestions='[[searchWikipedia]]'>
          </search-component>
        </template>
      </body>

      <script>
        document.querySelector('#wrapperTemplate').searchWikipedia = function searchWikipedia(term) {
          return $.ajax({
            url: 'http://en.wikipedia.org/w/api.php',
            dataType: 'jsonp',
            data: {
              action: 'opensearch',
              format: 'json',
              search: term
            }
          }).promise();
        };
      </script>
...

You can delete searchWikipedia() from search-component.html now.

Now lets add more api documentation to our component. To achieve this simply add comments above each property:

search-component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
...
Polymer({
    is: 'search-component',
    properties: {
        /**
         * `inputPlaceholder` indicates the placeholder of search input
         */
        inputPlaceholder: {
            type: String,
            value: "Default placeholder text"
        },
        /**
         * `searchResults` are the results of searching
         */
        searchResults: {
            type: Array,
            value: [],
            observer: "_resultsChanged"
        },
        /**
         * `timeout` is time in ms after which search request will be send to server
         */
        timeout: {
            type: Number,
            value: 500
        },
        /**
         * `minLength` minimal length of input value needed to make request to server
         */
        minLength: {
            type: Number,
            value: 2
        },
        /**
         * `getRemoteSuggestions` function returning promise. Here you should specify ajax call to server. This
         * function should take search term as parameter
         */
        getRemoteSuggestions: {
            type: Object
        }
    },
...

Check the results, you can build your component’s documentation with no trouble using comments in appropriate places. The same with sample usage of your component.

This component could be extended even more, to work not only with remote collections but I will finish here because this post has grown a little to much. You can find repo with this project on my github(branch: final-version):

https://github.com/BBartosz/polymer-rx-tutorial/tree/final-version

Thanks for reading. Feel free to give me a reply ;)

Comments