Difference between revisions of "AppSuite:Writing a wizard"

(Created page with "<div class="title">Writing a wizard</div> '''Summary:''' In this example we will build a simple welcome wizard that tries to complete the users information. __TOC__ /** ...")
 
Line 5: Line 5:
 
__TOC__
 
__TOC__
  
/**
+
  /**
 
     * All content on this website (including text, images, source
 
     * All content on this website (including text, images, source
 
     * code and any other original works), unless otherwise noted,
 
     * code and any other original works), unless otherwise noted,
Line 17: Line 17:
 
     * @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.
Line 39: Line 39:
 
                 // Just append to 'this' to draw what you need. One caveat though: Make sure to unlock the 'next' button
 
                 // 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
 
                 // so this step can be finished
 
+
 
 
                 // Some text. Note that you want to take some more care here, to make this look
 
                 // 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
 
                 // good and make sense. We'll firmly stay in example land here and not make a fuss about looks
Line 50: Line 50:
 
             }
 
             }
 
         });
 
         });
 
+
 
 
         // Now let's actually ask for some user input. Let's start with the users gender.
 
         // Now let's actually ask for some user input. Let's start with the users gender.
 
         point.extend({
 
         point.extend({
Line 64: Line 64:
 
                 // We will use this for some fun, though.
 
                 // We will use this for some fun, though.
 
                 baton.form = {};
 
                 baton.form = {};
 
+
 
 
                 this.append(
 
                 this.append(
 
                     $('<p/>').text("Please pick one:"),
 
                     $('<p/>').text("Please pick one:"),
Line 80: Line 80:
 
                     )
 
                     )
 
                 );
 
                 );
 
+
 
 
                 // We want to enable the navigation once the user picked his or her gender.
 
                 // 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
 
                 // And we'll capture that logic in one method that we call everytime we have
Line 97: Line 97:
 
                 baton.form.male.on('click', baton.helpers.updateState);
 
                 baton.form.male.on('click', baton.helpers.updateState);
 
                 baton.form.female.on('click', baton.helpers.updateState);
 
                 baton.form.female.on('click', baton.helpers.updateState);
                   
+
 
 
             },
 
             },
  
Line 105: Line 105:
 
                 baton.helpers.updateState();
 
                 baton.helpers.updateState();
 
             },
 
             },
 
+
 
 
             finish: function (baton) {
 
             finish: function (baton) {
 
                 // When the page is left, the 'finish' method is called and we can do something
 
                 // 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
 
                 // with the entered value, in this case we'll remember it in the wizards data section for inter-page stuff
 
+
 
 
                 var gender = null;
 
                 var gender = null;
 
                 if (baton.form.male.attr("checked") === 'checked') {
 
                 if (baton.form.male.attr("checked") === 'checked') {
Line 116: Line 116:
 
                     gender = 'female';
 
                     gender = 'female';
 
                 }
 
                 }
 
+
 
 
                 baton.wizard.pageData.gender = gender;
 
                 baton.wizard.pageData.gender = gender;
 
             }
 
             }
 
         });
 
         });
 
+
 
 
         // Anything above a trivial form may benefit from using backbone model and view classes
 
         // Anything above a trivial form may benefit from using backbone model and view classes
 
         point.extend({
 
         point.extend({
Line 131: Line 131:
 
                 // even before the page shows up. Return a deferred to let the wizard framework know when you're done.
 
                 // 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.
 
                 // We will fetch the user data for our example.
 
                 var def = $.Deferred();
 
                 var def = $.Deferred();
 
+
 
 
                 require(["io.ox/core/api/user", "io.ox/backbone/basicModel", "io.ox/backbone/mini-views"], function (userAPI, Model, mini) {
 
                 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
 
                     // Alright, let's stick the APIs into our baton, we'll need these later
Line 142: Line 142:
 
                         mini: mini
 
                         mini: mini
 
                     };
 
                     };
 
+
 
 
                     // And let's load the current user
 
                     // And let's load the current user
 
+
 
 
                     userAPI.getCurrentUser().done(function (user) {
 
                     userAPI.getCurrentUser().done(function (user) {
 
                         // Note, that this is a backbone model
 
                         // Note, that this is a backbone model
 
                         // We could turn this into a model by instantiating a BasicModel, otherwise.
 
                         // We could turn this into a model by instantiating a BasicModel, otherwise.
 
                         baton.user = user;
 
                         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
 
                         // 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
 
                         // listen for the change events on the user object
Line 160: Line 160:
 
                         }
 
                         }
 
                         baton.user.on('change', updateButtonState);
 
                         baton.user.on('change', updateButtonState);
 
+
 
 
                         updateButtonState();
 
                         updateButtonState();
 
+
 
 
                         // And we're done
 
                         // And we're done
 
                         def.resolve();
 
                         def.resolve();
 
                     }).fail(def.reject);
 
                     }).fail(def.reject);
 
                 });
 
                 });
 
+
 
 
                 return def;
 
                 return def;
 
             },
 
             },
 
+
 
 
             draw: function (baton) {
 
             draw: function (baton) {
 
                 // Now, for fun, let's try and build a backbone backed form
 
                 // Now, for fun, let's try and build a backbone backed form
Line 178: Line 178:
 
                 // responsive to different devices and accessible. Depending on the use of this
 
                 // responsive to different devices and accessible. Depending on the use of this
 
                 // wizard, you'll have to take care of these aspects yourself.
 
                 // 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
 
                 // 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
 
                 // getting to know the user. Personally, I think software would do well to be more flirtatious, but
Line 189: Line 189:
 
                     this.append($("<p>").text("So, who are you, stranger?"));
 
                     this.append($("<p>").text("So, who are you, stranger?"));
 
                 }
 
                 }
 
+
 
 
                 // Now, on to the serious business
 
                 // Now, on to the serious business
 
                 var mini = baton.libraries.mini;
 
                 var mini = baton.libraries.mini;
 
+
 
 
                 this.append(
 
                 this.append(
 
                     $('<form class="form-horizontal" />').append(
 
                     $('<form class="form-horizontal" />').append(
Line 209: Line 209:
 
                     )
 
                     )
 
                 );
 
                 );
 
+
 
 
             },
 
             },
 
+
 
 
             finish: function (baton) {
 
             finish: function (baton) {
 
                 // Depending on the capabilities of the model, this could be more complicated
 
                 // Depending on the capabilities of the model, this could be more complicated
Line 220: Line 220:
 
             }
 
             }
 
         });
 
         });
 
+
 
 
         // If you want to provide your own navigation controls in the page
 
         // 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
 
         // (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)
 
         // of the dialog, but have to then call baton.wizard.next or baton.wizard.prev or baton.wizard.goToPage(pageNumberOrID)
 
         // manually.
 
         // manually.
 
+
 
 
         point.extend({
 
         point.extend({
 
             id: 'spamMe',
 
             id: 'spamMe',
Line 252: Line 252:
 
                 );
 
                 );
 
             },
 
             },
 
+
 
 
             finish: function (baton) {
 
             finish: function (baton) {
 
                 // Save baton.specialOffers preference
 
                 // Save baton.specialOffers preference
Line 258: Line 258:
 
             }
 
             }
 
         });
 
         });
 
+
 
 
         point.extend({
 
         point.extend({
 
             id: 'byebye',
 
             id: 'byebye',
Line 268: Line 268:
 
             }
 
             }
 
         });
 
         });
 
+
 
 
         // To enable the wizard to run upon startup, you have to use the extension system to add a new stage to the boot process.
 
         // 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:
 
         // Use a manifest.json to extend the core/main file:
Line 274: Line 274:
 
         //      namespace: 'io.ox/core/main'
 
         //      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:
 
         // 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) {
 
         define('...', ['io.ox/core/extPatterns/stage'], function (Stage) {
 
         'use strict';
 
         'use strict';
 
+
 
 
             new Stage('io.ox/core/stages', {
 
             new Stage('io.ox/core/stages', {
 
                 id: 'welcome-wizard',
 
                 id: 'welcome-wizard',
Line 295: Line 295:
 
                         }).fail(def.reject);
 
                         }).fail(def.reject);
 
                     });
 
                     });
 
+
 
 
                     return def;
 
                     return def;
 
                 }
 
                 }
 
             });
 
             });
 
+
 
           
+
               
 
         });
 
         });
 
+
 
 
         */
 
         */
 
         return {
 
         return {
Line 312: Line 312:
 
         };
 
         };
 
     });
 
     });
 +
 +
 +
[[Category:AppSuite]]
 +
[[Category:UI]]
 +
[[Category:Developer]]

Revision as of 08:52, 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($("

").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!")); // 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( $('

').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($("

").text("So, who are you, handsome?")); } else if (baton.wizard.pageData.gender && baton.wizard.pageData.gender === 'female') { this.append($("

").text("So, who are you, beautiful?")); } else { this.append($("

").text("So, who are you, stranger?")); } // Now, on to the serious business var mini = baton.libraries.mini; this.append( $('<form class="form-horizontal" />').append( $('

').append(

                           $('<label class="control-label" for="first_name" />').text("First Name"), // Don't forget i18n in your own wizard!

$('

').append(

                               new mini.InputView({name: 'first_name', model: baton.user}).render().$el
                           )
                       ),

$('

').append(

                           $('<label class="control-label" for="last_name" />').text("Last Name"), // Don't forget i18n in your own wizard!

$('

').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(

$('

').text("Would you like to be informed of special offers from time to time?"),

                   "
",

$('

').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();
                       })
                   ),
                   "
",

$('

').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($("

").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}); } }; });