Working with forms

Overview

This tutorial will extend on Responding to events, where we allowed the user to interact with the application by listening for click events. In this tutorial, we will add a form to the Biz-E-Worker page so that a user can add new workers to the application. This will be done by using some of Dojo’s form widgets to allow the feature to be developed more rapidly.

Prerequisites

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

The @dojo/cli command line tool should be installed globally. Refer to the Dojo local installation article for more information.

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

Forms

Create a form.

The first step to allowing the user to create new workers is to create a form. This form will contain the input elements that will accept the new worker’s initial settings.

Add the following to WorkerForm.ts.

  1. import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
  2. import { v } from '@dojo/framework/widget-core/d';
  3. import { ThemedMixin, theme } from '@dojo/framework/widget-core/mixins/Themed';
  4. import * as css from '../styles/workerForm.m.css';
  5. export interface WorkerFormProperties {
  6. }
  7. @theme(css)
  8. export default class WorkerForm extends ThemedMixin(WidgetBase)<WorkerFormProperties> {
  9. private _onSubmit(event: Event) {
  10. event.preventDefault();
  11. }
  12. protected render() {
  13. return v('form', {
  14. classes: this.theme(css.workerForm),
  15. onsubmit: this._onSubmit
  16. });
  17. }
  18. }

Reminder
If you cannot see the application, remember to run dojo build -m dev -w -s to build the application and start the development server.

This widget will render an empty form with a submit handler that prevents the form from being submitted to the server. Before we continue to expand on this starting point though, let’s integrate the form into the application so we can observe the form as we add more features.

Add the following widget CSS rules to workerForm.m.css.

  1. .workerForm {
  2. margin-bottom: 40px;
  3. text-align: center;
  4. }
  5. .workerForm fieldset,
  6. .workerForm label {
  7. display: inline-block;
  8. text-align: left;
  9. }
  10. .workerForm label {
  11. margin-right: 10px;
  12. }
  13. .nameField {
  14. border: 0;
  15. margin: 0;
  16. padding: 0;
  17. }
  18. .nameLabel {
  19. font: 14px/1 sans-serif;
  20. margin: 5px 0;
  21. }
  22. .workerButton {
  23. padding: 5px 20px;
  24. }

Now, add the WorkerForm to the App class.

Import the WorkerForm class and the WorkerFormData interface and update the render method to draw the WorkerForm. It should be included after the Banner and before the WorkerContainer so the render method should look like this:

  1. protected render() {
  2. return v('div', [
  3. w(Banner, {}),
  4. w(WorkerForm, {
  5. }),
  6. w(WorkerContainer, {
  7. workerData: this._workerData
  8. })
  9. ]);
  10. }

Now, open the application in a browser and inspect it via the browser’s developer tools. Notice that the empty form element is being rendered onto the page as expected.

Next, we’ll add the visual elements of the form.

Form widgets

Populate the form.

Our form will contain fields allowing the user to create a new worker:

  • A first name field for the worker
  • A last name field for the worker
  • An e-mail address field
  • A save button that will use the form’s data to create a new worker
    We could create these fields and buttons using the v function to create simple virtual DOM elements. If we did this, however, we would have to handle details such as theming, internationalization (i18n) and accessibility (a11y) ourselves. Instead, we are going to leverage some of Dojo’s built-in widgets that have been designed with these considerations in mind.

Add w to the imports from @dojo/framework/widget-core/d and add imports for the Button and TextInput classes to WorkerForm.ts.

  1. import { v, w } from '@dojo/framework/widget-core/d';
  2. import Button from '@dojo/widgets/button';
  3. import TextInput from '@dojo/widgets/text-input';

These imports are for built-in Dojo Widgets. You can explore other widgets in the Dojo Widget Showcase.

The Button class will be used to provide the form’s submit button and the TextInput class will provide the data entry fields for the worker data.

Replace your render() method with the definition below. The code below adds the necessary visual elements to the form

  1. protected render() {
  2. return v('form', {
  3. classes: this.theme(css.workerForm),
  4. onsubmit: this._onSubmit
  5. }, [
  6. v('fieldset', { classes: this.theme(css.nameField) }, [
  7. v('legend', { classes: this.theme(css.nameLabel) }, [ 'Name' ]),
  8. w(TextInput, {
  9. key: 'firstNameInput',
  10. label: 'First Name',
  11. labelHidden: true,
  12. placeholder: 'Given name',
  13. required: true
  14. }),
  15. w(TextInput, {
  16. key: 'lastNameInput',
  17. label: 'Last Name',
  18. labelHidden: true,
  19. placeholder: 'Surname name',
  20. required: true
  21. })
  22. ]),
  23. w(TextInput, {
  24. label: 'Email address',
  25. type: 'email',
  26. required: true
  27. }),
  28. w(Button, {}, [ 'Save!' ])
  29. ]);
  30. }

