Reusable Components - Observing Events

9. Lesson of RiotJS

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.

Martin Muzatko Written by Martin Muzatko
on

UI components almost always offer ways to observe and listen to whats happening. This is especially useful if you can't achieve every aspect of the component through configuration. You have to listen to the user input and react accordingly, depending on what you need. Such event listening API may look like noUiSlider or a jQuery UI component.

We are going to implement something similar that can broadcast events and listens to them.

What kind of user input is there for our picker?

  • filtered - return the filtered items
  • picked - return the picked items
  • opened
  • closed

For this, Riot implemented a generic tool to do that: the Observable.

Observable is built into every tag instance already. Events like update, mount and unmount already make use of that. Which means that you can already listen to this.on('mount', function(){}) or on update etc. To create custom events we have these possibilities

Trigger

JAVASCRIPT
this.trigger('picked', data)

This triggers the named event that you can listen to from outside the tag.

Listening

JAVASCRIPT
this.on('picked', function(data){})

whenever the tag triggers picked, then the supplied function is called.
Note: make sure to add .bind(this) after the function ending curly bracket, to work in the tags scope.

JAVASCRIPT
this.one('picked', function(data){})

The function is called only once, and then never again.

Stop listening

JAVASCRIPT
this.off('picked')

Note: "this" could be the instance of any tag. So this.parent.trigger(...) works and this.tags.modal.on('opened',...) too.

You can also implement this generic event-listening and triggering tool in your personal javascript too. Just add riot.observable(this) to any object you like. You can find more information about this in the official RiotJs guide.

Using Observable as our Data API

We already have the tag-level events like pick or filter - so we can trigger from within these events

JAVASCRIPT
filter(event) {
    this.filterByLabels(this.items, this.labels, event.target.value)
    this.trigger('filtered', this.items.filter(function(item){return !item.filtered}))
}

When we listen to filtered now, we will get all items that are displayed when we filter for something.

Important: If you listen to custom events and update some data, you have to tell riot to update the JS expressions. This is because Riot only updates when you fire tag-level events like onclick or when the tag gets mounted. So make sure to add this.update() to the listener. See example below

<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"> <filtered-list> <h2>Select Authors for this lesson</h2> <small if=\{authorFilter.length\}>filtered \{authorFilter.length\}</small> <small if=\{authorPicks.length\}>picked \{authorPicks.length\}</small> <trigger-picker ref="authorPicker" items=\{lessonUsers\} labels=\{['name', 'email', 'country']\}></trigger-picker> <h2>Select People for hire</h2> <trigger-picker ref="employeePicker" items=\{lessonUsers\} labels=\{['id', 'name', 'job']\}></trigger-picker> this.authorPicks = [] this.authorFilter = [] this.on('mount', function()\{ this.refs.authorPicker.on( 'filtered', function(data) \{ this.authorFilter = data this.update() \}.bind(this) ) this.refs.authorPicker.on( 'picked', function(data) \{ this.authorPicks = data this.update() \}.bind(this) ) \}) this.authorFilter = false 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"\} ] </filtered-list> </script> <script type="riot/tag"> <trigger-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] </trigger-picker> </script> <filtered-list></filtered-list>

Also note, that when you listen to an event, you are in the scope of the originating tag. So if you need to change something in the tag you listen to, make sure to add .bind(this) to the listener function. Or if you use ES6 syntax, using the arrow syntax for functions works too: () => { //...}

Best practice

There are many ways to get the data you need. You could access this.tags.picker.items from the parent tag, or push the items to the parent tag after picking with this.parent.items = this.items. The problem is, that you don't know when the final item was picked or you already require the parent tag to have a certain structure . We don't want that. Triggers and listeners are one of the cleanest ways to work with data between several tags.

Summary

What's really great about this, is that you could use this combination of tags as a new tag - a new variation if you will. This is what components are meant for: to be used in other components no matter what their size is. You can use something low-level like a button together with a modal, a picker or whatever. The components alone are already powerful enough. A bit of event listening and you can customize your components for almost everything you need. Have you even took a minute to realize how brilliant riot is in quickly creating apps of any size?

What are your thoughts about this? Don't be shy, let me know in the comments!

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 - Reusable Features

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

Next Lesson

Interaction Design with Riot - Making of Rain Animation

We are going to see how together with Riot, we can do awesome animations without bending our minds around the DOM.

See all Lessons