Working with forms

    Prerequisites

    You can open the or download the demo project and run to get started.

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

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

    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.

    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
    • 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 () 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';

    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. }),
    14. w(TextInput, {
    15. key: 'lastNameInput',
    16. label: 'Last Name',
    17. labelHidden: true,
    18. placeholder: 'Surname name',
    19. required: true
    20. })
    21. ]),
    22. w(TextInput, {
    23. label: 'Email address',
    24. type: 'email',
    25. required: true
    26. }),
    27. w(Button, {}, [ 'Save!' ])
    28. ]);
    29. }

    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 , 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.

    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. email: string;
    4. }
    5. export interface WorkerFormProperties {
    6. formData: Partial<WorkerFormData>;
    7. onFormInput: (data: Partial<WorkerFormData>) => void;
    8. onFormSave: () => void;
    9. }

    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 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.

    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 article.

    Import the WorkerFormData interface into App.ts.

    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 ()” 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 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 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 or alternatively download the project.

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