At this point, the user interface for the form is available, but it does not do anything since we have not specified any event handlers. In the last tutorial, we learned how to add event handlers to custom widgets by assigning a method to an event. When using pre-built widgets, however, we pass the handlers as properties. For example, we are going to need a way to handle each text field’s input event. To do that, we provide the desired handler function as the onInput property that is passed to the widget.

Update the render method once again.

  1. protected render() {
  2. const {
  3. formData: { firstName, lastName, email }
  4. } = this.properties;
  5. return v('form', {
  6. classes: this.theme(css.workerForm),
  7. onsubmit: this._onSubmit
  8. }, [
  9. v('fieldset', { classes: this.theme(css.nameField) }, [
  10. v('legend', { classes: this.theme(css.nameLabel) }, [ 'Name' ]),
  11. w(TextInput, {
  12. key: 'firstNameInput',
  13. label: 'First Name',
  14. labelHidden: true,
  15. placeholder: 'First name',
  16. value: firstName,
  17. required: true,
  18. onInput: this.onFirstNameInput
  19. }),
  20. w(TextInput, {
  21. key: 'lastNameInput',
  22. label: 'Last Name',
  23. labelHidden: true,
  24. placeholder: 'Last name',
  25. value: lastName,
  26. required: true,
  27. onInput: this.onLastNameInput
  28. })
  29. ]),
  30. w(TextInput, {
  31. label: 'Email address',
  32. type: 'email',
  33. value: email,
  34. required: true,
  35. onInput: this.onEmailInput
  36. }),
  37. w(Button, { }, [ 'Save' ])
  38. ]);
  39. }

This form of the render method now does everything that we need: it creates the user interface and registers event handlers that will update the application as the user enters information. However, we need to add a few more methods to the WorkerForm to define the event handlers.

Add these methods:

  1. protected onFirstNameInput(firstName: string) {
  2. this.properties.onFormInput({ firstName });
  3. }
  4. protected onLastNameInput(lastName: string) {
  5. this.properties.onFormInput({ lastName });
  6. }
  7. protected onEmailInput(email: string) {
  8. this.properties.onFormInput({ email });
  9. }

The render method starts by decomposing the properties into local constants. We still need to define those properties.

Update the WorkerFormProperties interface to include them, and add a WorkerFormData interface.

  1. export interface WorkerFormData {
  2. firstName: string;
  3. lastName: string;
  4. email: string;
  5. }
  6. export interface WorkerFormProperties {
  7. formData: Partial<WorkerFormData>;
  8. onFormInput: (data: Partial<WorkerFormData>) => void;
  9. onFormSave: () => void;
  10. }

Most of these properties should be familiar by now, but notice the type signature for the formData property and the argument of the onFormInput property. They’re both objects of type Partial<WorkerFormData>. The Partial type will convert all of the properties of the provided type (WorkerFormData in this case) to be optional. This will inform the consumer that it is not guaranteed to receive all of the WorkerFormData properties every time - it should be prepared to receive only part of the data and process only those values that it receives.

There are two types of properties that we are using in this form. The firstName, lastName and email properties are grouped together in the WorkerFormData interface and are going to set the values that are displayed in the form fields. The onFormInput and onFormSave properties expose the events that the WorkerForm widget can emit. To see how these different property types are used, let’s examine the properties that are being passed into the first TextInput widget:

  1. w(TextInput, {
  2. key: 'firstNameInput',
  3. label: 'First Name',
  4. labelHidden: true,
  5. placeholder: 'First name',
  6. value: firstName,
  7. required: true,
  8. onInput: this.onFirstNameInput
  9. }),

The first thing that we see is a key property. As mentioned before, a key is necessary whenever more than one of the same type of widget or virtual DOM element will be rendered by a widget. The label, placeholder, and required fields map to their expected properties.

The value property renders the value of the field that is passed into the widget via its properties. Notice that there is no code that manipulates this value within the widget. As parts of a reactive framework, Dojo widgets do not normally update their own state. Rather, they inform their parent that a change has occurred via events or some other mechanism. The parent will then pass updated properties back into the widget after all of the changes have been processed. This allows Dojo applications to centralize data and keep the entire application synchronized.

