Responding to events

Overview

In this tutorial, you will learn how to update an application in response to user-generated events, such as clicking a button.

We will start with an application that renders widgets containing the portrait and names of several employees for the hypothetical company, “Biz-E Corp”. In this tutorial, you will learn how to add event listeners to these widgets so that they show additional information about each worker including a list of their active tasks.

Prerequisites

You can open the tutorial on codesandbox.io or download the demo project and run npm install to get started.

You should install the @dojo/cli command globally. Refer to the Dojo local installation article for more information.

You also need to be familiar with TypeScript as Dojo uses it extensively.

Event listeners

Create an event listener.

In Creating widgets, we created an application that contains several widgets that render worker information. In this tutorial, you will add event listeners to these widgets to show additional information about an employee when clicking on the widget.

The first step is to add the listener itself. In Dojo, event listeners get assigned like any other property passed to the rendering function, v. Look at the Worker widget that is in src/widgets. Currently, the top level DNode has one property assigned: classes.

Update the object containing that property as follows.

  1. {
  2. classes: this.theme(css.worker),
  3. onclick: this.flip
  4. }

The onclick property registers a function to call when clicking on the node to which it is attached. In this case, the registered function is a method called flip.

Add a basic implementation for that method within the Worker class.

  1. flip(): void {
  2. console.log('the flip method has been fired!');
  3. }

Now, run the app (using dojo build -m dev -w -s) and navigate to localhost:9999. Once there,

Open the console window and click on any of the worker widgets to confirm that the flip method gets called as expected.

Automatic Binding of Handlers
The context for event handlers and function properties are automatically bound to the this context of the widget that defined the v() or w() call. If you are just passing on a property that has already been bound, then this will not be bound again.

Using event handlers

Add a second visual state.

Now that we have an event handler, it is time to extend the render method to show detailed information about a worker in addition to the current overview. For the sake of this tutorial, we will call the current view the front and the detailed view the back.

We could add the additional rendering logic in the current render method, but that method could become difficult to maintain as it would have to contain all of the rendering code for both the front and back of the card. Instead, we will generate the two views using two private methods and then call them from the render method.

Create a new private method called _renderFront and move the existing render code inside it.

  1. private _renderFront() {
  2. const {
  3. firstName = 'firstName',
  4. lastName = 'lastName'
  5. } = this.properties;
  6. return v('div', {
  7. classes: this.theme(css.workerFront),
  8. onclick: this.flip
  9. }, [
  10. v('img', {
  11. classes: this.theme(css.image),
  12. src: 'https://dojo.io/tutorials/resources/worker.svg' }),
  13. v('div', [
  14. v('strong', [ `${lastName}, ${firstName}` ])
  15. ])
  16. ]
  17. );
  18. }

Create another private method called _renderBack to render the back view.

  1. private _renderBack() {
  2. const {
  3. firstName = 'firstName',
  4. lastName = 'lastName',
  5. email = 'unavailable',
  6. timePerTask = 0,
  7. tasks = []
  8. } = this.properties;
  9. return v('div', {
  10. classes: this.theme(css.workerBack),
  11. onclick: this.flip
  12. }, [
  13. v('img', {
  14. classes: this.theme(css.imageSmall),
  15. src: 'https://dojo.io/tutorials/resources/worker.svg'
  16. }),
  17. v('div', {
  18. classes: this.theme(css.generalInfo)
  19. }, [
  20. v('div', {
  21. classes : this.theme(css.label)
  22. }, ['Name']),
  23. v('div', [`${lastName}, ${firstName}`]),
  24. v('div', {
  25. classes: this.theme(css.label)
  26. }, ['Email']),
  27. v('div', [`${email}`]),
  28. v('div', {
  29. classes: this.theme(css.label)
  30. }, ['Avg. Time per Task']),
  31. v('div', [`${timePerTask}`])
  32. ]),
  33. v('div', [
  34. v('strong', ['Current Tasks']),
  35. v('div', tasks.map(task => {
  36. return v('div', { classes: this.theme(css.task) }, [ task ]);
  37. }))
  38. ])
  39. ]
  40. );
  41. }

This code is not doing anything new. We are composing together multiple virtual nodes to generate the elements required to render the detailed view. This method does, however, refer to some properties and CSS selectors that do not exist yet.

We need to add three new properties to the WorkerProperties interface. These properties are the email address of the worker, the average number of hours they take to complete a task, and the active tasks for the worker.

Update the WorkerProperties interface.

  1. export interface WorkerProperties {
  2. firstName?: string;
  3. lastName?: string;
  4. email?: string;
  5. timePerTask?: number;
  6. tasks?: string[];
  7. }

