Reusable Components - Reusable Features

8. Lesson of RiotJS

We are going to implement a filtering feature and later we will turn that into a reusable mixin.

Martin Muzatko Written by Martin Muzatko
on

Before you feel secure enough in creating components, lets take one more look at the pickers more complex functionality.

Finally adding the awesome features!

We would have needed to rewrite some functionality like filtering and sorting more than once, after all the changes we did to the datahandling. So often it is much easier to implement the complex stuff later on, when you know how you are going to deal with data within that component.

So before you see yourself slinging around with complex features, make sure you know exactly what kind of data your component will process.

Note: Again - This is not more complicated than what we already did from a Riot-perspective, just the JavaScript part gets a bit harder. If you want to, you can use whatever additional library you prefer, if that helps you to accomplish the task.

As for filtering: There are many approaches to do so. You could use jQuery, Lodash, whatever. I prefer to use vanilla JavaScript.

The Layout

RIOT
<input oninput={filter} onfocus={openList} type="text">
<tr each={item in items} if={!item.filtered} class={selected: item.selected} onclick={pick}></tr>

Similar to item.selected, I use another item flag to decide whether or not to display the item in the table.

The Event

Filtering always belonged to one of the more tough tasks. There are hundreds of products and tools out there, just to master filtering with javascript. Diving into the many other aspects of filterling like performance would be overkill for this lesson, which is why we will focus on basic filtering across all rows and columns.

JAVASCRIPT
filter(event) {
    for (var item in this.items)
    {
        item = this.items[item]
            var combinedValue = ''
            for (var label in this.labels)
            {
                label = this.labels[label]
                combinedValue += item[label]
            }
            item.filtered = !~combinedValue.toLowerCase().indexOf(event.target.value.toLowerCase())
    }
}

What are we doing exactly? We loop through each item, combine their active labels values (e.g. when we have chosen name, email and country to display, only these labels will be concatenated) and then look if the inputs value is occuring in this string. Additionally we make our input and comparisonstring lowercase so searching is easier.

Jack Stevens    jstevens0@cdbaby.com    France

becomes

jack stevensjstevens0@cdbaby.comfrance

Then we set our filtered flag. indexOf returns -1 if there are no matches or the index of the first occurence. But we need true or false to determine whether or not the item is filtered. You could either compare the result to != -1 or reverse the number (~) and negate it (!). I prefer the shorter version.