The final property assigns the onFirstNameInput method to the onInput property. The onFirstNameInput method, in turn, calls the onFormInput property, informing the WorkerForm‘s parent that a change has occurred. This is another common pattern within Dojo applications - the WorkerForm does not expose any of the components that it is using to build the form. Rather, the WorkerForm manages its children internally and, if necessary, calls its event properties to inform its parent of any changes. This decouples the consumers of the WorkerForm widget and frees them from having to understand the internal structure of the widget. Additionally, it allows the WorkerForm to change its implementation without affecting its parent as long as it continues to fulfill the WorkerFormProperties interface.

The last change that needs to be made in the WorkerForm is to update the _onSubmit method to delegate to the onFormSave property when it is called.

Replace the _onSubmit method with.

  1. private _onSubmit(event: Event) {
  2. event.preventDefault();
  3. this.properties.onFormSave();
  4. }

The form is now ready to be integrated into the application. We will do that in the next step.

Using forms

Integrate the form into the application.

Now that the WorkerForm widget is complete, we will update the App class to use it. First, we need to address how to store the user-completed form data. Recall that the WorkerForm will accept an onFormInput property that will allow the App class to be informed when a field value changes. However, the App class does not currently have a place to store those changes. We will add a private property to the App to store this state, and a method to update the state and re-render the parts of the application that have changed. As the application grows and needs to store more data, using private properties on a widget class can become difficult to maintain. Dojo uses containers and injectors to help manage the complexities of dealing with state in a large application. For more information, refer to the Containers and Injecting State article.

Import the WorkerFormData interface into App.ts.

  1. import WorkerForm, { WorkerFormData } from './WorkerForm';

Add _newWorker as a private property.

  1. private _newWorker: Partial<WorkerFormData> = {};

Notice that _newWorker is a Partial<WorkerFormData>, since it may include only some, or none, of the WorkerFormData interface properties.

Update the render method to populate the WorkerForm's properties.

  1. protected render() {
  2. return v('div', [
  3. w(Banner, {}),
  4. w(WorkerForm, {
  5. formData: this._newWorker,
  6. onFormInput: this._onFormInput,
  7. onFormSave: this._addWorker
  8. }),
  9. w(WorkerContainer, {
  10. workerData: this._workerData
  11. })
  12. ]);
  13. }

The onFormInput handler is calling the App‘s _onFormInput method.

Add the _onFormInput method.

  1. private _onFormInput(data: Partial<WorkerFormData>) {
  2. this._newWorker = {
  3. ...this._newWorker,
  4. ...data
  5. };
  6. this.invalidate();
  7. }

The _onFormInput method updates the _newWorker object with the latest form data and then invalidates the app so that the form field will be re-rendered with the new data.

The onFormSave handler calls the _addWorker method.

Add the _addWorker method to the App class.

  1. private _addWorker() {
  2. this._workerData = this._workerData.concat(this._newWorker);
  3. this._newWorker = {};
  4. this.invalidate();
  5. }

The _addWorker method sets _workerData to a new array that includes the _newWorker object (which is the current WorkerFormData), sets _newWorker to a new empty object, and then invalidates the App widget. The reason that _workerData is not updated in place is because Dojo decides whether a new render is needed by comparing the previous value of a property to the current value. If we are modifying the existing value then any comparison performed would report that the previous and current values are identical.

With the WidgetForm in place and the App configured to handle it, let’s try it. For now let’s test the happy path by providing the expected values to the form. Provide values for the fields, for example: “Suzette McNamara ([email protected])” and click the Save button. As expected, the form is cleared and a new Worker widget is added to the page. Clicking on the new Worker widget shows the detailed information of the card where we find that the first name, last name, and email values have been properly rendered.

Summary

In this tutorial, we learned how to create complex widgets by composing simpler widgets together. We also got a first-hand look at how Dojo’s reactive programming style allows an application’s state to be centralized, simplifying data validation and synchronization tasks. Finally, we saw a couple of the widgets that come in Dojo’s widgets package and learned how they address many common use cases while providing support for theming, internationalization, and accessibility.

Dojo widgets are provided in the @dojo/widgets GitHub repository. Common built-in widgets exist, such as buttons, accordions, form inputs, etc. You can view these widgets in the Widget Showcase.

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

In Deploying to production, we will wrap up this series by learning how to take a completed Dojo application and prepare it for deployment to production.