Now, we need to add the CSS selectors that will provide the rules for rendering this view’s elements.

Open worker.m.css and replace the existing classes with the following.

  1. .generalInfo {
  2. box-sizing: border-box;
  3. display: inline-block;
  4. margin-bottom: 20px;
  5. padding-left: 10px;
  6. vertical-align: middle;
  7. width: 60%;
  8. }
  9. .image {
  10. margin: 0 auto 20px;
  11. max-width: 250px;
  12. width: 100%;
  13. }
  14. .imageSmall {
  15. margin-bottom: 20px;
  16. vertical-align: middle;
  17. width: 40%;
  18. }
  19. .label {
  20. font-weight: bold;
  21. }
  22. .task {
  23. border: solid 1px #333;
  24. border-radius: 0.3em;
  25. margin-top: 3px;
  26. padding: 3px;
  27. text-align: left;
  28. }
  29. .worker {
  30. flex: 1 1 calc(33% - 20px);
  31. margin: 0 10px 40px;
  32. max-width: 350px;
  33. min-width: 250px;
  34. position: relative;
  35. /* flip transform styles */
  36. perspective: 1000px;
  37. transform-style: preserve-3d;
  38. }
  39. .workerFront, .workerBack {
  40. border: 1px solid #333;
  41. border-radius: 4px;
  42. box-sizing: border-box;
  43. padding: 30px;
  44. /* flip transform styles */
  45. backface-visibility: hidden;
  46. transition: all 0.6s;
  47. transform-style: preserve-3d;
  48. }
  49. .workerBack {
  50. font-size: 0.75em;
  51. height: 100%;
  52. left: 0;
  53. position: absolute;
  54. top: 0;
  55. transform: rotateY(-180deg);
  56. width: 100%;
  57. }
  58. .workerFront {
  59. text-align: center;
  60. transform: rotateY(0deg);
  61. z-index: 2;
  62. }
  63. .reverse .workerFront {
  64. transform: rotateY(180deg);
  65. }
  66. .reverse .workerBack {
  67. transform: rotateY(0deg);
  68. }

We also need to update the CSS selector for the front view by changing the selector from css.worker to css.workerFront.

Finally, we need to update the render method to choose between the two rendering methods.

Add a private field to the class.

  1. private _isFlipped = false;

In general, the use of private state is discouraged. Dojo encourages the use of a form of the inversion of control pattern, where the properties passed to the component by its parent control the behavior of the component. This pattern helps make components more modular and reusable since the parent component is in complete control of the child component’s behavior and does not need to make any assumptions about its internal state. For widgets that have state, the use of a field to store this kind of data is standard practice in Dojo. Properties are used to allow other components to view and modify a widget’s published state, and private fields are used to enable widgets to encapsulate state information that should not be exposed publicly.

Use that field's value to determine which side to show.

  1. protected render() {
  2. return v('div', {
  3. classes: this.theme([ css.worker, this._isFlipped ? css.reverse : null ])
  4. }, [
  5. this._renderFront(),
  6. this._renderBack()
  7. ]);
  8. }

Confirm that everything is working by viewing the application in a browser. All three cards should be showing their front faces. Now change the value of the _isFlipped field to true and, after the application re-compiles, all three widgets should be showing their back faces.

To re-render our widget, we need to update the flip method to toggle the _isFlipped field and invalidate the widget

Replace the flip method with this one.

  1. flip(): void {
  2. this._isFlipped = !this._isFlipped;
  3. this.invalidate();
  4. }

Now, the widget may flip between its front and back sides by clicking on it.

Final steps

Provide additional properties.

Currently, several of the properties are missing for the widgets. As an exercise, try to update the first widget to contain the following properties:

  1. {
  2. firstName: 'Tim',
  3. lastName: 'Jones',
  4. email: '[email protected]',
  5. tasks: [
  6. '6267 - Untangle paperclips',
  7. '4384 - Shred documents',
  8. '9663 - Digitize 1985 archive'
  9. ]
  10. }

This change will pass the specified properties to the first worker. The widget’s parentis responsible for passing properties to the widget. In this application, Workerwidgets are receiving data from the App class via the WorkerContainer.

Summary

In this tutorial, we learned how to attach event listeners to respond to widget-generated events. Event handlers get assigned to virtual nodes like any other Dojo property.

If you would like, you can open the completed demo application on codesandbox.io or alternatively download the project.

In Form widgets, we will work with more complicated interactions in Dojo by extending the demo application, allowing new Workers to be created using forms.