<script type="riot/tag"> <application> <h2>Select Authors for this lesson</h2> <picker items=\{lessonUsers\} labels=\{['name', 'email', 'country']\}></picker> <h2>Select People for hire</h2> <picker items=\{lessonUsers\} labels=\{['name', 'job']\}></picker> this.lessonUsers = [ \{"name":"Jack Stevens","email":"jstevens0@cdbaby.com","country":"France","job":"Payment Adjustment Coordinator"\}, \{"name":"Teresa Lawson","email":"tlawson1@meetup.com","country":"Indonesia","job":"Computer Systems Analyst I"\}, \{"name":"Timothy Alvarez","email":"talvarez2@chicagotribune.com","country":"Russia","job":"Software Test Engineer I"\}, \{"name":"Ernest Lawrence","email":"elawrence3@google.com.au","country":"Philippines","job":"Software Engineer II"\}, \{"name":"Lois Patterson","email":"lpatterson4@forbes.com","country":"Indonesia","job":"Assistant Professor"\}, \{"name":"Tina Nelson","email":"tnelson5@ihg.com","country":"Myanmar","job":"Occupational Therapist"\}, \{"name":"Carlos Torres","email":"ctorres6@sfgate.com","country":"Mexico","job":"Analog Circuit Design manager"\}, \{"name":"Donna Henderson","email":"dhenderson7@liveinternet.ru","country":"China","job":"Safety Technician I"\}, \{"name":"Randy Grant","email":"rgrant8@blogs.com","country":"Japan","job":"Pharmacist"\}, \{"name":"Denise Campbell","email":"dcampbell9@illinois.edu","country":"Pakistan","job":"Analyst Programmer"\}, \{"name":"Pamela Cooper","email":"pcoopera@sitemeter.com","country":"China","job":"Software Engineer I"\}, \{"name":"Stephen Rogers","email":"srogersb@sogou.com","country":"Netherlands","job":"Payment Adjustment Coordinator"\}, \{"name":"Helen Powell","email":"hpowellc@accuweather.com","country":"United States","job":"Budget/Accounting Analyst III"\}, \{"name":"Fred Wheeler","email":"fwheelerd@marketwatch.com","country":"Portugal","job":"Staff Scientist"\}, \{"name":"Bonnie Bradley","email":"bbradleye@privacy.gov.au","country":"Thailand","job":"Engineer IV"\}, \{"name":"Michelle Medina","email":"mmedinaf@theguardian.com","country":"Nicaragua","job":"Physical Therapy Assistant"\}, \{"name":"Henry Howell","email":"hhowellg@mlb.com","country":"France","job":"Recruiting Manager"\}, \{"name":"Jose Ross","email":"jrossh@livejournal.com","country":"Ukraine","job":"Account Executive"\}, \{"name":"Jesse Hart","email":"jharti@joomla.org","country":"Republic of the Congo","job":"Community Outreach Specialist"\}, \{"name":"Margaret Simpson","email":"msimpsonj@de.vu","country":"Russia","job":"Database Administrator III"\}, \{"name":"Carol Warren","email":"cwarrenk@devhub.com","country":"Japan","job":"Media Manager III"\}, \{"name":"Shirley Ray","email":"srayl@ca.gov","country":"Russia","job":"Marketing Assistant"\}, \{"name":"Kenneth Wood","email":"kwoodm@google.fr","country":"Indonesia","job":"Junior Executive"\}, \{"name":"Ruth Bailey","email":"rbaileyn@zimbio.com","country":"France","job":"Design Engineer"\}, \{"name":"James Morales","email":"jmoraleso@aol.com","country":"Honduras","job":"Senior Sales Associate"\}, \{"name":"Bobby Bradley","email":"bbradleyp@webeden.co.uk","country":"Philippines","job":"Environmental Specialist"\}, \{"name":"Peter Henry","email":"phenryq@printfriendly.com","country":"Vietnam","job":"Budget/Accounting Analyst III"\}, \{"name":"Benjamin Banks","email":"bbanksr@prlog.org","country":"Peru","job":"Teacher"\}, \{"name":"Stephanie Kelley","email":"skelleys@mayoclinic.com","country":"China","job":"Recruiting Manager"\}, \{"name":"Roger Wells","email":"rwellst@google.com.hk","country":"China","job":"Safety Technician III"\} ] </application> </script> <script type="riot/tag"> <picker> <div class="picked-items"> <span if=\{item.selected\} each=\{item in items\}> \{item[mainLabel]\} </span> <input onkeyup=\{filter\} onfocus=\{openList\} type="text"> </div> <div if=\{isPicking\} class="picker-list"> <a onclick=\{closeList\}>&times;</a> <table> <tr> <th each=\{label in labels\}>\{label\}</th> </tr> <tr if=\{!item.filtered\} class=\{selected: item.selected\} onclick=\{pick\} each=\{item in items\}> <td each=\{label in labels\}> \{item[label]\} </td> </tr> </table> </div> <style> .picker-list a \{ position: absolute; cursor: pointer; font-size: 1.25em; right: .5em; top: .5em; \} .picker-list \{ position: relative; display: inline-block; background: #EEE; \} .picker-list table \{ cursor: pointer; margin-top: 1em; \} .picker-list table tr:nth-child(odd) \{ background: #DDD; \} table td \{ padding: .4em; \} .picker-list table \{ border-collapse: collapse; \} .picked-items span \{ margin-right: .5em; margin-bottom: .2em; padding: .1em; display: inline-block; \} .selected, .picked-items span \{ background: #3377B5; color: #F1F1F1; \} .picker-list .selected:nth-child(odd) \{ background: #4488C6; \} input \{ display: block; \} </style> this.isPicking = false openList() \{ this.isPicking = true \} closeList() \{ this.isPicking = false \} pick(event) \{ event.item.item.selected = !event.item.item.selected \} filter(event) \{ for (var item in this.items) \{ item = this.items[item] // We need a combinedvalue, consisting out of the itemvalues with the labels that exist // We search in a string that combines the value of each data row. var combinedValue = '' for (var label in this.labels) \{ label = this.labels[label] // concatenating the item value combinedValue += item[label] \} // !~ means: If NOT -1, we lowercase everything so searching is easier. item.filtered = !~combinedValue.toLowerCase().indexOf(event.target.value.toLowerCase()) \} \} this.items = opts.items this.labels = opts.labels || Object.keys(this.items[0]) this.mainLabel = opts.mainLabel || this.labels[0] </picker> </script> <application></application>

Reusing functionality

Some features are so tiny, compared to the tag, you wouldn't put them in their own custom tag, neither would you want to rewrite or copypaste all of that. The filtering function is one of these. If we decided to use that filtering mechanism somewhere else without the picker, we would have needed to copypaste that - not that clean.

Riot offers a clean way to reuse functionality across tags: mixins.

Mixins contain functions and properties that are merged into the tag instance when using that mixin.

Registering a mixin

Globall

JAVASCRIPT
riot.mixin('arrayhelper', arrayhelper)

Usage in Tag:

JAVASCRIPT
this.mixin('arrayhelper')

Local usage without globally registering

JAVASCRIPT
this.mixin(arrayhelper)

To make our filtering feature reusable, we have to move the filter function to an object of functions, which we will call arrayhelper and parametrize it.

arrayhelper.js

JAVASCRIPT
var arrayhelper = {
    filterByLabels : function(items, labels, filter)
    {
        for (var item in items)
        {
            item = items[item]
            var combinedValue = ''
            for (var label in labels)
            {
                label = labels[label]
                combinedValue += item[label]
            }
            item.filtered = !~combinedValue.toLowerCase().indexOf(filter.toLowerCase())
        }
    }
}

picker.js

We include the mixin by using the variable we assigned the functions to:

JAVASCRIPT
this.mixin(arrayhelper)

The filter event function is pointing now to the function of our mixin:

JAVASCRIPT
filter(event) {
    this.filterByLabels(this.items, this.labels, event.target.value)
}

Our picker looks a lot more cleaner now, codewise. Additionally we can reuse that filter function everywhere now.

Here is the final component:

<script> var arrayhelper = \{ filterByLabels : function(items, labels, filter) \{ for (var item in items) \{ item = items[item] // We need a combinedvalue, consisting out of the itemvalues with the labels that exist // We search in a string that combines the value of each data row. var combinedValue = '' for (var label in labels) \{ label = labels[label] // concatenating the item value combinedValue += item[label] \} // !~ means: If NOT -1, we lowercase everything so searching is easier. item.filtered = !~combinedValue.toLowerCase().indexOf(filter.toLowerCase()) \} \} \} </script> <script type="riot/tag"> <application> <h2>Select Authors for this lesson</h2> <picker items=\{lessonUsers\} labels=\{['name', 'email', 'country']\}></picker> <h2>Select People for hire</h2> <picker items=\{lessonUsers\} labels=\{['name', 'job']\}></picker> this.lessonUsers = [ \{"name":"Jack Stevens","email":"jstevens0@cdbaby.com","country":"France","job":"Payment Adjustment Coordinator"\}, \{"name":"Teresa Lawson","email":"tlawson1@meetup.com","country":"Indonesia","job":"Computer Systems Analyst I"\}, \{"name":"Timothy Alvarez","email":"talvarez2@chicagotribune.com","country":"Russia","job":"Software Test Engineer I"\}, \{"name":"Ernest Lawrence","email":"elawrence3@google.com.au","country":"Philippines","job":"Software Engineer II"\}, \{"name":"Lois Patterson","email":"lpatterson4@forbes.com","country":"Indonesia","job":"Assistant Professor"\}, \{"name":"Tina Nelson","email":"tnelson5@ihg.com","country":"Myanmar","job":"Occupational Therapist"\}, \{"name":"Carlos Torres","email":"ctorres6@sfgate.com","country":"Mexico","job":"Analog Circuit Design manager"\}, \{"name":"Donna Henderson","email":"dhenderson7@liveinternet.ru","country":"China","job":"Safety Technician I"\}, \{"name":"Randy Grant","email":"rgrant8@blogs.com","country":"Japan","job":"Pharmacist"\}, \{"name":"Denise Campbell","email":"dcampbell9@illinois.edu","country":"Pakistan","job":"Analyst Programmer"\}, \{"name":"Pamela Cooper","email":"pcoopera@sitemeter.com","country":"China","job":"Software Engineer I"\}, \{"name":"Stephen Rogers","email":"srogersb@sogou.com","country":"Netherlands","job":"Payment Adjustment Coordinator"\}, \{"name":"Helen Powell","email":"hpowellc@accuweather.com","country":"United States","job":"Budget/Accounting Analyst III"\}, \{"name":"Fred Wheeler","email":"fwheelerd@marketwatch.com","country":"Portugal","job":"Staff Scientist"\}, \{"name":"Bonnie Bradley","email":"bbradleye@privacy.gov.au","country":"Thailand","job":"Engineer IV"\}, \{"name":"Michelle Medina","email":"mmedinaf@theguardian.com","country":"Nicaragua","job":"Physical Therapy Assistant"\}, \{"name":"Henry Howell","email":"hhowellg@mlb.com","country":"France","job":"Recruiting Manager"\}, \{"name":"Jose Ross","email":"jrossh@livejournal.com","country":"Ukraine","job":"Account Executive"\}, \{"name":"Jesse Hart","email":"jharti@joomla.org","country":"Republic of the Congo","job":"Community Outreach Specialist"\}, \{"name":"Margaret Simpson","email":"msimpsonj@de.vu","country":"Russia","job":"Database Administrator III"\}, \{"name":"Carol Warren","email":"cwarrenk@devhub.com","country":"Japan","job":"Media Manager III"\}, \{"name":"Shirley Ray","email":"srayl@ca.gov","country":"Russia","job":"Marketing Assistant"\}, \{"name":"Kenneth Wood","email":"kwoodm@google.fr","country":"Indonesia","job":"Junior Executive"\}, \{"name":"Ruth Bailey","email":"rbaileyn@zimbio.com","country":"France","job":"Design Engineer"\}, \{"name":"James Morales","email":"jmoraleso@aol.com","country":"Honduras","job":"Senior Sales Associate"\}, \{"name":"Bobby Bradley","email":"bbradleyp@webeden.co.uk","country":"Philippines","job":"Environmental Specialist"\}, \{"name":"Peter Henry","email":"phenryq@printfriendly.com","country":"Vietnam","job":"Budget/Accounting Analyst III"\}, \{"name":"Benjamin Banks","email":"bbanksr@prlog.org","country":"Peru","job":"Teacher"\}, \{"name":"Stephanie Kelley","email":"skelleys@mayoclinic.com","country":"China","job":"Recruiting Manager"\}, \{"name":"Roger Wells","email":"rwellst@google.com.hk","country":"China","job":"Safety Technician III"\} ] </application> </script> <script type="riot/tag"> <picker> <div class="picked-items"> <span if=\{item.selected\} each=\{item in items\}> \{item[mainLabel]\} </span> <input onkeyup=\{filter\} onfocus=\{openList\} type="text"> </div> <div if=\{isPicking\} class="picker-list"> <a onclick=\{closeList\}>&times;</a> <table> <tr> <th each=\{label in labels\}>\{label\}</th> </tr> <tr if=\{!item.filtered\} class=\{selected: item.selected\} onclick=\{pick\} each=\{item in items\}> <td each=\{label in labels\}> \{item[label]\} </td> </tr> </table> </div> <style> .picker-list a \{ position: absolute; cursor: pointer; font-size: 1.25em; right: .5em; top: .5em; \} .picker-list \{ position: relative; display: inline-block; background: #EEE; \} .picker-list table \{ cursor: pointer; margin-top: 1em; \} .picker-list table tr:nth-child(odd) \{ background: #DDD; \} table td \{ padding: .4em; \} .picker-list table \{ border-collapse: collapse; \} .picked-items span \{ margin-right: .5em; margin-bottom: .2em; padding: .1em; display: inline-block; \} .selected, .picked-items span \{ background: #3377B5; color: #F1F1F1; \} .picker-list .selected:nth-child(odd) \{ background: #4488C6; \} input \{ display: block; \} </style> this.mixin(arrayhelper) this.isPicking = false openList() \{ this.isPicking = true \} closeList() \{ this.isPicking = false \} pick(event) \{ event.item.item.selected = !event.item.item.selected \} filter(event) \{ this.filterByLabels(this.items, this.labels, event.target.value) \} this.items = opts.items this.labels = opts.labels || Object.keys(this.items[0]) this.mainLabel = opts.mainLabel || this.labels[0] </picker> </script> <application></application>

Summary

If you take inventory of your features early on and turn them into reusable mixins, you are making your life a lot easier. How often do you see yourself looking where you already solved that problem just to look for a quick and dirty solution on stackoverflow or google?

As always: If you have any questions, please leave a comment below.

Do you need inspiration how to solve those tricky issues? Issues that when resolved once and for all, would ease a lot of daily user interface interaction.

Get on my list! You are invited to reply to any email with your important questions and get a hand-crafted reply with hands-on guides. You will learn how to solve your individual use-cases. Fill in your name below and get in touch today :) . It is about a nice conversation from developer to developer after all.

Previous Lesson

Reusable Components - Configuring Options

Options can be used for a lot more. We can accomplish a variety of usecases with just a few options. Keep a close eye, as we proceed at a faster pace.

Next Lesson

Reusable Components - Observing Events

We are finally going to obtain the data in the picker and we upgrade our component with an Event-API. Many more awesome things happen when we combine our tag with other components.

See all Lessons