<!DOCTYPE html>
<html>
<head>
<script data-require="jquery@1.10.1"
data-semver="1.10.1"
src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<link data-require="bootstrap@3.0.0"
data-semver="3.0.0" rel="stylesheet"
href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" />
<script data-require="bootstrap@3.0.0"
data-semver="3.0.0"
src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="jquery.bootstrap.wizard.js"></script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js@1.0.8"
data-semver="1.0.8"
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
<script src="rcSubmit.js"></script>
<script src="rcDisabled.js"></script>
<script src="rcWizard.js"></script>
<script src="sampleWizardController.js"></script>
<script src="sampleWizardApp.js"></script>
</head>
<body ng-app="SampleWizardApp">
<div class="container" >
<div class="row">
<div class="col-xs-12 col-sm-6 col-sm-offset-3">
<h1>Simple Wizard</h1>
<div ng-controller="SampleWizardController"
rc-wizard="sampleWizard" rc-disabled="rc.firstForm.submitInProgress">
<ul class="nav rc-nav-wizard">
<li class="active">
<a class="active" href="#first" data-toggle="tab">
<span class="badge">1</span>
<span>First Step</span>
</a>
</li>
<li>
<a href="#second" data-toggle="tab">
<span class="badge">2</span>
<span>Second Step</span>
</a>
</li>
<li>
<a href="#last" data-toggle="tab">
<span class="badge">3</span>
<span>Last Step</span>
</a>
</li>
</ul>
<div class="tab-content">
<form class="tab-pane active" id="first" name="firstForm"
rc-submit="saveState()" rc-step novalidate>
<h2>Enter first step data</h2>
<div class="form-group"
ng-class="{'has-error': rc.firstForm.needsAttention(firstForm.firstName)}">
<label class="control-label">First Name</label>
<input name="firstName" class="form-control" type="text" required
ng-model="user.firstName"/>
</div>
<div class="form-group"
ng-class="{'has-error': rc.firstForm.needsAttention(firstForm.lastName)}">
<label class="control-label">Last Name</label>
<input name="lastName" class="form-control" type="text" required
ng-model="user.lastName" />
</div>
</form>
<form class="tab-pane" id="second" name="secondForm" rc-submit rc-step>
<h2>Enter second step data</h2>
<div class="form-group">
<label class="control-label">Street Address</label>
<input name="streetAddress" class="form-control" type="text"
ng-model="user.streetAddress" />
</div>
<div class="form-group">
<label class="control-label">City</label>
<input name="city" class="form-control" type="text"
ng-model="user.city" />
</div>
<div class="form-group">
<label class="control-label">State</label>
<input name="state" class="form-control" type="text"
ng-model="user.state" />
</div>
<div class="form-group">
<label class="control-label">City</label>
<input name="postalCode" class="form-control" type="text"
ng-model="user.postalCode" />
</div>
</form>
<form class="tab-pane" id="last" name="lastForm" rc-submit="completeWizard()" rc-step>
<h2>Finish last step</h2>
<div class="form-group">
<label class="control-label">First Name:</label>
<p class="form-control-static">{{ user.firstName }}</p>
</div>
<div class="form-group">
<label class="control-label">Last Name:</label>
<p class="form-control-static">{{ user.lastName }}</p>
</div>
<div class="form-group">
<label class="control-label">Address:</label>
<p class="form-control-static">
{{ user.streetAddress }}
<br />
{{ user.city }}, {{ user.state }} {{ user.postalCode }}
</p>
</div>
</form>
</div>
<div class="form-group">
<div class="pull-right">
<a class="btn btn-default" ng-click="rc.sampleWizard.backward()"
ng-show="rc.sampleWizard.currentIndex > rc.sampleWizard.firstIndex">Back</a>
<a class="btn btn-primary" data-loading-text="Please Wait..." ng-click="rc.sampleWizard.forward()"
ng-show="rc.sampleWizard.currentIndex < rc.sampleWizard.navigationLength">Continue</a>
<a class="btn btn-primary" ng-click="rc.sampleWizard.forward()"
ng-show="rc.sampleWizard.currentIndex == rc.sampleWizard.navigationLength">Complete</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
/* Styles go here */
.rc-nav-wizard > li {
float: left;
font-size: 18px;
}
.rc-nav-wizard > li + li {
margin-left: 2px;
}
.rc-nav-wizard > li > a {
border-radius: 5px;
cursor: default;
color: #999;
}
.rc-nav-wizard > li > a,
.rc-nav-wizard > li > a:hover,
.rc-nav-wizard > li > a:focus {
background-color: transparent;
}
.rc-nav-wizard > li > a > .badge {
margin-left: 3px;
font-size: 18px;
padding: 5px 9px;
border-radius: 15px;
}
/* active = current wizard step */
.rc-nav-wizard > li.active > a,
.rc-nav-wizard > li.active > a:hover,
.rc-nav-wizard > li.active > a:focus {
color: #428bca;
background-color: transparent;
}
.rc-nav-wizard > .active > a > .badge {
color: #ffffff;
background-color: #428bca;
}
/* success = completed wizard step */
.rc-nav-wizard > li.success > a,
.rc-nav-wizard > li.success > a:hover,
.rc-nav-wizard > li.success > a:focus {
color: #D4AF37;
background-color: transparent;
}
.rc-nav-wizard > .success > a > .badge {
color: #ffffff;
background-color: #D4AF37;
}
The is a live example of code found in the following article:
http://code.realcrowd.com/the-wonderful-wizard-of-angularjs/
/*!
* jQuery twitter bootstrap wizard plugin
* Examples and documentation at: http://github.com/VinceG/twitter-bootstrap-wizard
* version 1.0
* Requires jQuery v1.3.2 or later
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
* Authors: Vadim Vincent Gabriel (http://vadimg.com), Jason Gill (www.gilluminate.com)
*/
;(function($) {
var bootstrapWizardCreate = function(element, options) {
var element = $(element);
var obj = this;
// Merge options with defaults
var $settings = $.extend({}, $.fn.bootstrapWizard.defaults, options);
var $activeTab = null;
var $navigation = null;
this.rebindClick = function(selector, fn)
{
selector.unbind('click', fn).bind('click', fn);
}
this.fixNavigationButtons = function() {
// Get the current active tab
if(!$activeTab.length) {
// Select first one
$navigation.find('a:first').tab('show');
$activeTab = $navigation.find('li:first');
}
// See if we're currently in the first/last then disable the previous and last buttons
$($settings.previousSelector, element).toggleClass('disabled', (obj.firstIndex() >= obj.currentIndex()));
$($settings.nextSelector, element).toggleClass('disabled', (obj.currentIndex() >= obj.navigationLength()));
// We are unbinding and rebinding to ensure single firing and no double-click errors
obj.rebindClick($($settings.nextSelector, element), obj.next);
obj.rebindClick($($settings.previousSelector, element), obj.previous);
obj.rebindClick($($settings.lastSelector, element), obj.last);
obj.rebindClick($($settings.firstSelector, element), obj.first);
if($settings.onTabShow && typeof $settings.onTabShow === 'function' && $settings.onTabShow($activeTab, $navigation, obj.currentIndex())===false){
return false;
}
};
this.next = function(e) {
// If we clicked the last then dont activate this
if(element.hasClass('last')) {
return false;
}
if($settings.onNext && typeof $settings.onNext === 'function' && $settings.onNext($activeTab, $navigation, obj.nextIndex())===false){
return false;
}
// Did we click the last button
$index = obj.nextIndex();
if($index > obj.navigationLength()) {
} else {
$navigation.find('li:eq('+$index+') a').tab('show');
}
};
this.previous = function(e) {
// If we clicked the first then dont activate this
if(element.hasClass('first')) {
return false;
}
if($settings.onPrevious && typeof $settings.onPrevious === 'function' && $settings.onPrevious($activeTab, $navigation, obj.previousIndex())===false){
return false;
}
$index = obj.previousIndex();
if($index < 0) {
} else {
$navigation.find('li:eq('+$index+') a').tab('show');
}
};
this.first = function(e) {
if($settings.onFirst && typeof $settings.onFirst === 'function' && $settings.onFirst($activeTab, $navigation, obj.firstIndex())===false){
return false;
}
// If the element is disabled then we won't do anything
if(element.hasClass('disabled')) {
return false;
}
$navigation.find('li:eq(0) a').tab('show');
};
this.last = function(e) {
if($settings.onLast && typeof $settings.onLast === 'function' && $settings.onLast($activeTab, $navigation, obj.lastIndex())===false){
return false;
}
// If the element is disabled then we won't do anything
if(element.hasClass('disabled')) {
return false;
}
$navigation.find('li:eq('+obj.navigationLength()+') a').tab('show');
};
this.currentIndex = function() {
return $navigation.find('li').index($activeTab);
};
this.firstIndex = function() {
return 0;
};
this.lastIndex = function() {
return obj.navigationLength();
};
this.getIndex = function(e) {
return $navigation.find('li').index(e);
};
this.nextIndex = function() {
return $navigation.find('li').index($activeTab) + 1;
};
this.previousIndex = function() {
return $navigation.find('li').index($activeTab) - 1;
};
this.navigationLength = function() {
return $navigation.find('li').length - 1;
};
this.activeTab = function() {
return $activeTab;
};
this.nextTab = function() {
return $navigation.find('li:eq('+(obj.currentIndex()+1)+')').length ? $navigation.find('li:eq('+(obj.currentIndex()+1)+')') : null;
};
this.previousTab = function() {
if(obj.currentIndex() <= 0) {
return null;
}
return $navigation.find('li:eq('+parseInt(obj.currentIndex()-1)+')');
};
this.show = function(index) {
return element.find('li:eq(' + index + ') a').tab('show');
};
this.disable = function(index) {
$navigation.find('li:eq('+index+')').addClass('disabled');
};
this.enable = function(index) {
$navigation.find('li:eq('+index+')').removeClass('disabled');
};
this.hide = function(index) {
$navigation.find('li:eq('+index+')').hide();
};
this.display = function(index) {
$navigation.find('li:eq('+index+')').show();
};
this.remove = function(args) {
var $index = args[0];
var $removeTabPane = typeof args[1] != 'undefined' ? args[1] : false;
var $item = $navigation.find('li:eq('+$index+')');
// Remove the tab pane first if needed
if($removeTabPane) {
var $href = $item.find('a').attr('href');
$($href).remove();
}
// Remove menu item
$item.remove();
};
$navigation = element.find('ul:first', element);
$activeTab = $navigation.find('li.active', element);
if(!$navigation.hasClass($settings.tabClass)) {
$navigation.addClass($settings.tabClass);
}
// Load onInit
if($settings.onInit && typeof $settings.onInit === 'function'){
$settings.onInit($activeTab, $navigation, 0);
}
// Load onShow
if($settings.onShow && typeof $settings.onShow === 'function'){
$settings.onShow($activeTab, $navigation, obj.nextIndex());
}
// Work the next/previous buttons
obj.fixNavigationButtons();
$('a[data-toggle="tab"]', $navigation).on('click', function (e) {
// Get the index of the clicked tab
var clickedIndex = $navigation.find('li').index($(e.currentTarget).parent('li'));
if($settings.onTabClick && typeof $settings.onTabClick === 'function' && $settings.onTabClick($activeTab, $navigation, obj.currentIndex(), clickedIndex)===false){
return false;
}
});
$('a[data-toggle="tab"]', $navigation).on('shown shown.bs.tab', function (e) { // use shown instead of show to help prevent double firing
$element = $(e.target).parent();
var nextTab = $navigation.find('li').index($element);
// If it's disabled then do not change
if($element.hasClass('disabled')) {
return false;
}
if($settings.onTabChange && typeof $settings.onTabChange === 'function' && $settings.onTabChange($activeTab, $navigation, obj.currentIndex(), nextTab)===false){
return false;
}
$activeTab = $element; // activated tab
obj.fixNavigationButtons();
});
};
$.fn.bootstrapWizard = function(options) {
//expose methods
if (typeof options == 'string') {
var args = Array.prototype.slice.call(arguments, 1)
if(args.length === 1) {
args.toString();
}
return this.data('bootstrapWizard')[options](args);
}
return this.each(function(index){
var element = $(this);
// Return early if this element already has a plugin instance
if (element.data('bootstrapWizard')) return;
// pass options to plugin constructor
var wizard = new bootstrapWizardCreate(element, options);
// Store plugin object in this element's data
element.data('bootstrapWizard', wizard);
});
};
// expose options
$.fn.bootstrapWizard.defaults = {
tabClass: 'nav nav-pills',
nextSelector: '.wizard li.next',
previousSelector: '.wizard li.previous',
firstSelector: '.wizard li.first',
lastSelector: '.wizard li.last',
onShow: null,
onInit: null,
onNext: null,
onPrevious: null,
onLast: null,
onFirst: null,
onTabChange: null,
onTabClick: null,
onTabShow: null
};
})(jQuery);
var rcWizardDirective = {
'rcWizard': function () {
return {
restrict: 'A',
controller: ['$scope', function ($scope) {
var self;
var wizardElement;
var wizardOptions = {};
var steps = [];
this.currentIndex = 0;
this.firstIndex = 0;
this.navigationLength = 0;
this.addStep = function (step) {
steps.push(step);
if (!step.element || !step.submitController) return;
// if a rcSubmitController is being used, automatically add a _hidden_
// submit button so that
// in order to place an submit button that is still functional it
// has to technically be visible, so instead we place it way off
// screen
jQuery(step.element)
.append('<input type="submit" tabindex="-1" style="position: absolute; left: -9999px; width: 1px; height: 1px;" />')
.attr('action', 'javascript:void(0);');
// bind to the submit complete event on the rcSubmitController and
// if the action was successful, trigger a next on the wizard.
step.submitController.onSubmitComplete(function (evt) {
if (evt.success) {
onForward(step);
}
});
};
this.forward = function () {
if (steps.length)
var currentStep = (steps.length > self.currentIndex) ? steps[self.currentIndex] : null;
if (0 < steps.length && !currentStep) return;
if (0 < steps.length && currentStep.submitController) {
currentStep.submitController.submit();
} else {
onForward(currentStep);
}
};
var onForward = function(currentStep) {
if (0 < steps.length &&
currentStep.formController &&
currentStep.formController.$invalid) return;
wizardElement.bootstrapWizard('next');
};
this.backward = function () {
wizardElement.bootstrapWizard('previous');
};
var onTabChange = function (activeTab, navigation, currentIndex, nextTab) {
self.currentIndex = nextTab;
self.firstIndex = wizardElement.bootstrapWizard('firstIndex');
self.navigationLength = wizardElement.bootstrapWizard('navigationLength');
if (!$scope.$$phase) $scope.$apply();
};
var onTabClick = function (activeTab, navigation, currentIndex, clickedIndex) {
return false;
};
var onTabShow = function (activeTab, navigation, currentIndex) {
if (currentIndex > 0) {
wizardElement
.find('.nav li:gt(' + (currentIndex - 1) + ')')
.removeClass("success");
wizardElement.find('.nav li:lt(' + currentIndex + ')')
.addClass("success");
} else {
wizardElement.find('.nav li').removeClass("success");
}
// if a rcStep is being used on the current tab,
// automatically focus on the first input of the current tab. This
// allows for easier keyboard-ony navigation.
if (steps.length > currentIndex && steps[currentIndex].element) {
steps[currentIndex].element.find('input').first().focus();
}
};
var updateWizard = function (options) {
wizardOptions = options;
if (wizardElement) {
wizardElement.bootstrapWizard(options);
self.currentIndex = wizardElement.bootstrapWizard('currentIndex');
self.firstIndex = wizardElement.bootstrapWizard('firstIndex');
self.navigationLength = wizardElement.bootstrapWizard('navigationLength');
if (!$scope.$$phase) $scope.$apply();
}
};
this.setWizardElement = function (element) {
wizardElement = element;
self = this;
updateWizard({
'onTabChange': onTabChange,
'onTabShow': onTabShow,
'onTabClick': onTabClick
});
};
}],
compile: function (cElement, cAttributes, transclude) {
return {
pre: function (scope, formElement, attributes, wizardController) {
// put a reference to the wizardcontroller on the scope so we can
// use some of the properties in the markup
scope.rc = scope.rc || {};
scope.rc[attributes.rcWizard] = wizardController;
},
post: function (scope, element, attributes, wizardController) {
// let the controller know about the element
wizardController.setWizardElement(element);
if (!scope.$$phase) scope.$apply();
}
};
}
}
}
};
var rcWizardStepDirective = {
'rcStep': function () {
return {
restrict: 'A',
require: ['^rcWizard', '?form', '?rcSubmit'],
link: function (scope, element, attributes, controllers) {
var wizardController = controllers[0];
// find all the optional controllers for the step
var formController = controllers.length > 1 ? controllers[1] : null;
var submitController = controllers.length > 2 ? controllers[2] : null;
// add the step to the wizard controller
var step = wizardController.addStep({
'element': element,
'attributes': attributes,
'formController': formController,
'submitController': submitController });
}
};
}
};
angular.module('rcWizard', ['ng'])
.directive(rcWizardDirective)
.directive(rcWizardStepDirective);
var rcSubmitDirective = {
'rcSubmit': ['$parse', '$q', '$timeout', function ($parse, $q, $timeout) {
return {
restrict: 'A',
require: ['rcSubmit', '?form'],
controller: ['$scope', function ($scope) {
var formElement = null;
var formController = null;
var submitCompleteHandlers = [];
this.attempted = false;
this.submitInProgress = false;
this.setFormElement = function(element) {
formElement = element;
}
this.submit = function() {
if (!formElement) return;
jQuery(formElement).submit();
}
this.setAttempted = function() {
this.attempted = true;
};
this.setFormController = function(controller) {
formController = controller;
};
this.needsAttention = function (fieldModelController) {
if (!formController) return false;
if (fieldModelController) {
return fieldModelController.$invalid && (fieldModelController.$dirty || this.attempted);
} else {
return formController && formController.$invalid && (formController.$dirty || this.attempted);
}
};
this.onSubmitComplete = function (handler) {
submitCompleteHandlers.push(handler);
};
this.setSubmitComplete = function (success, data) {
angular.forEach(submitCompleteHandlers, function (handler) {
handler({ 'success': success, 'data': data });
});
};
}],
compile: function(cElement, cAttributes, transclude) {
return {
pre: function(scope, formElement, attributes, controllers) {
var submitController = controllers[0];
var formController = (controllers.length > 1) ? controllers[1] : null;
submitController.setFormElement(formElement);
submitController.setFormController(formController);
scope.rc = scope.rc || {};
scope.rc[attributes.name] = submitController;
},
post: function(scope, formElement, attributes, controllers) {
var submitController = controllers[0];
var formController = (controllers.length > 1) ? controllers[1] : null;
var fn = $parse(attributes.rcSubmit);
formElement.bind('submit', function () {
submitController.setAttempted();
if (!scope.$$phase) scope.$apply();
if (!formController.$valid) return false;
var doSubmit = function () {
submitController.submitInProgress = true;
if (!scope.$$phase) scope.$apply();
var returnPromise = $q.when(fn(scope, { $event: event }));
returnPromise.then(function (result) {
submitController.submitInProgress = false;
if (!scope.$$phase) scope.$apply();
$timeout(function() {
submitController.setSubmitComplete(true, result);
});
}, function (error) {
submitController.submitInProgress = false;
if (!scope.$$phase) scope.$apply();
submitController.setSubmitComplete(false, error);
});
};
if (!scope.$$phase) {
scope.$apply(doSubmit);
} else {
doSubmit();
if (!scope.$$phase) scope.$apply();
}
});
}
};
}
};
}]
};
var rcDisabledDirective = {
'rcDisabled': ['rcDisabled', function (rcDisabled) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
scope.$watch(attributes.rcDisabled, function(isDisabled) {
rcDisabled.disable(element, isDisabled);
});
}
}
}]
};
var rcDisabledProvider = function () {
var defaultDisableHandler = function(rootElement, isDisabled) {
var jElement = jQuery(rootElement);
return jElement
.find(':not([rc-disabled])')
.filter(function(index) {
return jQuery(this).parents().not(jElement).filter('[rc-disabled]').length === 0;
})
.filter('input:not([ng-disabled]), button:not([ng-disabled])')
.prop('disabled', isDisabled);
};
var customDisableHandler;
this.onDisable = function (customHandler) {
customDisableHandler = customHandler;
};
this.$get = function () {
return {
disable: function (rootElement, isDisabled) {
return (customDisableHandler) ? customDisableHandler(rootElement, isDisabled) : defaultDisableHandler(rootElement, isDisabled);
}
}
};
};
angular.module('rcDisabled', [])
.provider('rcDisabled', rcDisabledProvider)
.directive(rcDisabledDirective);
angular.module('rcDisabledBootstrap', ['rcDisabled'])
.provider('rcDisabled', rcDisabledProvider)
.directive(rcDisabledDirective)
.config(['rcDisabledProvider', function(rcDisabledProvider) {
rcDisabledProvider.onDisable(function(rootElement, isDisabled) {
var jqElement = jQuery(rootElement);
jqElement = jqElement
.find(':not([rc-disabled])')
.filter(function(index) {
return jQuery(this).parents().not(jqElement).filter('[rc-disabled]').length === 0;
})
.filter('input:not([ng-disabled]), button:not([ng-disabled]), .btn, li')
.add(jqElement);
// if the Bootstrap "Button" jQuery plug-in is loaded, use it on those
// that have it configured
if (jqElement.button) {
jqElement.find('[data-loading-text]').button((isDisabled) ? 'loading' : 'reset');
}
jqElement.toggleClass('disabled', isDisabled)
.filter('input, button')
.prop('disabled', isDisabled);
});
}]);
angular.module('rcForm', [])
.directive(rcSubmitDirective);
// define module for app
angular.module('SampleWizardApp', ['rcWizard', 'rcForm', 'rcDisabledBootstrap']);
// define controller for wizard
var SampleWizardController = ['$scope', '$q', '$timeout',
function ($scope, $q, $timeout) {
$scope.user = {};
$scope.saveState = function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve();
}, 5000);
return deferred.promise;
};
$scope.completeWizard = function() {
alert('Completed!');
}
}];