As a part of our Free Dojo Support initiative, we received the following question from Ron about the best way to build a complex form:

The Question

I’d like to see a nice clean example of how to best implement a form inside a popup dialog (either dijit.Dialog or dijit.TooltipDialog), but with the complication that the form has various types of behavior such as the disabled status, visibility, and/or required status of fields changing based on changes to other fields. I have typically done this by subclassing dijit.form.Form and using a template to make use of attach points and events, but have not yet figured out the best way to tie that in with validating and handling the dialog submit through JavaScript (rather than a regular form submit). To add yet another wrinkle, can localization be added as well so that strings are not hard-coded into the template?

Great question, Ron! There’s no One True Way to do exactly what you’ve described, but here are some recommendations that should give you a maintainable form with all of the functionality you’re looking for.

First, it’s useful to know that while dijit.Dialog (and dijit.TooltipDialog) actually include dijit.form._FormMixin, which means the Dialog itself can be used as a form, using this feature has some drawbacks. The biggest of these drawbacks is that, without modifying the Dialog’s _onSubmit function, it will be closed immediately when the form is submitted. This could make the user experience less pleasant if errors are generated by the server—or if you wanted to display more information in the dialog after the form was submitted (such as a “Please wait” message). Using Dialog in this way also means that you wouldn’t be able to easily reuse a single Dialog instance across your entire application, which could lead to a glut of orphaned Dialogs if you’re not careful about destroying them after they’ve been hidden. For these reasons, we generally recommend placing a form directly inside a Dialog instead of using the Dialog itself as a form.

For dynamic forms like the ones you’ve described, dojox.form.manager’s mixins can be extremely useful. There’s a great tutorial already available for this component, but we’ll go over how to use it briefly here as well, since we’ll be using it a bit differently in this case.

Finally, internationalization can be accomplished using dijit._Templated (dijit._TemplatedMixin in 1.7 and later) by attaching the i18n object bundle to the widget and referencing it in your template’s substitution strings.

Let’s go over each of these parts in detail. Since you didn’t provide specific information on exactly what you are trying to accomplish, we’ll use an example of a simple sign-up form:

<form dojoType="dijit.form.Form">
    <div dojoAttachPoint="formNode">
        <div>Username: <input
            dojoType="dijit.form.ValidationTextBox" name="username"
            required regExp="^[A-Za-z0-9]{4,}$"
            promptMessage="Username must be letters and numbers
                only, and at least 4 characters long"></div>

        <div dojoType="dojox.form.PasswordValidator"
            name="password">
            <div>Password: <input type="password"
                pwType="new"></div>
            <div>Verify password: <input type="password"
                pwType="verify"></div>
        </div>

        <div><label><input dojoType="dijit.form.CheckBox"
            name="agree"> I agree to the T&amp;Cs</label></div>

        <fieldset>
            <legend>Newsletter?</legend>

            <label class="i"><input
                dojoType="dijit.form.RadioButton" name="newsletter"
                value="1"> Yes</label>
            <label class="i"><input
                dojoType="dijit.form.RadioButton" name="newsletter"
                value="0" checked> No</label>

            <div dojoAttachPoint="newsletterFrequency"
                style="display:none">
                Frequency:
                <select dojoType="dijit.form.Select"
                    name="frequency">
                    <option value="daily">Daily</option>
                    <option value="weekly">Weekly</option>
                    <option value="monthly">Monthly</option>
                </select>
            </div>
        </fieldset>

        <input dojoType="dijit.form.Button" name="submitForm"
            type="submit" value="Sign up" label="Sign up">
    </div>
    <div dojoAttachPoint="spinnerNode" style="display:none">
        Please wait…
    </div>
    <div dojoAttachPoint="successNode" style="display:none">
        It worked! Yay!
    </div>
</form>

The first thing we need to do to this form is turn it into a templated widget. We’ll be subclassing dijit.form.Form for this, so we need to make sure that our container <form> markup matches the default dijit.form.Form template. That means the <form> tag changes to this:

<form dojoAttachPoint="containerNode"
    dojoAttachEvent="onreset:_onReset,onsubmit:_onSubmit"
    ${!nameAttrSetting}>

Our subclass’ skeleton looks like this:

dojo.declare("my.SignupForm", [dijit.form.Form], {
    templateString: dojo.cache("my", "templates/SignupForm.html"),

    postCreate: function(){
        // TODO: Set up state watch handler for the form
        this.inherited(arguments);
    },

    startup: function(){
        // TODO: Add extra stuff for a dialog, if one exists
        this.inherited(arguments);
    },

    onSubmit: function(evt) {
        evt.preventDefault();

        submitForm(this.get('value')).then(function(data){
            // TODO: success
        }, function(err){
            // TODO: error
        });

        // TODO: update form display
    }
};

We now need to add the dojox.form.manager mixins to our form to make it easy to implement the dynamic features. Here, we’ll be using _Mixin, _NodeMixin, _EnableMixin, _DisplayMixin, and _ValueMixin. Once we’ve added these, our dependency list looks like this:

dojo.declare("my.SignupForm", [dijit.form.Form,
  dojox.form.manager._Mixin,
  dojox.form.manager._NodeMixin,
  dojox.form.manager._EnableMixin,
  dojox.form.manager._ValueMixin,
  dojox.form.manager._DisplayMixin], {
    …
});

Now that we’ve got all of our dojox.form.manager dependencies ready, we need to update our markup to add some observers using the observer attribute. These observers are names of class methods that will execute any time the value of a field has changed. We’ll be using these to enable/disable the submit button and to show/hide the newsletter frequency selector. Once we’ve added these, our markup looks like this:

…

<div><label><input dojoType="dijit.form.CheckBox"
    name="agree" observer="checkValid">
    I agree to the T&amp;Cs</label></div>

    <fieldset>
        <legend>Newsletter?</legend>

        <label class="i"><input dojoType="dijit.form.RadioButton"
            name="newsletter" value="1"
            observer="checkNewsletter"> Yes</label>
        <label class="i"><input dojoType="dijit.form.RadioButton"
            name="newsletter" value="0" checked
            observer="checkNewsletter"> No</label>

        …

Next, we need to actually write the observer methods. We’ll be using some of the functions that were mixed in by _EnableMixin and _ValueMixin to enable the submit button when the form is valid, and functions from _DisplayMixin to show the newsletter frequency selector.

dojo.declare("my.SignupForm", […], {

    checkValid: function(){
        this.enable({ submitForm: this.isValid() &&
            this.elementValue("agree") });
    },

    checkNewsletter: function(value){
        this.show({ newsletterFrequency: value === "1" });
        this.dialog && this.dialog.layout();
    },

    …
});

We also want to call checkValid any time the valid state of the form changes; for this, we’ll use a little dojo.Stateful magic:

…
postCreate: function(){
    this.watch("state", this.checkValid);
    this.inherited(arguments);
},
…

Finally, we need to make sure that when the form is submitted, we change which elements are visible. In this case, we’re using the show convenience function from _DisplayMixin:

…
onSubmit: function(evt) {
    evt.preventDefault();

    submitForm(this.get("value")).then(dojo.hitch(this, function(data) {
        this.show({
            spinnerNode: false,
            successNode: true
        });
    }), dojo.hitch(this, function(err) {
        this.show({
            spinnerNode: false,
            formNode: true
        });
    }));

    this.show({
        spinnerNode: true,
        formNode: false
    });
},
…

At this point, when we instantiate our custom widget, we’ve got a dynamic form that responds appropriately to user interaction and that won’t allow users to submit until they have filled out the form correctly.

Making it international

Now that the form is working, we need to internationalize it. Fortunately, this is extremely easy, since we’re already using a templated widget. First, we need to require and fetch the NLS bundle:

dojo.requireLocalization("my", "SignupForm");

Next, we attach the bundle to the prototype of our class:

dojo.declare("my.SignupForm", […], {
    i18n: dojo.i18n.getLocalization("my", "SignupForm"),

    …
});

Finally, we update the template to use placeholders for our labels that will pull values from the NLS bundle:

<form dojoAttachPoint="containerNode"
    dojoAttachEvent="onreset:_onReset,onsubmit:_onSubmit"
    ${!nameAttrSetting}>
    <div dojoAttachPoint="formNode">
        <div>${i18n.username}: <input
            dojoType="dijit.form.ValidationTextBox" name="username"
            required regExp="^[A-Za-z0-9]{4,}$"
            promptMessage="${i18n.usernameRules}"></div>

        <div dojoType="dojox.form.PasswordValidator"
            name="password">
            <div>${i18n.password}: <input type="password"
                pwType="new"></div>
            <div>${i18n.verifyPassword}: <input type="password"
                pwType="verify"></div>
        </div>

        <div><label><input dojoType="dijit.form.CheckBox"
            name="agree" observer="checkValid">
            ${i18n.agreeTerms}</label></div>

        <fieldset>
            <legend>${i18n.newsletter}</legend>

            <label class="i"><input
                dojoType="dijit.form.RadioButton"
                name="newsletter" value="1"
                observer="checkNewsletter"> ${i18n.yes}</label>
            <label class="i"><input
                dojoType="dijit.form.RadioButton"
                name="newsletter" value="0" checked
                observer="checkNewsletter"> ${i18n.no}</label>

            <div dojoAttachPoint="newsletterFrequency"
                style="display:none">
                ${i18n.frequency}:
                <select dojoType="dijit.form.Select"
                    name="frequency">
                    <option value="daily">${i18n.daily}</option>
                    <option value="weekly">${i18n.weekly}</option>
                    <option value="monthly">${i18n.monthly}</option>
                </select>
            </div>
        </fieldset>

        <input dojoType="dijit.form.Button" name="submitForm"
            type="submit" label="${i18n.signUp}" disabled>
    </div>
    <div dojoAttachPoint="spinnerNode"
        style="display:none">${i18n.pleaseWait}</div>
    <div dojoAttachPoint="successNode"
        style="display:none">${i18n.success}</div>
</form>

The form is now internationalized, and our markup is complete.

Putting it in a dialog

The final part of this puzzle is putting the form in a Dialog. This isn’t very challenging: we simply create a dialog and set the content to the value of our widget. For this example, we’ll just create a basic dijit.form.Button to open the dialog.

Markup:

<input dojoType="dijit.form.Button" type="button"
    label="Sign up now" onclick="app.showSignupForm()">

Code:

dojo.ready(function(){
    app = {
        dialog: new dijit.Dialog().placeAt(document.body),
        showSignupForm: function() {
            this.dialog.set({
                title: i18n.signUp,
                content: new my.SignupForm()
            });
            this.dialog.show();
        }
    };
});

Now that the form is in a dialog, we’re pretty much done! One last thing you might want to do, though, is make it so that the form can display a button that causes the parent dialog to close. We can do this by passing the dialog as an argument to the form so that it knows which hide function to call:

dojo.ready(function(){
    app = {
        dialog: new dijit.Dialog().placeAt(document.body),
        showSignupForm: function() {
            this.dialog.set({
                title: i18n.signUp,
                content: new my.SignupForm({
                    dialog: this.dialog
                })
            });
            this.dialog.show();
        }
    };
});

Since we want to make this form as versatile as possible, the hide button only appears when a dialog property is actually set. The actual code that does this is in our startup function:

…
startup: function(){
    if(this.dialog){
        new dijit.form.Button({
            type: "button",
            label: this.i18n.ok,
            onClick: dojo.hitch(this, function(){
                this.dialog.hide();
            })
        }).placeAt(this.successNode);
    }
    this.inherited(arguments);
}, …

We also need to take care to call this.dialog.layout() any time we change the visible content of the Dialog in order to ensure it stays centred on the page:

…
onSubmit: function(evt) {
    evt.preventDefault();

    submitForm(this.get("value")).then(
    dojo.hitch(this, function(data) {
        this.show({
            spinnerNode: false,
            successNode: true
        });
        this.dialog && this.dialog.layout();
    }), dojo.hitch(this, function(err) {
        this.show({
            spinnerNode: false,
            formNode: true
        });
        this.dialog && this.dialog.layout();
    }));

    this.show({
        spinnerNode: true,
        formNode: false
    });
    this.dialog && this.dialog.layout();
}
…

Hopefully this gives you everything you were hoping for in a package that is extremely maintainable and versatile. Since we separated the form from the Dialog, we could easily put it somewhere else—a TabContainer, a ContentPane, or directly into the page—and it would still work just as well.

Here’s a demo of everything all together:

Thanks for writing, and happy coding!

SitePen Support

Have a Dojo question you’re just dying to ask? Get your free Dojo support now! Or sign-up for the Best JavaScript and Dojo Support available to get all of your questions answered all the time!