Difference between revisions of "AppSuite:Writing a wizard"

Line 5: Line 5:
 
__TOC__
 
__TOC__
  
  /**
+
<syntaxhighlight lang="javascript">
    * All content on this website (including text, images, source
+
/**
    * code and any other original works), unless otherwise noted,
+
* All content on this website (including text, images, source
    * is licensed under a Creative Commons License.
+
* code and any other original works), unless otherwise noted,
    *
+
* is licensed under a Creative Commons License.
    * http://creativecommons.org/licenses/by-nc-sa/2.5/
+
*
    *
+
* http://creativecommons.org/licenses/by-nc-sa/2.5/
    * Copyright (C) Open-Xchange Inc., 2006-2011
+
*
    * Mail: info@open-xchange.com
+
* Copyright (C) Open-Xchange Inc., 2006-2011
    *
+
* Mail: info@open-xchange.com
    * @author Francisco Laguna <francisco.laguna@open-xchange.com>
+
*
    */
+
* @author Francisco Laguna <francisco.laguna@open-xchange.com>
 
+
*/
    /**
+
 
    * In this example we will build a simple welcome wizard that tries to complete the users information.
+
/**
    */
+
* In this example we will build a simple welcome wizard, that tries to complete the users information.
    define('io.ox/dev/wizard/welcomeWizard', ['io.ox/core/extensions', 'io.ox/core/wizard/registry'], function (ext, wizards) {
+
*/
        'use strict';
+
define('io.ox/dev/wizard/welcomeWizard', ['io.ox/core/extensions', 'io.ox/core/wizard/registry'], function (ext, wizards) {
        // Grab the extension point for the wizard
+
    'use strict';
        // Every page in the wizard will be an extension to this extension point
+
    // Grab the extension point for the wizard
        var point = ext.point("io.ox/dev/wizard/welcomeWizard");
+
    // Every page in the wizard will be an extension to this extension point
       
+
    var point = ext.point("io.ox/dev/wizard/welcomeWizard");
        // We will build a few pages here to showcase how you can use the framework.
+
   
        // Firstly, the simplest case, just a static page
+
    // We will build a few pages here to showcase how you can use the framework.
        // It's a nice trick to start off with a static page, so the subsequent page can already start loading data
+
    // Firstly, the simplest case, just a static page
        // and initialize itself without the user having to wait around for that. Distract them with a nice welcome page!
+
    // It's a nice trick to start off with a static page, so the subsequent page  
        point.extend({
+
    // can already start loading data
            id: 'welcomeMessage',
+
    // and initialize itself without the user having to wait around for that.  
            index: 100,
+
    // Distract them with a nice welcome page!
            title: "Welcome to App Suite", // be sure to internationalize this
+
    point.extend({
            draw: function (baton) {
+
        id: 'welcomeMessage',
                // A regular #draw method, that you may know and love from other extension points
+
        index: 100,
                // Just append to 'this' to draw what you need. One caveat though: Make sure to unlock the 'next' button
+
        title: "Welcome to App Suite", // be sure to internationalize this
                // so this step can be finished
+
        draw: function (baton) {
 
+
            // A regular #draw method, that you may know and love from other extension points
                // Some text. Note that you want to take some more care here, to make this look
+
            // Just append to 'this' to draw what you need. One caveat though: Make sure to  
                // good and make sense. We'll firmly stay in example land here and not make a fuss about looks
+
            // unlock the 'next' button
                // Make sure you do better than this, also, this needs to be internationalized with a gt() call!
+
            // so this step can be finished
                this.append($("<p>").text("Hi there, stranger! Welcome to OX App Suite, glad you made it. To make sure your experience with us is a pleasent one, let's set up some basics together!"));
+
 
 +
            // Some text. Note that you want to take some more care here, to make this look
 +
            // good and make sense. We'll firmly stay in example land here and not make a  
 +
            // fuss about looks
 +
            // Make sure you do better than this, also, this needs to be internationalized  
 +
            // with a gt() call!
 +
            this.append($("<p>").text("Hi there, stranger! Welcome to OX App Suite, glad you made it. To make sure your experience with us is a pleasant one, let's set up some basics together!"));
 +
 
 +
            // Enable the next (or 'done', if this is the last page) button.
 +
            // You will have to call this once for every page, once every needed entry has been made.
 +
            baton.buttons.enableNext();
 +
        }
 +
    });
 +
 
 +
    // Now let's actually ask for some user input. Let's start with the users gender.
 +
    point.extend({
 +
        id: 'gender',
 +
        index: 200,
 +
        title: "Gender",
 +
        draw: function (baton) {
 +
            // Every method of a page is always called with a baton that is unique to
 +
            // every page instance, so
 +
            // we can set state information in it to our hearts content without
 +
            // bothering everyone else.
 +
            // The baton holds some interesting objects, though. 'wizard' is the instance
 +
            // of the wizard object, 'buttons', like above
 +
            // can be used to enable or disable the next button. The wizard also has a
 +
            // pageData member object that we can use to store
 +
            // data that is available to every subsequent page. Note though, that that
 +
            // tightly couples pages together, so use this with care!
 +
            // We will use this for some fun, though.
 +
            baton.form = {};
  
                // Enable the next (or 'done', if this is the last page) button.
+
             this.append(
                // You will have to call this once for every page, once every needed entry has been made.
+
                $('<p/>').text("Please pick one:"),
                baton.buttons.enableNext();
+
                $('<form>').append(
             }
+
                    $("<fieldset>").append(
        });
+
                        $('<label class="radio">').append(
 
+
                            baton.form.male = $('<input type="radio" name="genderRadio" value="male">'),
        // Now let's actually ask for some user input. Let's start with the users gender.
+
                            $.txt("Gentleman")
        point.extend({
+
                        ),
            id: 'gender',
+
                        $('<label class="radio">').append(
            index: 200,
+
                            baton.form.female = $('<input type="radio" name="genderRadio" value="male">'),
            title: "Gender",
+
                            $.txt("Lady")
            draw: function (baton) {
 
                // Every method of a page is always called with a baton that is unique to every page instance, so
 
                // we can set state information in it to our hearts content without bothering everyone else.
 
                // The baton holds some interesting objects, though. 'wizard' is the instance of the wizard object, 'buttons', like above
 
                // can be used to enable or disable the next button. The wizard also has a pageData member object that we can use to store
 
                // data that is available to every subsequent page. Note though, that that tightly couples pages together, so use this with care!
 
                // We will use this for some fun, though.
 
                baton.form = {};
 
 
 
                this.append(
 
                    $('<p/>').text("Please pick one:"),
 
                    $('<form>').append(
 
                        $("<fieldset>").append(
 
                            $('<label class="radio">').append(
 
                                baton.form.male = $('<input type="radio" name="genderRadio" value="male">'),
 
                                $.txt("Gentleman")
 
                            ),
 
                            $('<label class="radio">').append(
 
                                baton.form.female = $('<input type="radio" name="genderRadio" value="male">'),
 
                                $.txt("Lady")
 
                            )
 
 
                         )
 
                         )
 
                     )
 
                     )
                 );
+
                 )
 
+
            );
                // We want to enable the navigation once the user picked his or her gender.
+
 
                // And we'll capture that logic in one method that we call everytime we have
+
            // We want to enable the navigation once the user picked his or her gender.
                // reason to believe the state changed
+
            // And we'll capture that logic in one method that we call everytime we have
                baton.helpers = {
+
            // reason to believe the state changed
                    updateState: function () {
+
            baton.helpers = {
                        if (baton.form.male.attr("checked") === 'checked' || baton.form.female.attr("checked") === 'checked') {
+
                updateState: function () {
                            // One of the two was picked, so enable the next button
+
                    if (baton.form.male.attr("checked") === 'checked' || baton.form.female.attr("checked") === 'checked') {
 +
                        // One of the two was picked, so enable the next button
 +
                        baton.buttons.enableNext();
 +
                    } else {
 +
                        // No choice was made, so disable the button
 +
                        baton.buttons.disableNext();
 +
                    }
 +
                }
 +
            };
 +
            baton.form.male.on('click', baton.helpers.updateState);
 +
            baton.form.female.on('click', baton.helpers.updateState);
 +
               
 +
        },
 +
 
 +
        activate: function (baton) {
 +
            // Whenever the page is entered, the activate method is called.
 +
            // we just have to make sure the button state is correct
 +
            baton.helpers.updateState();
 +
        },
 +
 
 +
        finish: function (baton) {
 +
            // When the page is left, the 'finish' method is called and we can do something
 +
            // with the entered value, in this case we'll remember it in the wizards data section for inter-page stuff
 +
 
 +
            var gender = null;
 +
            if (baton.form.male.attr("checked") === 'checked') {
 +
                gender = 'male';
 +
            } else if (baton.form.female.attr("checked") === 'checked') {
 +
                gender = 'female';
 +
            }
 +
 
 +
            baton.wizard.pageData.gender = gender;
 +
        }
 +
    });
 +
 
 +
    // Anything above a trivial form may benefit from using backbone model and view classes
 +
    point.extend({
 +
        id: 'completeUserInfo',
 +
        index: 300,
 +
        title: "Personal Information",
 +
        load: function (baton) {
 +
            // The load method is an optional method. It is called to load data that you
 +
            // need to set up the page
 +
            // And it is called as soon as the page is the 'next' or 'previous' page of the
 +
            // active page, so you can start loading
 +
            // even before the page shows up. Return a deferred to let the wizard framework
 +
            // know when you're done.
 +
           
 +
 
 +
            // We will fetch the user data for our example.
 +
            var def = $.Deferred();
 +
 
 +
            require(["io.ox/core/api/user", "io.ox/backbone/basicModel", "io.ox/backbone/mini-views"], function (userAPI, Model, mini) {
 +
                // Alright, let's stick the APIs into our baton, we'll need these later
 +
                // This is also a nice little trick for loading APIs in the wizard framework.
 +
                baton.libraries = {
 +
                    userAPI: userAPI,
 +
                    mini: mini
 +
                };
 +
 
 +
                // And let's load the current user
 +
 
 +
                userAPI.getCurrentUser().done(function (user) {
 +
                    // Note, that this is a backbone model
 +
                    // We could turn this into a model by instantiating a BasicModel, otherwise.
 +
                    baton.user = user;
 +
 
 +
                    // We want to enable the next button on this page based on whether a first
 +
                    // an last name is set, so, let's listen for the change events on the user object
 +
                    function updateButtonState() {
 +
                        if (!_.isEmpty(user.get("first_name")) && !_.isEmpty(user.get("last_name"))) {
 
                             baton.buttons.enableNext();
 
                             baton.buttons.enableNext();
 
                         } else {
 
                         } else {
                            // No choice was made, so disable the button
 
 
                             baton.buttons.disableNext();
 
                             baton.buttons.disableNext();
 
                         }
 
                         }
 
                     }
 
                     }
                };
+
                    baton.user.on('change', updateButtonState);
                baton.form.male.on('click', baton.helpers.updateState);
+
 
                 baton.form.female.on('click', baton.helpers.updateState);
+
                    updateButtonState();
 
+
 
             },
+
                    // And we're done
 +
                    def.resolve();
 +
                 }).fail(def.reject);
 +
            });
 +
 
 +
             return def;
 +
        },
 +
 
 +
        draw: function (baton) {
 +
            // Now, for fun, let's try and build a backbone backed form
 +
            // Depending on the complexity of the form, this is a good route to take
 +
            // I would, however, also suggest to scour the appsuite source code for
 +
            // reusable parts, as they will usually be internationalized, localized,
 +
            // responsive to different devices and accessible. Depending on the use of this
 +
            // wizard, you'll have to take care of these aspects yourself.
  
             activate: function (baton) {
+
             // Firstly some fun, though. Why not have this wizard be flirtatious, since it's just
                // Whenever the page is entered, the activate method is called.
+
            // getting to know the user. Personally, I think software would do well to be more
                // we just have to make sure the button state is correct
+
             // flirtatious, but I'm just a lonely developer, so YMMV
                baton.helpers.updateState();
+
            if (baton.wizard.pageData.gender && baton.wizard.pageData.gender === 'male') {
            },
+
                this.append($("<p>").text("So, who are you, handsome?"));
 
+
            } else if (baton.wizard.pageData.gender && baton.wizard.pageData.gender === 'female') {
             finish: function (baton) {
+
                this.append($("<p>").text("So, who are you, beautiful?"));
                // When the page is left, the 'finish' method is called and we can do something
+
            } else {
                // with the entered value, in this case we'll remember it in the wizards data section for inter-page stuff
+
                 this.append($("<p>").text("So, who are you, stranger?"));
 
 
                var gender = null;
 
                if (baton.form.male.attr("checked") === 'checked') {
 
                    gender = 'male';
 
                } else if (baton.form.female.attr("checked") === 'checked') {
 
                    gender = 'female';
 
                }
 
 
 
                 baton.wizard.pageData.gender = gender;
 
 
             }
 
             }
        });
+
 
 
+
            // Now, on to the serious business
         // Anything above a trivial form may benefit from using backbone model and view classes
+
            var mini = baton.libraries.mini;
         point.extend({
+
 
            id: 'completeUserInfo',
+
            this.append(
            index: 300,
+
                $('<form class="form-horizontal" />').append(
            title: "Personal Information",
+
                    $('<div class="control-group" />').append(
            load: function (baton) {
+
                        $('<label class="control-label" for="first_name" />').text("First Name"), // Don't forget i18n in your own wizard!
                 // The load method is an optional method. It is called to load data that you need to set up the page
+
                        $('<div class="controls" />').append(
                 // And it is called as soon as the page is the 'next' or 'previous' page of the active page, so you can start loading
+
                            new mini.InputView({name: 'first_name', model: baton.user}).render().$el
                // even before the page shows up. Return a deferred to let the wizard framework know when you're done.
+
                        )
               
+
                    ),
 
+
                    $('<div class="control-group" />').append(
                // We will fetch the user data for our example.
+
                        $('<label class="control-label" for="last_name" />').text("Last Name"), // Don't forget i18n in your own wizard!
 +
                        $('<div class="controls" />').append(
 +
                            new mini.InputView({name: 'last_name', model: baton.user}).render().$el
 +
                        )
 +
                    )
 +
                )
 +
            );
 +
 
 +
         },
 +
 
 +
        finish: function (baton) {
 +
            // Depending on the capabilities of the model, this could be more complicated
 +
            // you might have to interrogate the model for the #changedAttributes
 +
            // and call an API method. In any case, finish may return a deferred object
 +
            // to denote the state of the save operation
 +
            return baton.user.save();
 +
         }
 +
    });
 +
 
 +
    // If you want to provide your own navigation controls in the page
 +
    // (useful for a simple choice), you can get rid of the default buttons
 +
    // of the dialog, but have to then call baton.wizard.next or baton.wizard.prev
 +
    // or baton.wizard.goToPage(pageNumberOrID) manually.
 +
 
 +
    point.extend({
 +
        id: 'spamMe',
 +
        index: 400,
 +
        title: "Special Offers",
 +
        hideButtons: 'true',
 +
        draw: function (baton) {
 +
            this.append(
 +
                 $('<div />').text("Would you like to be informed of special offers from time to time?"),
 +
                 "<br />",
 +
                $('<div />').append(
 +
                    $('<button class="btn btn-primary" />').text("Yes! Send me information about special offers").on("click", function () {
 +
                        baton.specialOffers = true;
 +
                        baton.buttons.enableNext();
 +
                        baton.wizard.next();
 +
                    })
 +
                ),
 +
                "<br />",
 +
                $('<div />').append(
 +
                    $('<button class="btn" />').text("No, thanks").on("click", function () {
 +
                        baton.specialOffers = false;
 +
                        baton.buttons.enableNext();
 +
                        baton.wizard.next();
 +
                    })
 +
                )
 +
            );
 +
        },
 +
 
 +
        finish: function (baton) {
 +
            // Save baton.specialOffers preference
 +
            console.log(baton.specialOffers);
 +
        }
 +
    });
 +
 
 +
    point.extend({
 +
        id: 'byebye',
 +
        index: 500,
 +
        title: "Thank you!",
 +
        draw: function (baton) {
 +
            this.append($("<p>").text("Thank you for completing our welcome wizard! Be sure to tell us what you like and what we could improve in App Suite!"));
 +
            baton.buttons.enableNext();
 +
        }
 +
    });
 +
 
 +
    // To enable the wizard to run upon startup, you have to use the extension system to add a new
 +
    // stage to the boot process.
 +
    // Use a manifest.json to extend the core/main file:
 +
    // {
 +
    //      namespace: 'io.ox/core/main'
 +
    // }
 +
 
 +
    // Then, in the plugins file, define a new stage that runs the wizard after the curtain has
 +
    // been drawn back:
 +
 
 +
    /*
 +
    define('...', ['io.ox/core/extPatterns/stage'], function (Stage) {
 +
    'use strict';
 +
 
 +
        new Stage('io.ox/core/stages', {
 +
            id: 'welcome-wizard',
 +
            after: 'curtain',
 +
            run: function (baton) {
 
                 var def = $.Deferred();
 
                 var def = $.Deferred();
 
+
                 //TODO: Check a JSLob if the wizard needs to be run, or has been cleared successfully
                 require(["io.ox/core/api/user", "io.ox/backbone/basicModel", "io.ox/backbone/mini-views"], function (userAPI, Model, mini) {
+
                // If it has to be run, require the wizards source file and trigger the wizard
                     // Alright, let's stick the APIs into our baton, we'll need these later
+
                require(["io.ox/dev/wizard/welcomeWizard"], function (w) {
                    // This is also a nice little trick for loading APIs in the wizard framework.
+
                     w.getInstance().start().done(function () {
                    baton.libraries = {
+
                         //TODO: Mark this wizard as passed, so as not to start it again
                        userAPI: userAPI,
+
                         // Resolve the deferred, so the next stage can start
                        mini: mini
 
                    };
 
 
 
                    // And let's load the current user
 
 
 
                    userAPI.getCurrentUser().done(function (user) {
 
                         // Note, that this is a backbone model
 
                        // We could turn this into a model by instantiating a BasicModel, otherwise.
 
                        baton.user = user;
 
 
 
                         // We want to enable the next button on this page based on whether a first an last name is set, so, let's
 
                        // listen for the change events on the user object
 
                        function updateButtonState() {
 
                            if (!_.isEmpty(user.get("first_name")) && !_.isEmpty(user.get("last_name"))) {
 
                                baton.buttons.enableNext();
 
                            } else {
 
                                baton.buttons.disableNext();
 
                            }
 
                        }
 
                        baton.user.on('change', updateButtonState);
 
 
 
                        updateButtonState();
 
 
 
                        // And we're done
 
 
                         def.resolve();
 
                         def.resolve();
 
                     }).fail(def.reject);
 
                     }).fail(def.reject);
 
                 });
 
                 });
 
+
 
 
                 return def;
 
                 return def;
            },
 
 
 
            draw: function (baton) {
 
                // Now, for fun, let's try and build a backbone backed form
 
                // Depending on the complexity of the form, this is a good route to take
 
                // I would, however, also suggest to scour the appsuite source code for
 
                // reusable parts, as they will usually be internationalized, localized,
 
                // responsive to different devices and accessible. Depending on the use of this
 
                // wizard, you'll have to take care of these aspects yourself.
 
 
 
                // Firstly some fun, though. Why not have this wizard be flirtatious, since it's just
 
                // getting to know the user. Personally, I think software would do well to be more flirtatious, but
 
                // I'm just a lonely developer, so YMMV
 
                if (baton.wizard.pageData.gender && baton.wizard.pageData.gender === 'male') {
 
                    this.append($("<p>").text("So, who are you, handsome?"));
 
                } else if (baton.wizard.pageData.gender && baton.wizard.pageData.gender === 'female') {
 
                    this.append($("<p>").text("So, who are you, beautiful?"));
 
                } else {
 
                    this.append($("<p>").text("So, who are you, stranger?"));
 
                }
 
 
 
                // Now, on to the serious business
 
                var mini = baton.libraries.mini;
 
 
 
                this.append(
 
                    $('<form class="form-horizontal" />').append(
 
                        $('<div class="control-group" />').append(
 
                            $('<label class="control-label" for="first_name" />').text("First Name"), // Don't forget i18n in your own wizard!
 
                            $('<div class="controls" />').append(
 
                                new mini.InputView({name: 'first_name', model: baton.user}).render().$el
 
                            )
 
                        ),
 
                        $('<div class="control-group" />').append(
 
                            $('<label class="control-label" for="last_name" />').text("Last Name"), // Don't forget i18n in your own wizard!
 
                            $('<div class="controls" />').append(
 
                                new mini.InputView({name: 'last_name', model: baton.user}).render().$el
 
                            )
 
                        )
 
                    )
 
                );
 
 
 
            },
 
 
 
            finish: function (baton) {
 
                // Depending on the capabilities of the model, this could be more complicated
 
                // you might have to interrogate the model for the #changedAttributes
 
                // and call an API method. In any case, finish may return a deferred object
 
                // to denote the state of the save operation
 
                return baton.user.save();
 
 
             }
 
             }
 
         });
 
         });
 
+
 
        // If you want to provide your own navigation controls in the page
+
       
        // (useful for a simple choice), you can get rid of the default buttons
 
        // of the dialog, but have to then call baton.wizard.next or baton.wizard.prev or baton.wizard.goToPage(pageNumberOrID)
 
        // manually.
 
 
 
        point.extend({
 
            id: 'spamMe',
 
            index: 400,
 
            title: "Special Offers",
 
            hideButtons: 'true',
 
            draw: function (baton) {
 
                this.append(
 
                    $('<div />').text("Would you like to be informed of special offers from time to time?"),
 
                    "<br />",
 
                    $('<div />').append(
 
                        $('<button class="btn btn-primary" />').text("Yes! Send me information about special offers").on("click", function () {
 
                            baton.specialOffers = true;
 
                            baton.buttons.enableNext();
 
                            baton.wizard.next();
 
                        })
 
                    ),
 
                    "<br />",
 
                    $('<div />').append(
 
                        $('<button class="btn" />').text("No, thanks").on("click", function () {
 
                            baton.specialOffers = false;
 
                            baton.buttons.enableNext();
 
                            baton.wizard.next();
 
                        })
 
                    )
 
                );
 
            },
 
 
 
            finish: function (baton) {
 
                // Save baton.specialOffers preference
 
                console.log(baton.specialOffers);
 
            }
 
        });
 
 
 
        point.extend({
 
            id: 'byebye',
 
            index: 500,
 
            title: "Thank you!",
 
            draw: function (baton) {
 
                this.append($("<p>").text("Thank you for completing our welcome wizard! Be sure to tell us what you like and what we could improve in App Suite!"));
 
                baton.buttons.enableNext();
 
            }
 
        });
 
 
 
        // To enable the wizard to run upon startup, you have to use the extension system to add a new stage to the boot process.
 
        // Use a manifest.json to extend the core/main file:
 
        // {
 
        //      namespace: 'io.ox/core/main'
 
        // }
 
 
 
        // Then, in the plugins file, define a new stage that runs the wizard after the curtain has been drawn back:
 
 
 
        /*
 
        define('...', ['io.ox/core/extPatterns/stage'], function (Stage) {
 
        'use strict';
 
 
 
            new Stage('io.ox/core/stages', {
 
                id: 'welcome-wizard',
 
                after: 'curtain',
 
                run: function (baton) {
 
                    var def = $.Deferred();
 
                    //TODO: Check a JSLob if the wizard needs to be run, or has been cleared successfully
 
                    // If it has to be run, require the wizards source file and trigger the wizard
 
                    require(["io.ox/dev/wizard/welcomeWizard"], function (w) {
 
                        w.getInstance().start().done(function () {
 
                            //TODO: Mark this wizard as passed, so as not to start it again
 
                            // Resolve the deferred, so the next stage can start
 
                            def.resolve();
 
                        }).fail(def.reject);
 
                    });
 
 
 
                    return def;
 
                }
 
            });
 
 
 
               
 
        });
 
 
 
        */
 
        return {
 
            getInstance: function () {
 
                // Create a new instance of the wizard. Note that the id of the wizard determines the extension point
 
                // that pages have to extend
 
                return wizards.getWizard({id: 'io.ox/dev/wizard/welcomeWizard', closeable: true});
 
            }
 
        };
 
 
     });
 
     });
  
 +
    */
 +
    return {
 +
        getInstance: function () {
 +
            // Create a new instance of the wizard. Note that the id of the wizard determines
 +
            // the extension point
 +
            // that pages have to extend
 +
            return wizards.getWizard({id: 'io.ox/dev/wizard/welcomeWizard', closeable: true});
 +
        }
 +
    };
 +
});
 +
</syntaxhighlight>
  
 
[[Category:AppSuite]]
 
[[Category:AppSuite]]
 
[[Category:UI]]
 
[[Category:UI]]
 
[[Category:Developer]]
 
[[Category:Developer]]

Revision as of 08:59, 24 September 2013

Writing a wizard

Summary: In this example we will build a simple welcome wizard that tries to complete the users information.


/**
 * All content on this website (including text, images, source
 * code and any other original works), unless otherwise noted,
 * is licensed under a Creative Commons License.
 *
 * http://creativecommons.org/licenses/by-nc-sa/2.5/
 *
 * Copyright (C) Open-Xchange Inc., 2006-2011
 * Mail: info@open-xchange.com
 *
 * @author Francisco Laguna <francisco.laguna@open-xchange.com>
 */

/**
* In this example we will build a simple welcome wizard, that tries to complete the users information.
*/
define('io.ox/dev/wizard/welcomeWizard', ['io.ox/core/extensions', 'io.ox/core/wizard/registry'], function (ext, wizards) {
    'use strict';
    // Grab the extension point for the wizard
    // Every page in the wizard will be an extension to this extension point
    var point = ext.point("io.ox/dev/wizard/welcomeWizard");
    
    // We will build a few pages here to showcase how you can use the framework.
    // Firstly, the simplest case, just a static page
    // It's a nice trick to start off with a static page, so the subsequent page 
    // can already start loading data
    // and initialize itself without the user having to wait around for that. 
    // Distract them with a nice welcome page!
    point.extend({
        id: 'welcomeMessage',
        index: 100,
        title: "Welcome to App Suite", // be sure to internationalize this
        draw: function (baton) {
            // A regular #draw method, that you may know and love from other extension points
            // Just append to 'this' to draw what you need. One caveat though: Make sure to 
            // unlock the 'next' button
            // so this step can be finished

            // Some text. Note that you want to take some more care here, to make this look
            // good and make sense. We'll firmly stay in example land here and not make a 
            // fuss about looks
            // Make sure you do better than this, also, this needs to be internationalized 
            // with a gt() call!
            this.append($("<p>").text("Hi there, stranger! Welcome to OX App Suite, glad you made it. To make sure your experience with us is a pleasant one, let's set up some basics together!"));

            // Enable the next (or 'done', if this is the last page) button.
            // You will have to call this once for every page, once every needed entry has been made.
            baton.buttons.enableNext();
        }
    });

    // Now let's actually ask for some user input. Let's start with the users gender.
    point.extend({
        id: 'gender',
        index: 200,
        title: "Gender",
        draw: function (baton) {
            // Every method of a page is always called with a baton that is unique to 
            // every page instance, so
            // we can set state information in it to our hearts content without 
            // bothering everyone else.
            // The baton holds some interesting objects, though. 'wizard' is the instance 
            // of the wizard object, 'buttons', like above
            // can be used to enable or disable the next button. The wizard also has a 
            // pageData member object that we can use to store
            // data that is available to every subsequent page. Note though, that that 
            // tightly couples pages together, so use this with care!
            // We will use this for some fun, though.
            baton.form = {};

            this.append(
                $('<p/>').text("Please pick one:"),
                $('<form>').append(
                    $("<fieldset>").append(
                        $('<label class="radio">').append(
                            baton.form.male = $('<input type="radio" name="genderRadio" value="male">'),
                            $.txt("Gentleman")
                        ),
                        $('<label class="radio">').append(
                            baton.form.female = $('<input type="radio" name="genderRadio" value="male">'),
                            $.txt("Lady")
                        )
                    )
                )
            );

            // We want to enable the navigation once the user picked his or her gender.
            // And we'll capture that logic in one method that we call everytime we have
            // reason to believe the state changed
            baton.helpers = {
                updateState: function () {
                    if (baton.form.male.attr("checked") === 'checked' || baton.form.female.attr("checked") === 'checked') {
                        // One of the two was picked, so enable the next button
                        baton.buttons.enableNext();
                    } else {
                        // No choice was made, so disable the button
                        baton.buttons.disableNext();
                    }
                }
            };
            baton.form.male.on('click', baton.helpers.updateState);
            baton.form.female.on('click', baton.helpers.updateState);
                
        },

        activate: function (baton) {
            // Whenever the page is entered, the activate method is called.
            // we just have to make sure the button state is correct
            baton.helpers.updateState();
        },

        finish: function (baton) {
            // When the page is left, the 'finish' method is called and we can do something
            // with the entered value, in this case we'll remember it in the wizards data section for inter-page stuff

            var gender = null;
            if (baton.form.male.attr("checked") === 'checked') {
                gender = 'male';
            } else if (baton.form.female.attr("checked") === 'checked') {
                gender = 'female';
            }

            baton.wizard.pageData.gender = gender;
        }
    });

    // Anything above a trivial form may benefit from using backbone model and view classes
    point.extend({
        id: 'completeUserInfo',
        index: 300,
        title: "Personal Information",
        load: function (baton) {
            // The load method is an optional method. It is called to load data that you 
            // need to set up the page
            // And it is called as soon as the page is the 'next' or 'previous' page of the 
            // active page, so you can start loading
            // even before the page shows up. Return a deferred to let the wizard framework 
            // know when you're done.
            

            // We will fetch the user data for our example.
            var def = $.Deferred();

            require(["io.ox/core/api/user", "io.ox/backbone/basicModel", "io.ox/backbone/mini-views"], function (userAPI, Model, mini) {
                // Alright, let's stick the APIs into our baton, we'll need these later
                // This is also a nice little trick for loading APIs in the wizard framework.
                baton.libraries = {
                    userAPI: userAPI,
                    mini: mini
                };

                // And let's load the current user

                userAPI.getCurrentUser().done(function (user) {
                    // Note, that this is a backbone model
                    // We could turn this into a model by instantiating a BasicModel, otherwise.
                    baton.user = user;

                    // We want to enable the next button on this page based on whether a first 
                    // an last name is set, so, let's listen for the change events on the user object
                    function updateButtonState() {
                        if (!_.isEmpty(user.get("first_name")) && !_.isEmpty(user.get("last_name"))) {
                            baton.buttons.enableNext();
                        } else {
                            baton.buttons.disableNext();
                        }
                    }
                    baton.user.on('change', updateButtonState);

                    updateButtonState();

                    // And we're done
                    def.resolve();
                }).fail(def.reject);
            });

            return def;
        },

        draw: function (baton) {
            // Now, for fun, let's try and build a backbone backed form
            // Depending on the complexity of the form, this is a good route to take
            // I would, however, also suggest to scour the appsuite source code for
            // reusable parts, as they will usually be internationalized, localized,
            // responsive to different devices and accessible. Depending on the use of this
            // wizard, you'll have to take care of these aspects yourself.

            // Firstly some fun, though. Why not have this wizard be flirtatious, since it's just
            // getting to know the user. Personally, I think software would do well to be more 
            // flirtatious, but I'm just a lonely developer, so YMMV
            if (baton.wizard.pageData.gender && baton.wizard.pageData.gender === 'male') {
                this.append($("<p>").text("So, who are you, handsome?"));
            } else if (baton.wizard.pageData.gender && baton.wizard.pageData.gender === 'female') {
                this.append($("<p>").text("So, who are you, beautiful?"));
            } else {
                this.append($("<p>").text("So, who are you, stranger?"));
            }

            // Now, on to the serious business
            var mini = baton.libraries.mini;

            this.append(
                $('<form class="form-horizontal" />').append(
                    $('<div class="control-group" />').append(
                        $('<label class="control-label" for="first_name" />').text("First Name"), // Don't forget i18n in your own wizard!
                        $('<div class="controls" />').append(
                            new mini.InputView({name: 'first_name', model: baton.user}).render().$el
                        )
                    ),
                    $('<div class="control-group" />').append(
                        $('<label class="control-label" for="last_name" />').text("Last Name"), // Don't forget i18n in your own wizard!
                        $('<div class="controls" />').append(
                            new mini.InputView({name: 'last_name', model: baton.user}).render().$el
                        )
                    )
                )
            );

        },

        finish: function (baton) {
            // Depending on the capabilities of the model, this could be more complicated
            // you might have to interrogate the model for the #changedAttributes
            // and call an API method. In any case, finish may return a deferred object
            // to denote the state of the save operation
            return baton.user.save();
        }
    });

    // If you want to provide your own navigation controls in the page
    // (useful for a simple choice), you can get rid of the default buttons
    // of the dialog, but have to then call baton.wizard.next or baton.wizard.prev 
    // or baton.wizard.goToPage(pageNumberOrID) manually.

    point.extend({
        id: 'spamMe',
        index: 400,
        title: "Special Offers",
        hideButtons: 'true',
        draw: function (baton) {
            this.append(
                $('<div />').text("Would you like to be informed of special offers from time to time?"),
                "<br />",
                $('<div />').append(
                    $('<button class="btn btn-primary" />').text("Yes! Send me information about special offers").on("click", function () {
                        baton.specialOffers = true;
                        baton.buttons.enableNext();
                        baton.wizard.next();
                    })
                ),
                "<br />",
                $('<div />').append(
                    $('<button class="btn" />').text("No, thanks").on("click", function () {
                        baton.specialOffers = false;
                        baton.buttons.enableNext();
                        baton.wizard.next();
                    })
                )
            );
        },

        finish: function (baton) {
            // Save baton.specialOffers preference
            console.log(baton.specialOffers);
        }
    });

    point.extend({
        id: 'byebye',
        index: 500,
        title: "Thank you!",
        draw: function (baton) {
            this.append($("<p>").text("Thank you for completing our welcome wizard! Be sure to tell us what you like and what we could improve in App Suite!"));
            baton.buttons.enableNext();
        }
    });

    // To enable the wizard to run upon startup, you have to use the extension system to add a new 
    // stage to the boot process.
    // Use a manifest.json to extend the core/main file:
    // {
    //      namespace: 'io.ox/core/main'
    // }

    // Then, in the plugins file, define a new stage that runs the wizard after the curtain has 
    // been drawn back:

    /*
    define('...', ['io.ox/core/extPatterns/stage'], function (Stage) {
    'use strict';

        new Stage('io.ox/core/stages', {
            id: 'welcome-wizard',
            after: 'curtain',
            run: function (baton) {
                var def = $.Deferred();
                //TODO: Check a JSLob if the wizard needs to be run, or has been cleared successfully
                // If it has to be run, require the wizards source file and trigger the wizard
                require(["io.ox/dev/wizard/welcomeWizard"], function (w) {
                    w.getInstance().start().done(function () {
                        //TODO: Mark this wizard as passed, so as not to start it again
                        // Resolve the deferred, so the next stage can start
                        def.resolve();
                    }).fail(def.reject);
                });

                return def;
            }
        });

         
    });

    */
    return {
        getInstance: function () {
            // Create a new instance of the wizard. Note that the id of the wizard determines 
            // the extension point
            // that pages have to extend
            return wizards.getWizard({id: 'io.ox/dev/wizard/welcomeWizard', closeable: true});
        }
    };
});