').addClass('form-step');
70 |
71 | var controller = void 0,
72 | template = void 0,
73 | promisesHash = {};
74 |
75 | // Get template
76 | promisesHash.$template = resolveTemplate(formStep);
77 |
78 | // Get resolve
79 | _angular2.default.forEach(formStep.resolve, function (resolveVal, resolveName) {
80 | promisesHash[resolveName] =
81 | // angular.isString(resolveVal) ?
82 | // $injector.get(resolveVal) :
83 | $injector.invoke(resolveVal);
84 | });
85 |
86 | // After all locals are resolved (template and "resolves") //
87 | return $q.all(promisesHash).then(function (locals) {
88 | // Extend formStep locals with resolved locals
89 | locals = _angular2.default.extend({}, formStep.locals, locals);
90 | // Load template inside element
91 | locals.$template = locals.$template.data || locals.$template;
92 | formStepElement.html(locals.$template);
93 | // Create scope
94 | var formStepScope = getScope(multiStepFormScope, formStep, multiStepFormInstance);
95 |
96 | if (formStep.controller) {
97 | // Create form step scope
98 | locals.$scope = formStepScope;
99 | // Add multi step form service instance to local injectables
100 | locals.multiStepFormInstance = multiStepFormInstance;
101 | // Add multi step form scope to local injectables if isolated
102 | if (formStep.isolatedScope) {
103 | locals.multiStepFormScope = multiStepFormScope;
104 | }
105 | // Instanciate controller
106 | controller = $controller(formStep.controller, locals);
107 | // controllerAs
108 | if (formStep.controllerAs) {
109 | formStepScope[formStep.controllerAs] = controller;
110 | }
111 | formStepElement.data('$stepController', controller);
112 | // formStepElement.children().data('$ngControllerController', controller);
113 | }
114 |
115 | // Compile form step element and link with scope
116 | $compile(formStepElement)(formStepScope);
117 |
118 | // Return element and scope
119 | return {
120 | element: formStepElement,
121 | scope: formStepScope
122 | };
123 | });
124 | };
125 | }
--------------------------------------------------------------------------------
/dist/commonjs/services/form-step-object.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = FormStep;
7 |
8 | /**
9 | * @ngdoc object
10 | * @name multiStepForm:FormStep
11 | *
12 | * @description A constructor for creating form steps
13 | * @error If no template or templateUrl properties are supplied
14 | */
15 |
16 | function FormStep() {
17 | return function FormStep(config) {
18 | if (!config.template && !config.templateUrl) {
19 | throw new Error('Either template or templateUrl properties have to be provided for' + ' multi step form' + config.title);
20 | }
21 |
22 | /**
23 | * @ngdoc property
24 | * @propertyOf multiStepForm:FormStep
25 | *
26 | * @description The form step title
27 | * @type {String}
28 | */
29 | this.title = config.title;
30 |
31 | /**
32 | * @ngdoc property
33 | * @propertyOf multiStepForm:FormStep
34 | *
35 | * @description The form step additional data
36 | * @type {Object}
37 | */
38 | this.data = config.data || {};
39 |
40 | /**
41 | * @ngdoc property
42 | * @propertyOf multiStepForm:FormStep
43 | *
44 | * @description The form step controller
45 | * @type {String|Function|Array}
46 | */
47 | this.controller = config.controller;
48 |
49 | /**
50 | * @ngdoc property
51 | * @propertyOf multiStepForm:FormStep
52 | *
53 | * @description An identifier name for a reference to the controller
54 | * @type {String}
55 | */
56 | this.controllerAs = config.controllerAs;
57 |
58 | /**
59 | * @ngdoc property
60 | * @propertyOf multiStepForm:FormStep
61 | *
62 | * @description The form step template
63 | * @type {String}
64 | */
65 | this.template = config.template;
66 |
67 | /**
68 | * @ngdoc property
69 | * @propertyOf multiStepForm:FormStep
70 | *
71 | * @description The form step template URL
72 | * @type {String}
73 | */
74 | this.templateUrl = config.templateUrl;
75 |
76 | /**
77 | * Whether or not the form step should have an isolated scope
78 | * @type {Boolean}
79 | */
80 | this.isolatedScope = config.isolatedScope || false;
81 |
82 | /**
83 | * @ngdoc property
84 | * @propertyOf multiStepForm:resolve
85 | *
86 | * @description The form step resolve map (same use than for routes)
87 | * @type {Object}
88 | */
89 | this.resolve = config.resolve || {};
90 |
91 | /**
92 | * @ngdoc property
93 | * @propertyOf multiStepForm:resolve
94 | *
95 | * @description The form step locals map (same than resolve but for non deferred values)
96 | * Note: resolve also works with non deferred values
97 | * @type {Object}
98 | */
99 | this.locals = config.locals || {};
100 |
101 | /**
102 | * @ngdoc property
103 | * @propertyOf multiStepForm:FormStep
104 | *
105 | * @description Whether or not this form step contains a form
106 | * @type {Boolean}
107 | */
108 | this.hasForm = config.hasForm || false;
109 |
110 | /**
111 | * @ngdoc property
112 | * @propertyOf multiStepForm:FormStep
113 | *
114 | * @description Whether or not this form step is valid.
115 | * Form validity can been fed back using a specific directive.
116 | * {@link multiStepForm:formStepValidity formStepValidity}
117 | * @type {Boolean}
118 | */
119 | this.valid = false;
120 |
121 | /**
122 | * @ngdoc property
123 | * @propertyOf multiStepForm:FormStep
124 | *
125 | * @description Whether or not this step has been visited
126 | * (i.e. the user has moved to the next step)
127 | * @type {Boolean}
128 | */
129 | this.visited = false;
130 | };
131 | }
--------------------------------------------------------------------------------
/dist/commonjs/services/multi-step-form-factory.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = ['$q', '$location', '$rootScope', multiStepForm];
7 |
8 | /**
9 | * @ngdoc function
10 | * @name multiStepForm:multiStepForm
11 | *
12 | * @requires $q
13 | * @requires multiStepForm:FormStep
14 | *
15 | * @description A service returning an instance per multi step form.
16 | * The instance of the service is injected in each step controller.
17 | */
18 |
19 | function multiStepForm($q, $location, $rootScope) {
20 | function MultiFormStep(searchId) {
21 | var _this = this;
22 |
23 | /**
24 | * @ngdoc property
25 | * @propertyOf multiStepForm:multiStepForm
26 | *
27 | * @description The location search property name to store the active step index.
28 | * @type {String}
29 | */
30 | this.searchId = searchId;
31 | // If the search id is defined,
32 | if (angular.isDefined(searchId)) {
33 | $rootScope.$on('$locationChangeSuccess', function (event) {
34 | var searchIndex = parseInt($location.search()[_this.searchId]);
35 |
36 | if (!isNaN(searchIndex) && _this.activeIndex !== searchIndex) {
37 | // Synchronise
38 | _this.setActiveIndex(parseInt(searchIndex));
39 | }
40 | });
41 | }
42 |
43 | /**
44 | * @ngdoc property
45 | * @propertyOf multiStepForm:multiStepForm
46 | *
47 | * @description The form steps
48 | * @type {Array}
49 | */
50 | this.steps = [];
51 |
52 | /**
53 | * @ngdoc property
54 | * @propertyOf multiStepForm:multiStepForm
55 | *
56 | * @description Return the form steps
57 | * @return {Array}
58 | */
59 | this.getSteps = function () {
60 | return this.steps;
61 | };
62 |
63 | /**
64 | * @ngdoc property
65 | * @propertyOf multiStepForm:multiStepForm
66 | *
67 | * @description The multi-step form deferred object
68 | * @type {Deferred}
69 | */
70 | this.deferred = $q.defer();
71 |
72 | /**
73 | * @ngdoc method
74 | * @methodOf multiStepForm:multiStepForm
75 | *
76 | * @description Initialise the form steps and start
77 | * @error If no steps are provided
78 | * @param {Array} steps The form steps
79 | * @return {Promise} A promise which will be resolved when all steps are passed,
80 | * and rejected if the user cancel the multi step form.
81 | */
82 | this.start = function (steps) {
83 | if (!steps || !steps.length) {
84 | throw new Error('At least one step has to be defined');
85 | }
86 | // Initialise steps
87 | this.steps = steps;
88 | // Return promise
89 | return this.deferred.promise;
90 | };
91 |
92 | /**
93 | * @ngdoc method
94 | * @methodOf multiStepForm:multiStepForm
95 | *
96 | * @description Cancel this multi step form
97 | * @type {Array}
98 | */
99 | this.cancel = function () {
100 | this.deferred.reject('cancelled');
101 | };
102 |
103 | /**
104 | * @ngdoc method
105 | * @methodOf multiStepForm:multiStepForm
106 | *
107 | * @description Finish this multi step form (success)
108 | */
109 | this.finish = function () {
110 | this.deferred.resolve();
111 | };
112 |
113 | /**
114 | * @ngdoc method
115 | * @methodOf multiStepForm:multiStepForm
116 | *
117 | * @description Return the multi form current step
118 | * @return {Number} The active step index (starting at 1)
119 | */
120 | this.getActiveIndex = function () {
121 | return this.activeIndex;
122 | };
123 |
124 | /**
125 | * @ngdoc method
126 | * @methodOf multiStepForm:multiStepForm
127 | *
128 | * @description Initialise the multi step form instance with
129 | * an initial index
130 | * @param {Number} step The index to start with
131 | */
132 | this.setInitialIndex = function (initialStep) {
133 | var searchIndex = void 0;
134 | // Initial step in markup has the priority
135 | // to override any manually entered URL
136 | if (angular.isDefined(initialStep)) {
137 | return this.setActiveIndex(initialStep);
138 | }
139 | // Otherwise use search ID if applicable
140 | if (this.searchId) {
141 | searchIndex = parseInt($location.search()[this.searchId]);
142 | if (!isNaN(searchIndex)) {
143 | return this.setActiveIndex(searchIndex);
144 | }
145 | }
146 | // Otherwise set to 1
147 | this.setActiveIndex(1);
148 | };
149 |
150 | /**
151 | * @ngdoc method
152 | * @methodOf multiStepForm:multiStepForm
153 | *
154 | * @description Set the current step to the provided value and notify
155 | * @param {Number} step The step index (starting at 1)
156 | */
157 | this.setActiveIndex = function (step) {
158 | if (this.searchId) {
159 | // Update $location
160 | if (this.activeIndex) {
161 | $location.search(this.searchId, step);
162 | } else {
163 | // Replace current one
164 | $location.search(this.searchId, step).replace();
165 | }
166 | }
167 | // Notify deferred object
168 | this.deferred.notify({
169 | newStep: step,
170 | oldStep: this.activeIndex
171 | });
172 | // Update activeIndex
173 | this.activeIndex = step;
174 | };
175 |
176 | /**
177 | * @ngdoc method
178 | * @methodOf multiStepForm:multiStepForm
179 | *
180 | * @description Return the active form step object
181 | * @return {FormStep} The active form step
182 | */
183 | this.getActiveStep = function () {
184 | if (this.activeIndex) {
185 | return this.steps[this.activeIndex - 1];
186 | }
187 | };
188 |
189 | /**
190 | * @ngdoc method
191 | * @methodOf multiStepForm:multiStepForm
192 | *
193 | * @description Check if the current step is the first step
194 | * @return {Boolean} Whether or not the current step is the first step
195 | */
196 | this.isFirst = function () {
197 | return this.activeIndex === 1;
198 | };
199 |
200 | /**
201 | * @ngdoc method
202 | * @methodOf multiStepForm:multiStepForm
203 | *
204 | * @description Check if the current step is the last step
205 | * @return {Boolean} Whether or not the current step is the last step
206 | */
207 | this.isLast = function () {
208 | return this.activeIndex === this.steps.length;
209 | };
210 |
211 | /**
212 | * @ngdoc method
213 | * @methodOf multiStepForm:multiStepForm
214 | *
215 | * @description Go to the next step, if not the last step
216 | */
217 | this.nextStep = function () {
218 | if (!this.isLast()) {
219 | this.setActiveIndex(this.activeIndex + 1);
220 | }
221 | };
222 |
223 | /**
224 | * @ngdoc method
225 | * @methodOf multiStepForm:multiStepForm
226 | *
227 | * @description Go to the next step, if not the first step
228 | */
229 | this.previousStep = function () {
230 | if (!this.isFirst()) {
231 | this.setActiveIndex(this.activeIndex - 1);
232 | }
233 | };
234 |
235 | /**
236 | * @ngdoc method
237 | * @methodOf multiStepForm:multiStepForm
238 | *
239 | * @description Go to the next step, if not the first step
240 | */
241 | this.setValidity = function (isValid, stepIndex) {
242 | var step = this.steps[(stepIndex || this.activeIndex) - 1];
243 |
244 | if (step) {
245 | step.valid = isValid;
246 | }
247 | };
248 |
249 | /**
250 | * @ngdoc method
251 | * @methodOf multiStepForm:multiStepForm
252 | *
253 | * @description Augment a scope with useful methods from this object
254 | * @param {Object} scope The scope to augment
255 | */
256 | this.augmentScope = function (scope) {
257 | var _this2 = this;
258 |
259 | ['cancel', 'finish', 'getActiveIndex', 'setActiveIndex', 'getActiveStep', 'getSteps', 'nextStep', 'previousStep', 'isFirst', 'isLast', 'setValidity'].forEach(function (method) {
260 | scope['$' + method] = _this2[method].bind(_this2);
261 | });
262 | };
263 | }
264 |
265 | return function multiStepFormProvider(searchId) {
266 | return new MultiFormStep(searchId);
267 | };
268 | }
--------------------------------------------------------------------------------
/dist/umd/angular-multi-step-form.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('angular')) :
3 | typeof define === 'function' && define.amd ? define('angularMultiStepForm', ['angular'], factory) :
4 | (global.angularMultiStepForm = factory(global.angular));
5 | }(this, function (angular$1) { 'use strict';
6 |
7 | angular$1 = 'default' in angular$1 ? angular$1['default'] : angular$1;
8 |
9 | var multiStepContainer = ['$animate', '$q', '$controller', 'multiStepForm', 'FormStep', 'formStepElement', multiStepContainer$1];
10 |
11 | /**
12 | * @ngdoc directive
13 | * @name multiStepForm:multiStepContainer
14 | *
15 | * @requires $scope
16 | * @requires $q
17 | * @requires $controller
18 | * @requires multiStepForm:multiStepForm
19 | * @requires multiStepForm:FormStep
20 | * @requires multiStepForm:formStepElement
21 | *
22 | * @scope
23 | * @description Multi step directive (overall container)
24 | */
25 | function multiStepContainer$1($animate, $q, $controller, multiStepForm, FormStep, formStepElement) {
26 | return {
27 | restrict: 'EA',
28 | scope: true,
29 | /**
30 | * @ngdoc controller
31 | * @name multiStepForm:MultiStepFormCtrl
32 | *
33 | * @requires $scope
34 | * @requires multiStepForm:multiStepForm
35 | *
36 | * @description Controls a multi step form and track progress.
37 | */
38 | controller: ['$scope', function ($scope) {
39 | /**
40 | * @ngdoc function
41 | * @methodOf multiStepForm:MultiStepFormCtrl
42 | *
43 | * @description Register the step container element. This method
44 | * is invoked in the post link function of directive
45 | * {@link multiStepForm:formStepContainer formStepContainer}
46 | * @param {JQLite} elm The step form container
47 | */
48 | this.setStepContainer = function (elm) {
49 | this.stepContainer = elm;
50 | };
51 | }],
52 | link: {
53 | pre: function preLink(scope, element, attrs) {
54 | /**
55 | * @ngdoc property
56 | * @propertyOf multiStepForm:MultiStepFormContainer
57 | * @description The form steps
58 | * @type {FormStep[]}
59 | */
60 | scope.formSteps = scope.$eval(attrs.steps).map(function (step) {
61 | return new FormStep(step);
62 | });
63 |
64 | /**
65 | * @ngdoc property
66 | * @propertyOf multiStepForm:MultiStepFormContainer
67 | * @description The form step titles
68 | * @type {String[]}
69 | */
70 | scope.stepTitles = scope.formSteps.map(function (step) {
71 | return step.title;
72 | });
73 | },
74 | post: function postLink(scope, element, attrs, controller) {
75 | // Add .multi-step-container class
76 | element.addClass('multi-step-container');
77 | // Callbacks
78 | var onFinish = attrs.onFinish ? resolve(attrs.onFinish) : defaultResolve;
79 | var onCancel = attrs.onCancel ? resolve(attrs.onCancel) : defaultResolve;
80 | var onStepChange = attrs.onStepChange ? function () {
81 | return scope.$eval(attrs.onStepChange);
82 | } : angular$1.noop;
83 | // Step container (populated by child post link function)
84 | var stepContainer = controller.stepContainer;
85 | var multiStepFormInstance = multiStepForm(scope.$eval(attrs.searchId));
86 |
87 | // Augment scope
88 | multiStepFormInstance.augmentScope(scope);
89 |
90 | // Controller
91 | if (attrs.controller) {
92 | var customController = $controller(attrs.controller, {
93 | $scope: scope,
94 | $element: element,
95 | multiStepFormInstance: multiStepFormInstance
96 | });
97 | // controllerAs
98 | if (attrs.controllerAs) {
99 | scope[attrs.controllerAs] = customController;
100 | }
101 | }
102 |
103 | // Initial step
104 | var initialStep = scope.$eval(attrs.initialStep);
105 |
106 | var currentLeaveAnimation = void 0,
107 | currentEnterAnimation = void 0,
108 | currentStepScope = void 0,
109 | currentStepElement = void 0,
110 | isDeferredResolved = false;
111 |
112 | // Resolve any outstanding promises on destroy
113 | scope.$on('$destroy', function () {
114 | if (!isDeferredResolved) {
115 | isDeferredResolved = true;
116 | multiStepFormInstance.deferred.reject();
117 | }
118 | });
119 |
120 | // Initialise and start the multi step form
121 | multiStepFormInstance.start(scope.formSteps).then(onFinish, onCancel, function (data) {
122 | var step = data.newStep;
123 | var previousStep = data.oldStep;
124 | var direction = angular$1.isDefined(previousStep) ? step < previousStep ? 'step-backward' : 'step-forward' : 'step-initial';
125 |
126 | var formStep = scope.formSteps[step - 1];
127 | // Create new step element (promise)
128 | var newStepElement = formStepElement(formStep, multiStepFormInstance, scope);
129 |
130 | // Add direction class to the parent container;
131 | stepContainer.removeClass('step-forward step-backward step-initial').addClass(direction);
132 |
133 | // Cancel current leave animation if any
134 | if (currentLeaveAnimation) {
135 | $animate.cancel(currentLeaveAnimation);
136 | }
137 | // Cancel current enter animation if any
138 | if (currentEnterAnimation) {
139 | $animate.cancel(currentEnterAnimation);
140 | }
141 | // Destroy current scope
142 | if (currentStepScope) {
143 | currentStepScope.$destroy();
144 | }
145 | // Leave current step if any
146 | if (currentStepElement) {
147 | currentLeaveAnimation = $animate.leave(currentStepElement);
148 | }
149 | // Enter new step when new step element is ready
150 | newStepElement.then(function (step) {
151 | onStepChange();
152 | currentStepScope = step.scope;
153 | currentStepElement = step.element;
154 | currentStepElement.scrollTop = 0;
155 | stepContainer.scrollTop = 0;
156 | currentEnterAnimation = $animate.enter(currentStepElement, stepContainer);
157 | }, function () {
158 | throw new Error('Could not load step ' + step);
159 | });
160 | });
161 |
162 | // Initialise currentStep
163 | multiStepFormInstance.setInitialIndex(initialStep);
164 |
165 | // Default resolution function
166 | function defaultResolve() {
167 | $animate.leave(element);
168 | isDeferredResolved = true;
169 | scope.$destroy();
170 | }
171 |
172 | // On promise resolution
173 | function resolve(fn) {
174 | return function () {
175 | isDeferredResolved = true;
176 | scope.$eval(fn);
177 | };
178 | }
179 | }
180 | }
181 | };
182 | }
183 |
184 | var formStepValidity = ['$parse', formStepValidity$1];
185 |
186 | /**
187 | * @ngdoc directive
188 | * @name multiStepForm:formStepValidity
189 | *
190 | * @restrict A
191 | * @description Notify the multi step form instance of a change of validity of a step.
192 | * This directive can be used on a form element or within a form.
193 | */
194 | function formStepValidity$1($parse) {
195 | return {
196 | restrict: 'A',
197 | require: '^form',
198 | link: function postLink(scope, element, attrs, formCtrl) {
199 | // The callback to call when a change of validity
200 | // is detected
201 | var validtyChangeCallback = attrs.formStepValidity ? $parse(attrs.formStepValidity).bind(scope, scope) : scope.$setValidity;
202 |
203 | // Watch the form validity
204 | scope.$watch(function () {
205 | return formCtrl.$valid;
206 | }, function (val) {
207 | // Check if defined
208 | if (angular$1.isDefined(val)) {
209 | validtyChangeCallback(val);
210 | }
211 | });
212 | }
213 | };
214 | }
215 |
216 | /**
217 | * @ngdoc directive
218 | * @name multiStepForm:stepContainer
219 | *
220 | * @requires multiStepForm:stepContainer
221 | *
222 | * @restrict A
223 | * @description The container for form steps. It registers itself with the multi step container.
224 | * {@link multiStepForm:multiStepContainer multiStepContainer}
225 | */
226 | function stepContainer() {
227 | return {
228 | restrict: 'EA',
229 | require: '^^multiStepContainer',
230 | scope: false,
231 | link: function postLink(scope, element, attrs, multiStepCtrl) {
232 | element.addClass('multi-step-body');
233 | multiStepCtrl.setStepContainer(element);
234 | }
235 | };
236 | }
237 |
238 | var formStepElement = ['$compile', '$controller', '$http', '$injector', '$q', '$templateCache', formStepElement$1];
239 |
240 | /**
241 | * @ngdoc function
242 | * @name multiStepForm:formStepElement
243 | *
244 | * @requires $rootScope
245 | * @requires $controller
246 | *
247 | * @description A factory function for creating form step elements
248 | * (using controller, template and resolve)
249 | */
250 | function formStepElement$1($compile, $controller, $http, $injector, $q, $templateCache) {
251 | /**
252 | * Resolve the template of a form step
253 | * @param {FormStep} formStep The form step object
254 | * @return {Promise|String} A promise containing the template
255 | */
256 | function resolveTemplate(formStep) {
257 | if (formStep.template) {
258 | // If function or array, use $injector to get template value
259 | return angular$1.isFunction(formStep.template) || angular$1.isArray(formStep.template) ? $injector.$invoke(formStep.template) : formStep.template;
260 | }
261 | // Use templateUrl
262 | var templateUrl =
263 | // If function or array, use $injector to get templateUrl value
264 | angular$1.isFunction(formStep.templateUrl) || angular$1.isArray(formStep.templateUrl) ? $injector.$invoke(formStep.templateUrl) : formStep.templateUrl;
265 | // Request templateUrl using $templateCache
266 | return $http.get(templateUrl, { cache: $templateCache });
267 | }
268 |
269 | /**
270 | * Create a new scope with the multiStepContainer being the parent scope.
271 | * augmented with multi step form control methods.
272 | * @param {FormStep} formStep The form step object
273 | * @param {FormStep} formStep The form step object
274 | * @return {Object} The form step scope
275 | */
276 | function getScope(scope, formStep, multiStepFormInstance) {
277 | var stepScope = scope.$new(formStep.isolatedScope);
278 | // Augment scope with multi step form instance methods
279 | multiStepFormInstance.augmentScope(stepScope);
280 |
281 | return stepScope;
282 | }
283 |
284 | /**
285 | * Create a form step element, compiled with controller and dependencies resolved
286 | *
287 | * @param {FormStep} formStep The form step object
288 | * @param {Object} multiStepFormInstance The multi step form instance
289 | * @param {Object} multiStepFormScope The scope instance of the multi step form
290 | * @return {Promise} A promise containing the form step element
291 | */
292 | return function formStepElementFactory(formStep, multiStepFormInstance, multiStepFormScope) {
293 | var formStepElement = angular$1.element('
').addClass('form-step');
294 |
295 | var controller = void 0,
296 | template = void 0,
297 | promisesHash = {};
298 |
299 | // Get template
300 | promisesHash.$template = resolveTemplate(formStep);
301 |
302 | // Get resolve
303 | angular$1.forEach(formStep.resolve, function (resolveVal, resolveName) {
304 | promisesHash[resolveName] =
305 | // angular.isString(resolveVal) ?
306 | // $injector.get(resolveVal) :
307 | $injector.invoke(resolveVal);
308 | });
309 |
310 | // After all locals are resolved (template and "resolves") //
311 | return $q.all(promisesHash).then(function (locals) {
312 | // Extend formStep locals with resolved locals
313 | locals = angular$1.extend({}, formStep.locals, locals);
314 | // Load template inside element
315 | locals.$template = locals.$template.data || locals.$template;
316 | formStepElement.html(locals.$template);
317 | // Create scope
318 | var formStepScope = getScope(multiStepFormScope, formStep, multiStepFormInstance);
319 |
320 | if (formStep.controller) {
321 | // Create form step scope
322 | locals.$scope = formStepScope;
323 | // Add multi step form service instance to local injectables
324 | locals.multiStepFormInstance = multiStepFormInstance;
325 | // Add multi step form scope to local injectables if isolated
326 | if (formStep.isolatedScope) {
327 | locals.multiStepFormScope = multiStepFormScope;
328 | }
329 | // Instanciate controller
330 | controller = $controller(formStep.controller, locals);
331 | // controllerAs
332 | if (formStep.controllerAs) {
333 | formStepScope[formStep.controllerAs] = controller;
334 | }
335 | formStepElement.data('$stepController', controller);
336 | // formStepElement.children().data('$ngControllerController', controller);
337 | }
338 |
339 | // Compile form step element and link with scope
340 | $compile(formStepElement)(formStepScope);
341 |
342 | // Return element and scope
343 | return {
344 | element: formStepElement,
345 | scope: formStepScope
346 | };
347 | });
348 | };
349 | }
350 |
351 | /**
352 | * @ngdoc object
353 | * @name multiStepForm:FormStep
354 | *
355 | * @description A constructor for creating form steps
356 | * @error If no template or templateUrl properties are supplied
357 | */
358 | function FormStep() {
359 | return function FormStep(config) {
360 | if (!config.template && !config.templateUrl) {
361 | throw new Error('Either template or templateUrl properties have to be provided for' + ' multi step form' + config.title);
362 | }
363 |
364 | /**
365 | * @ngdoc property
366 | * @propertyOf multiStepForm:FormStep
367 | *
368 | * @description The form step title
369 | * @type {String}
370 | */
371 | this.title = config.title;
372 |
373 | /**
374 | * @ngdoc property
375 | * @propertyOf multiStepForm:FormStep
376 | *
377 | * @description The form step additional data
378 | * @type {Object}
379 | */
380 | this.data = config.data || {};
381 |
382 | /**
383 | * @ngdoc property
384 | * @propertyOf multiStepForm:FormStep
385 | *
386 | * @description The form step controller
387 | * @type {String|Function|Array}
388 | */
389 | this.controller = config.controller;
390 |
391 | /**
392 | * @ngdoc property
393 | * @propertyOf multiStepForm:FormStep
394 | *
395 | * @description An identifier name for a reference to the controller
396 | * @type {String}
397 | */
398 | this.controllerAs = config.controllerAs;
399 |
400 | /**
401 | * @ngdoc property
402 | * @propertyOf multiStepForm:FormStep
403 | *
404 | * @description The form step template
405 | * @type {String}
406 | */
407 | this.template = config.template;
408 |
409 | /**
410 | * @ngdoc property
411 | * @propertyOf multiStepForm:FormStep
412 | *
413 | * @description The form step template URL
414 | * @type {String}
415 | */
416 | this.templateUrl = config.templateUrl;
417 |
418 | /**
419 | * Whether or not the form step should have an isolated scope
420 | * @type {Boolean}
421 | */
422 | this.isolatedScope = config.isolatedScope || false;
423 |
424 | /**
425 | * @ngdoc property
426 | * @propertyOf multiStepForm:resolve
427 | *
428 | * @description The form step resolve map (same use than for routes)
429 | * @type {Object}
430 | */
431 | this.resolve = config.resolve || {};
432 |
433 | /**
434 | * @ngdoc property
435 | * @propertyOf multiStepForm:resolve
436 | *
437 | * @description The form step locals map (same than resolve but for non deferred values)
438 | * Note: resolve also works with non deferred values
439 | * @type {Object}
440 | */
441 | this.locals = config.locals || {};
442 |
443 | /**
444 | * @ngdoc property
445 | * @propertyOf multiStepForm:FormStep
446 | *
447 | * @description Whether or not this form step contains a form
448 | * @type {Boolean}
449 | */
450 | this.hasForm = config.hasForm || false;
451 |
452 | /**
453 | * @ngdoc property
454 | * @propertyOf multiStepForm:FormStep
455 | *
456 | * @description Whether or not this form step is valid.
457 | * Form validity can been fed back using a specific directive.
458 | * {@link multiStepForm:formStepValidity formStepValidity}
459 | * @type {Boolean}
460 | */
461 | this.valid = false;
462 |
463 | /**
464 | * @ngdoc property
465 | * @propertyOf multiStepForm:FormStep
466 | *
467 | * @description Whether or not this step has been visited
468 | * (i.e. the user has moved to the next step)
469 | * @type {Boolean}
470 | */
471 | this.visited = false;
472 | };
473 | }
474 |
475 | var multiStepForm = ['$q', '$location', '$rootScope', multiStepForm$1];
476 |
477 | /**
478 | * @ngdoc function
479 | * @name multiStepForm:multiStepForm
480 | *
481 | * @requires $q
482 | * @requires multiStepForm:FormStep
483 | *
484 | * @description A service returning an instance per multi step form.
485 | * The instance of the service is injected in each step controller.
486 | */
487 | function multiStepForm$1($q, $location, $rootScope) {
488 | function MultiFormStep(searchId) {
489 | var _this = this;
490 |
491 | /**
492 | * @ngdoc property
493 | * @propertyOf multiStepForm:multiStepForm
494 | *
495 | * @description The location search property name to store the active step index.
496 | * @type {String}
497 | */
498 | this.searchId = searchId;
499 | // If the search id is defined,
500 | if (angular.isDefined(searchId)) {
501 | $rootScope.$on('$locationChangeSuccess', function (event) {
502 | var searchIndex = parseInt($location.search()[_this.searchId]);
503 |
504 | if (!isNaN(searchIndex) && _this.activeIndex !== searchIndex) {
505 | // Synchronise
506 | _this.setActiveIndex(parseInt(searchIndex));
507 | }
508 | });
509 | }
510 |
511 | /**
512 | * @ngdoc property
513 | * @propertyOf multiStepForm:multiStepForm
514 | *
515 | * @description The form steps
516 | * @type {Array}
517 | */
518 | this.steps = [];
519 |
520 | /**
521 | * @ngdoc property
522 | * @propertyOf multiStepForm:multiStepForm
523 | *
524 | * @description Return the form steps
525 | * @return {Array}
526 | */
527 | this.getSteps = function () {
528 | return this.steps;
529 | };
530 |
531 | /**
532 | * @ngdoc property
533 | * @propertyOf multiStepForm:multiStepForm
534 | *
535 | * @description The multi-step form deferred object
536 | * @type {Deferred}
537 | */
538 | this.deferred = $q.defer();
539 |
540 | /**
541 | * @ngdoc method
542 | * @methodOf multiStepForm:multiStepForm
543 | *
544 | * @description Initialise the form steps and start
545 | * @error If no steps are provided
546 | * @param {Array} steps The form steps
547 | * @return {Promise} A promise which will be resolved when all steps are passed,
548 | * and rejected if the user cancel the multi step form.
549 | */
550 | this.start = function (steps) {
551 | if (!steps || !steps.length) {
552 | throw new Error('At least one step has to be defined');
553 | }
554 | // Initialise steps
555 | this.steps = steps;
556 | // Return promise
557 | return this.deferred.promise;
558 | };
559 |
560 | /**
561 | * @ngdoc method
562 | * @methodOf multiStepForm:multiStepForm
563 | *
564 | * @description Cancel this multi step form
565 | * @type {Array}
566 | */
567 | this.cancel = function () {
568 | this.deferred.reject('cancelled');
569 | };
570 |
571 | /**
572 | * @ngdoc method
573 | * @methodOf multiStepForm:multiStepForm
574 | *
575 | * @description Finish this multi step form (success)
576 | */
577 | this.finish = function () {
578 | this.deferred.resolve();
579 | };
580 |
581 | /**
582 | * @ngdoc method
583 | * @methodOf multiStepForm:multiStepForm
584 | *
585 | * @description Return the multi form current step
586 | * @return {Number} The active step index (starting at 1)
587 | */
588 | this.getActiveIndex = function () {
589 | return this.activeIndex;
590 | };
591 |
592 | /**
593 | * @ngdoc method
594 | * @methodOf multiStepForm:multiStepForm
595 | *
596 | * @description Initialise the multi step form instance with
597 | * an initial index
598 | * @param {Number} step The index to start with
599 | */
600 | this.setInitialIndex = function (initialStep) {
601 | var searchIndex = void 0;
602 | // Initial step in markup has the priority
603 | // to override any manually entered URL
604 | if (angular.isDefined(initialStep)) {
605 | return this.setActiveIndex(initialStep);
606 | }
607 | // Otherwise use search ID if applicable
608 | if (this.searchId) {
609 | searchIndex = parseInt($location.search()[this.searchId]);
610 | if (!isNaN(searchIndex)) {
611 | return this.setActiveIndex(searchIndex);
612 | }
613 | }
614 | // Otherwise set to 1
615 | this.setActiveIndex(1);
616 | };
617 |
618 | /**
619 | * @ngdoc method
620 | * @methodOf multiStepForm:multiStepForm
621 | *
622 | * @description Set the current step to the provided value and notify
623 | * @param {Number} step The step index (starting at 1)
624 | */
625 | this.setActiveIndex = function (step) {
626 | if (this.searchId) {
627 | // Update $location
628 | if (this.activeIndex) {
629 | $location.search(this.searchId, step);
630 | } else {
631 | // Replace current one
632 | $location.search(this.searchId, step).replace();
633 | }
634 | }
635 | // Notify deferred object
636 | this.deferred.notify({
637 | newStep: step,
638 | oldStep: this.activeIndex
639 | });
640 | // Update activeIndex
641 | this.activeIndex = step;
642 | };
643 |
644 | /**
645 | * @ngdoc method
646 | * @methodOf multiStepForm:multiStepForm
647 | *
648 | * @description Return the active form step object
649 | * @return {FormStep} The active form step
650 | */
651 | this.getActiveStep = function () {
652 | if (this.activeIndex) {
653 | return this.steps[this.activeIndex - 1];
654 | }
655 | };
656 |
657 | /**
658 | * @ngdoc method
659 | * @methodOf multiStepForm:multiStepForm
660 | *
661 | * @description Check if the current step is the first step
662 | * @return {Boolean} Whether or not the current step is the first step
663 | */
664 | this.isFirst = function () {
665 | return this.activeIndex === 1;
666 | };
667 |
668 | /**
669 | * @ngdoc method
670 | * @methodOf multiStepForm:multiStepForm
671 | *
672 | * @description Check if the current step is the last step
673 | * @return {Boolean} Whether or not the current step is the last step
674 | */
675 | this.isLast = function () {
676 | return this.activeIndex === this.steps.length;
677 | };
678 |
679 | /**
680 | * @ngdoc method
681 | * @methodOf multiStepForm:multiStepForm
682 | *
683 | * @description Go to the next step, if not the last step
684 | */
685 | this.nextStep = function () {
686 | if (!this.isLast()) {
687 | this.setActiveIndex(this.activeIndex + 1);
688 | }
689 | };
690 |
691 | /**
692 | * @ngdoc method
693 | * @methodOf multiStepForm:multiStepForm
694 | *
695 | * @description Go to the next step, if not the first step
696 | */
697 | this.previousStep = function () {
698 | if (!this.isFirst()) {
699 | this.setActiveIndex(this.activeIndex - 1);
700 | }
701 | };
702 |
703 | /**
704 | * @ngdoc method
705 | * @methodOf multiStepForm:multiStepForm
706 | *
707 | * @description Go to the next step, if not the first step
708 | */
709 | this.setValidity = function (isValid, stepIndex) {
710 | var step = this.steps[(stepIndex || this.activeIndex) - 1];
711 |
712 | if (step) {
713 | step.valid = isValid;
714 | }
715 | };
716 |
717 | /**
718 | * @ngdoc method
719 | * @methodOf multiStepForm:multiStepForm
720 | *
721 | * @description Augment a scope with useful methods from this object
722 | * @param {Object} scope The scope to augment
723 | */
724 | this.augmentScope = function (scope) {
725 | var _this2 = this;
726 |
727 | ['cancel', 'finish', 'getActiveIndex', 'setActiveIndex', 'getActiveStep', 'getSteps', 'nextStep', 'previousStep', 'isFirst', 'isLast', 'setValidity'].forEach(function (method) {
728 | scope['$' + method] = _this2[method].bind(_this2);
729 | });
730 | };
731 | }
732 |
733 | return function multiStepFormProvider(searchId) {
734 | return new MultiFormStep(searchId);
735 | };
736 | }
737 |
738 | /**
739 | * @ngdoc module
740 | * @name multiStepForm
741 | */
742 | var multiStepFormModule = angular$1.module('multiStepForm', [/*'ngAnimate'*/]);
743 |
744 | multiStepFormModule.directive('formStepValidity', formStepValidity).directive('multiStepContainer', multiStepContainer).directive('stepContainer', stepContainer).factory('formStepElement', formStepElement).factory('FormStep', FormStep).factory('multiStepForm', multiStepForm);
745 |
746 | return multiStepFormModule;
747 |
748 | }));
--------------------------------------------------------------------------------
/dist/umd/angular-multi-step-form.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("angular")):"function"==typeof define&&define.amd?define("angularMultiStepForm",["angular"],e):t.angularMultiStepForm=e(t.angular)}(this,function(t){"use strict";function e(e,i,n,r,s,o){return{restrict:"EA",scope:!0,controller:["$scope",function(t){this.setStepContainer=function(t){this.stepContainer=t}}],link:{pre:function(t,e,i){t.formSteps=t.$eval(i.steps).map(function(t){return new s(t)}),t.stepTitles=t.formSteps.map(function(t){return t.title})},post:function(i,s,a,c){function l(){e.leave(s),g=!0,i.$destroy()}function p(t){return function(){g=!0,i.$eval(t)}}s.addClass("multi-step-container");var d=a.onFinish?p(a.onFinish):l,u=a.onCancel?p(a.onCancel):l,h=a.onStepChange?function(){return i.$eval(a.onStepChange)}:t.noop,f=c.stepContainer,m=r(i.$eval(a.searchId));if(m.augmentScope(i),a.controller){var v=n(a.controller,{$scope:i,$element:s,multiStepFormInstance:m});a.controllerAs&&(i[a.controllerAs]=v)}var S=i.$eval(a.initialStep),$=void 0,I=void 0,x=void 0,A=void 0,g=!1;i.$on("$destroy",function(){g||(g=!0,m.deferred.reject())}),m.start(i.formSteps).then(d,u,function(n){var r=n.newStep,s=n.oldStep,a=t.isDefined(s)?r
").addClass("form-step"),d=void 0,u={};return u.$template=a(n),t.forEach(n.resolve,function(t,e){u[e]=r.invoke(t)}),s.all(u).then(function(r){r=t.extend({},n.locals,r),r.$template=r.$template.data||r.$template,p.html(r.$template);var s=c(l,n,o);return n.controller&&(r.$scope=s,r.multiStepFormInstance=o,n.isolatedScope&&(r.multiStepFormScope=l),d=i(n.controller,r),n.controllerAs&&(s[n.controllerAs]=d),p.data("$stepController",d)),e(p)(s),{element:p,scope:s}})}}function s(){return function(t){if(!t.template&&!t.templateUrl)throw new Error("Either template or templateUrl properties have to be provided for multi step form"+t.title);this.title=t.title,this.data=t.data||{},this.controller=t.controller,this.controllerAs=t.controllerAs,this.template=t.template,this.templateUrl=t.templateUrl,this.isolatedScope=t.isolatedScope||!1,this.resolve=t.resolve||{},this.locals=t.locals||{},this.hasForm=t.hasForm||!1,this.valid=!1,this.visited=!1}}function o(t,e,i){function n(n){var r=this;this.searchId=n,angular.isDefined(n)&&i.$on("$locationChangeSuccess",function(t){var i=parseInt(e.search()[r.searchId]);isNaN(i)||r.activeIndex===i||r.setActiveIndex(parseInt(i))}),this.steps=[],this.getSteps=function(){return this.steps},this.deferred=t.defer(),this.start=function(t){if(!t||!t.length)throw new Error("At least one step has to be defined");return this.steps=t,this.deferred.promise},this.cancel=function(){this.deferred.reject("cancelled")},this.finish=function(){this.deferred.resolve()},this.getActiveIndex=function(){return this.activeIndex},this.setInitialIndex=function(t){var i=void 0;return angular.isDefined(t)?this.setActiveIndex(t):this.searchId&&(i=parseInt(e.search()[this.searchId]),!isNaN(i))?this.setActiveIndex(i):void this.setActiveIndex(1)},this.setActiveIndex=function(t){this.searchId&&(this.activeIndex?e.search(this.searchId,t):e.search(this.searchId,t).replace()),this.deferred.notify({newStep:t,oldStep:this.activeIndex}),this.activeIndex=t},this.getActiveStep=function(){if(this.activeIndex)return this.steps[this.activeIndex-1]},this.isFirst=function(){return 1===this.activeIndex},this.isLast=function(){return this.activeIndex===this.steps.length},this.nextStep=function(){this.isLast()||this.setActiveIndex(this.activeIndex+1)},this.previousStep=function(){this.isFirst()||this.setActiveIndex(this.activeIndex-1)},this.setValidity=function(t,e){var i=this.steps[(e||this.activeIndex)-1];i&&(i.valid=t)},this.augmentScope=function(t){var e=this;["cancel","finish","getActiveIndex","setActiveIndex","getActiveStep","getSteps","nextStep","previousStep","isFirst","isLast","setValidity"].forEach(function(i){t["$"+i]=e[i].bind(e)})}}return function(t){return new n(t)}}t="default"in t?t.default:t;var a=["$animate","$q","$controller","multiStepForm","FormStep","formStepElement",e],c=["$parse",i],l=["$compile","$controller","$http","$injector","$q","$templateCache",r],p=["$q","$location","$rootScope",o],d=t.module("multiStepForm",[]);return d.directive("formStepValidity",c).directive("multiStepContainer",a).directive("stepContainer",n).factory("formStepElement",l).factory("FormStep",s).factory("multiStepForm",p),d});
--------------------------------------------------------------------------------
/docs/advanced-guide.md:
--------------------------------------------------------------------------------
1 | # How it works
2 |
3 | The `multiStepContainer` directive is the starting point. Each `multiStepContainer` has its own instance
4 | of a `MultiStepForm` object (provided by the `multiStepForm` factory) which provide controls such has: start,
5 | nextStep, previousStep, finish, cancel, etc...
6 |
7 | Form step elements are created with the help of the `formStepElement` factory. Each form step controller
8 | (if provided) gets the MultiStepForm instance of its container available to inject (dependency is called
9 | `multiStepFormInstance`). In addition, each form step scope is augmented with the following functions
10 | from its multiStepFormInstance (with a `$` prefix): `MultiStepForm.cancel()`, `MultiStepForm.finish()`,
11 | `MultiStepForm.getActiveIndex()`, `MultiStepForm.setActiveIndex()`, `MultiStepForm.getActiveStep()`,
12 | `MultiStepForm.nextStep()`, `MultiStepForm.previousStep()` and `MultiStepForm.setValidity()`.
13 |
14 | Each MultiStepForm object has a start method which is invoked by the multiStepContainer directive
15 | in its postLink function. It returns a promise for avoiding an inversion of control:
16 |
17 | * If the `MultiStepForm.cancel()` method is called, the promise is rejected and the onCancel
18 | callback is executed.
19 | * If the `MultiStepForm.finish()` method is called, the promise is resolved and the onFinish
20 | callback is executed.
21 | * The promise receives a notification each time there is a step change. The current form step
22 | element is destroyed and a new one is created.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/configuring-steps.md:
--------------------------------------------------------------------------------
1 | # Configuring steps
2 |
3 | ## Step properties
4 |
5 | Each step is defined with the following properties,
6 | in the same way routes or states are defined in an AngularJS application:
7 |
8 | * `controller` (optional)
9 | * `controllerAs`: an identifier name for a reference to the controller in scope
10 | * `template` or `templateUrl` property (required)
11 | * `resolve`
12 | * `locals` (like resolve but a more simple map of key-value pairs)
13 | * `title`
14 | * `hasForm`: whether or not a step contains a form. For example, a confirm or review
15 | step might not contain a form. This boolean has no influence on how directives behave.
16 | * `data`: an object containing custom data
17 | * `isolatedScope`: whether or not a form step form should be isolated. If isolated,
18 | the form step scope has still its multiFormContainer as a parent. If isolated,
19 | the multiStepForm's reference will be available to be injected in the form step's
20 | controller (`multiStepScope`). Default to false.
21 |
22 | ## Additional step controller dependencies
23 |
24 | The `formStepElement` factory is responsible for creating step elements, instantiating their controllers
25 | and compiling their contents. When a controller is instantiated, two extra dependencies are available
26 | to locally inject:
27 |
28 | * `multiStepInstance`: the current instance of `MultiFormStep`.
29 | * `multiStepScope`: the `multiStepContainer` directive scope, if the step's scope is isolated.
30 |
--------------------------------------------------------------------------------
/docs/migrating-to-1.1.x.md:
--------------------------------------------------------------------------------
1 | # Migrating from 1.0.x to 1.1.x
2 |
3 | 1.1.x gives you now the choice to have a header AND a footer, also provides full control over HTML layout and structure.
4 |
5 | ### Steps to follow
6 |
7 | - Add `step-container` in `multi-step-container` (see below)
8 | - Wrap your headers and / or footers in `` and ``
9 | - Remove `use-footer` attribute
10 |
11 | ### Note
12 |
13 | - The `multi-step-container` and `step-container` directives can be used as elements or attributes, leaving you to decide on the semantics of your HTML.
14 | - If a `step-container` is not supplied, an error will be thrown
15 |
16 | ## Differences between 1.0.x and 1.1.x
17 |
18 | ### With 1.1+
19 |
20 | There is now no transclusion happening anymore, leaving you full control on markup structure.
21 |
22 | ```html
23 |
24 | This my header content
25 |
26 |
27 |
28 | This is my footer content
29 |
30 | ```
31 |
32 | Will result in:
33 |
34 | ```html
35 |
36 | This my header content
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | This my footer content
45 |
46 | ```
47 |
48 | ### With 1.0.x
49 |
50 | Prior to 1.1, the content of `multi-step-container` was transcluded either in a header element or footer element (if attribute `useFooter` was defined).
51 |
52 | ```html
53 |
54 | This my header content
55 |
56 | ```
57 |
58 | Would result in:
59 |
60 | ```html
61 |
62 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | ```
73 |
--------------------------------------------------------------------------------
/docs/multi-step-container.md:
--------------------------------------------------------------------------------
1 | # The `multiStepContainer` directive
2 |
3 | ## Attributes
4 |
5 | ```html
6 |
12 |
13 |
14 | ```
15 |
16 | `steps` _angular expression_
17 | The list of steps.
18 |
19 | `initialStep` _angular expression_
20 | The starting step.
21 |
22 | `searchId` _angular expression_
23 | The name of the search parameter to use (enables browser navigation).
24 |
25 | `onFinish` _angular expression_
26 | The expression to execute if the multi-step form is finished.
27 |
28 | `onCancel` _angular expression_
29 | The expression to execute if the multi-step form is cancelled.
30 |
31 | `onStepChange` _angular expression_
32 | The expression to execute on a step change.
33 |
34 | `controller` _string_
35 | The name of a controller. `multiStepFormInstance` can be injected.
36 |
37 | `controllerAs` _string_
38 | For your custom controller.
39 |
40 | ## Class names
41 |
42 | The following class names are added:
43 | - `.multi-step-container` to the top directive element
44 | - `.multi-step-body` to the `stepContainer` directive element
45 | - `.form-step` to the step element
46 |
47 | ```html
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | ```
56 |
--------------------------------------------------------------------------------
/docs/multi-step-instance.md:
--------------------------------------------------------------------------------
1 | # Multi-step form instance object
2 |
3 | For each multi-step form rendered, there is an associated Object which is an instance of `MultiFormStep`.
4 |
5 | It can be injected in step controllers: `multiStepFormInstance`
6 |
7 | ```javascript
8 | myApp.controller('MyStepCtrl', [
9 | '$scope',
10 | 'multiStepFormInstance',
11 | function ($scope, multiStepFormInstance) {
12 | /* ... */
13 | }
14 | ])
15 | ```
16 |
17 | ## API
18 |
19 |
20 | `multiStepFormInstance.steps`
21 | _Array[FormSteps]_: The list of configured steps.
22 |
23 |
24 | `multiStepFormInstance.cancel()`
25 | Call the provided `onCancel` callback or destroy the multi-step form component.
26 |
27 |
28 | `multiStepFormInstance.finish()`
29 | Call the provided `onFinish` callback or destroy the multi-step form component.
30 |
31 |
32 | `multiStepFormInstance.getActiveIndex()`
33 | Return the current step index (starting at 1).
34 |
35 |
36 | `multiStepFormInstance.setActiveIndex(step)`
37 | Set the active step to the provided index (starting at 1).
38 |
39 |
40 | `multiStepFormInstance.getActiveStep()`
41 | Return the current step (`FormStep` Object).
42 |
43 |
44 | `multiStepFormInstance.getSteps()`
45 | Return the list of steps (`FormStep[]`)
46 |
47 |
48 | `multiStepFormInstance.isFirst()`
49 | Return true if the current step is the first step, false otherwise.
50 |
51 |
52 | `multiStepFormInstance.isLast()`
53 | Return true if the current step is the last step, false otherwise.
54 |
55 |
56 | `multiStepFormInstance.nextStep()`
57 | Navigate to the next step.
58 |
59 |
60 | `multiStepFormInstance.previousStep()`
61 | Navigate to the previous step.
62 |
63 |
64 | `multiStepFormInstance.setValidity(isValid[, stepIndex])`
65 | Set the validity of the current step (or of the specified step index).
66 |
--------------------------------------------------------------------------------
/docs/scopes.md:
--------------------------------------------------------------------------------
1 | # Scopes
2 |
3 | The `multiStepContainer` directive scope (the top directive to use) and step scopes are all augmented with the following methods:
4 |
5 | - `$isFirst()`: whether the current step is the first step or not
6 | - `$isLast()`: whether the current step is the last step or not
7 | - `$cancel()`: to cancel the current multi step form / wizard
8 | - `$finish()`: to finish the current multi step form / wizard
9 | - `$getActiveIndex()`: return the index of the active step (start with 1)
10 | - `$getActiveStep()`: return the active step object
11 | - `$getSteps()`: return the list of step objects
12 | - `$nextStep()`: go to the next step
13 | - `$previousStep()`: return to the previous step
14 | - `$setActiveIndex()`: navigate to a specific step
15 | - `$setValidity(validity, stepIndex)`: set validity of the specified step (current step if no given step)
16 |
17 | ## Isolated step scope
18 |
19 | By default, step scopes inherit from the top directive scope (which itself inherits from its view scope).
20 | You can configure steps to have an isolate scope by setting the `isolatedScope` property to true. An isolated scope
21 | will still be augmented with the helpers described above.
22 |
23 | When a step has an isolated scope, `multiStepFormScope` can be injected into your controller, in order to be
24 | able to access data from outside your step. See [saving data example](http://blog.reactandbethankful.com/angular-multi-step-form/#/saving-data).
25 |
--------------------------------------------------------------------------------
/docs/steps-lifecycle.md:
--------------------------------------------------------------------------------
1 | # Steps lifecycle
2 |
3 | ## Step transitions
4 |
5 | Animations can be performed using the following classes
6 |
7 | * `ng-enter` or `ng-leave` (see https://docs.angularjs.org/api/ngAnimate/service/$animate#enter
8 | and https://docs.angularjs.org/api/ngAnimate/service/$animate#leave)
9 | * A class is added to the `multi-step-body` element: `step-initial` for the first step being rendered,
10 | and `step-forward` or `step-backward` thereafter depending on the "direction".
11 |
12 | The entering and leaving animations are performed simutaneously (the leaving animation can be delayed
13 | if data or template need to be resolved). If you want separate animation, introduce a delay in your CSS.
14 | attribute for avoiding it.
15 |
16 | ## Navigation
17 |
18 | By supplying a search ID to the `multiStepContainer` directive, navigation will be enabled:
19 |
20 |
21 |
22 | The example above will add the search parameter 'id1' to your URL (`you_url?id1=`).
23 | When initialising a view, the initialStep property has the priority over an already defined
24 | search parameter, allowing you to having total control over a manually entered URL when starting
25 | the form.
26 |
27 | ## Callbacks
28 |
29 | Callbacks can be supplied to the multiStepContainer directive.
30 |
31 |
33 |
34 | * `onCancel` attribute: the provided callback will be invoked when the multi step form is cancelled
35 | * `onFinish` attribute: the provided callback will be invoked when the multi step form is finished
36 | * `onStepChange` attribute: the provided callback will be invoked on each step change
37 |
38 | By default, the directive element and its scope are destroyed on cancel and on finish. If you want
39 | to navigate away from the current view, you need to supply onCancel and onFinish callbacks.
40 |
--------------------------------------------------------------------------------
/karma.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | config.set({
3 | /**
4 | * From where to look for files, starting with the location of this file.
5 | */
6 | basePath: '',
7 | /**
8 | * This is the list of file patterns to load into the browser during testing.
9 | */
10 | files: [
11 | 'bower_components/angular/angular.js',
12 | 'dist/browser/angular-multi-step-form.js',
13 | // 'bower_components/angular-animate/angular-animate.js',
14 | 'bower_components/angular-mocks/angular-mocks.js',
15 | 'tests/helpers.js',
16 | 'tests/*.spec.js'
17 | ],
18 |
19 | frameworks: [ 'jasmine' ],
20 |
21 | plugins: [
22 | 'karma-jasmine',
23 | 'karma-chrome-launcher',
24 | 'karma-firefox-launcher',
25 | 'karma-coverage',
26 | 'karma-coveralls'
27 | ],
28 | /**
29 | * How to report, by default.
30 | */
31 | reporters: ['progress', 'coverage', 'coveralls'],
32 |
33 | coverageReporter: {
34 | dir: 'coverage',
35 | reporters: [
36 | {type: 'lcov'}
37 | ],
38 | },
39 |
40 | preprocessors: {
41 | 'src/**/*.js': ['coverage']
42 | },
43 |
44 | port: 9876,
45 | colors: true,
46 | singleRun: true,
47 | autoWatch: false,
48 | browsers: ['Chrome']
49 | });
50 | };
51 |
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-multi-step-form",
3 | "version": "1.3.0",
4 | "description": "An Angular module for creating wizards and multi step forms",
5 | "scripts": {
6 | "clean": "del dist",
7 | "build:cjs": "babel src --out-dir dist/commonjs",
8 | "build:umd": "rollup -c rollup.config.js --format umd && rollup -c rollup.config.js --format umd --uglify",
9 | "build:iife": "rollup -c rollup.config.js --format iife && rollup -c rollup.config.js --format iife --uglify",
10 | "build": "npm run build:cjs && npm run build:umd && npm run build:iife",
11 | "clog": "conventional-changelog -p angular -i CHANGELOG.md -w",
12 | "test": "karma start karma.config.js --single-run --browsers Firefox",
13 | "lint": "eslint src/**/*.js"
14 | },
15 | "main": "dist/commonjs",
16 | "devDependencies": {
17 | "babel-eslint": "^6.0.5",
18 | "babel-preset-es2015": "^6.9.0",
19 | "babel-preset-es2015-rollup": "^3.0.0",
20 | "conventional-changelog": "^1.1.0",
21 | "coveralls": "^2.11.9",
22 | "del": "^2.2.1",
23 | "eslint": "^2.13.1",
24 | "jasmine": "^2.6.0",
25 | "karma": "~0.13.22",
26 | "karma-chrome-launcher": "^1.0.1",
27 | "karma-coverage": "~1.0.0",
28 | "karma-coveralls": "^1.1.2",
29 | "karma-firefox-launcher": "^1.0.0",
30 | "karma-jasmine": "~1.0.2",
31 | "rollup": "^0.32.0",
32 | "rollup-plugin-babel": "^2.5.1",
33 | "rollup-plugin-uglify": "^1.0.0",
34 | "yargs": "^4.7.1"
35 | },
36 | "peerDependencies": {
37 | "angular": ">=1.3.14"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "https://github.com/troch/angular-multi-step-form.git"
42 | },
43 | "author": "Thomas Roch",
44 | "license": "ISC",
45 | "bugs": {
46 | "url": "https://github.com/troch/angular-multi-step-form/issues"
47 | },
48 | "homepage": "https://github.com/troch/angular-multi-step-form"
49 | }
50 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import uglify from 'rollup-plugin-uglify';
3 | import { argv } from 'yargs';
4 |
5 | const format = argv.format || argv.f || 'iife';
6 | const compress = argv.uglify;
7 |
8 | const babelOptions = {
9 | presets: [ 'es2015-rollup' ],
10 | babelrc: false
11 | };
12 |
13 | const dest = {
14 | umd: `dist/umd/angular-multi-step-form${ compress ? '.min' : '' }.js`,
15 | iife: `dist/browser/angular-multi-step-form${ compress ? '.min' : '' }.js`
16 | }[format];
17 |
18 | export default {
19 | entry: 'src/index.js',
20 | format,
21 | plugins: [ babel(babelOptions) ].concat(compress ? uglify() : []),
22 | moduleName: 'angularMultiStepForm',
23 | moduleId: 'angularMultiStepForm',
24 | dest,
25 | external: [ 'angular' ],
26 | globals: { 'angular': 'angular' }
27 | };
28 |
--------------------------------------------------------------------------------
/src/directives/form-step-validity.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | export default [ '$parse', formStepValidity ];
4 |
5 | /**
6 | * @ngdoc directive
7 | * @name multiStepForm:formStepValidity
8 | *
9 | * @restrict A
10 | * @description Notify the multi step form instance of a change of validity of a step.
11 | * This directive can be used on a form element or within a form.
12 | */
13 | function formStepValidity($parse) {
14 | return {
15 | restrict: 'A',
16 | require: '^form',
17 | link: function postLink(scope, element, attrs, formCtrl) {
18 | // The callback to call when a change of validity
19 | // is detected
20 | const validtyChangeCallback = attrs.formStepValidity ?
21 | $parse(attrs.formStepValidity).bind(scope, scope) :
22 | scope.$setValidity;
23 |
24 | // Watch the form validity
25 | scope.$watch(function () {
26 | return formCtrl.$valid;
27 | }, function (val) {
28 | // Check if defined
29 | if (angular.isDefined(val)) {
30 | validtyChangeCallback(val);
31 | }
32 | });
33 | }
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/src/directives/multi-step-container.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | export default [ '$animate', '$q', '$controller', 'multiStepForm', 'FormStep', 'formStepElement', multiStepContainer ];
4 |
5 | /**
6 | * @ngdoc directive
7 | * @name multiStepForm:multiStepContainer
8 | *
9 | * @requires $scope
10 | * @requires $q
11 | * @requires $controller
12 | * @requires multiStepForm:multiStepForm
13 | * @requires multiStepForm:FormStep
14 | * @requires multiStepForm:formStepElement
15 | *
16 | * @scope
17 | * @description Multi step directive (overall container)
18 | */
19 | function multiStepContainer($animate, $q, $controller, multiStepForm, FormStep, formStepElement) {
20 | return {
21 | restrict: 'EA',
22 | scope: true,
23 | /**
24 | * @ngdoc controller
25 | * @name multiStepForm:MultiStepFormCtrl
26 | *
27 | * @requires $scope
28 | * @requires multiStepForm:multiStepForm
29 | *
30 | * @description Controls a multi step form and track progress.
31 | */
32 | controller: [
33 | '$scope',
34 | function ($scope) {
35 | /**
36 | * @ngdoc function
37 | * @methodOf multiStepForm:MultiStepFormCtrl
38 | *
39 | * @description Register the step container element. This method
40 | * is invoked in the post link function of directive
41 | * {@link multiStepForm:formStepContainer formStepContainer}
42 | * @param {JQLite} elm The step form container
43 | */
44 | this.setStepContainer = function (elm) {
45 | this.stepContainer = elm;
46 | };
47 | }
48 | ],
49 | link: {
50 | pre: function preLink(scope, element, attrs) {
51 | /**
52 | * @ngdoc property
53 | * @propertyOf multiStepForm:MultiStepFormContainer
54 | * @description The form steps
55 | * @type {FormStep[]}
56 | */
57 | scope.formSteps = scope.$eval(attrs.steps).map(function (step) {
58 | return new FormStep(step);
59 | });
60 |
61 | /**
62 | * @ngdoc property
63 | * @propertyOf multiStepForm:MultiStepFormContainer
64 | * @description The form step titles
65 | * @type {String[]}
66 | */
67 | scope.stepTitles = scope.formSteps.map(function (step) {
68 | return step.title;
69 | });
70 | },
71 | post: function postLink(scope, element, attrs, controller) {
72 | // Add .multi-step-container class
73 | element.addClass('multi-step-container');
74 | // Callbacks
75 | const onFinish = attrs.onFinish ? resolve(attrs.onFinish) : defaultResolve;
76 | const onCancel = attrs.onCancel ? resolve(attrs.onCancel) : defaultResolve;
77 | const onStepChange = attrs.onStepChange ? () => scope.$eval(attrs.onStepChange) : angular.noop;
78 | // Step container (populated by child post link function)
79 | const stepContainer = controller.stepContainer;
80 | const multiStepFormInstance = multiStepForm(scope.$eval(attrs.searchId));
81 |
82 | // Augment scope
83 | multiStepFormInstance.augmentScope(scope);
84 |
85 | // Controller
86 | if (attrs.controller) {
87 | const customController = $controller(attrs.controller, {
88 | $scope: scope,
89 | $element: element,
90 | multiStepFormInstance
91 | });
92 | // controllerAs
93 | if (attrs.controllerAs) {
94 | scope[attrs.controllerAs] = customController;
95 | }
96 | }
97 |
98 | // Initial step
99 | const initialStep = scope.$eval(attrs.initialStep);
100 |
101 | let currentLeaveAnimation,
102 | currentEnterAnimation,
103 | currentStepScope,
104 | currentStepElement,
105 | isDeferredResolved = false;
106 |
107 | // Resolve any outstanding promises on destroy
108 | scope.$on('$destroy', () => {
109 | if (!isDeferredResolved) {
110 | isDeferredResolved = true;
111 | multiStepFormInstance.deferred.reject();
112 | }
113 | });
114 |
115 | // Initialise and start the multi step form
116 | multiStepFormInstance
117 | .start(scope.formSteps)
118 | .then(onFinish, onCancel, function (data) {
119 | const step = data.newStep;
120 | const previousStep = data.oldStep;
121 | const direction = angular.isDefined(previousStep) ?
122 | (step < previousStep ? 'step-backward' : 'step-forward') :
123 | 'step-initial';
124 |
125 | const formStep = scope.formSteps[step - 1];
126 | // Create new step element (promise)
127 | const newStepElement = formStepElement(formStep, multiStepFormInstance, scope);
128 |
129 | // Add direction class to the parent container;
130 | stepContainer
131 | .removeClass('step-forward step-backward step-initial')
132 | .addClass(direction);
133 |
134 | // Cancel current leave animation if any
135 | if (currentLeaveAnimation) {
136 | $animate.cancel(currentLeaveAnimation);
137 | }
138 | // Cancel current enter animation if any
139 | if (currentEnterAnimation) {
140 | $animate.cancel(currentEnterAnimation);
141 | }
142 | // Destroy current scope
143 | if (currentStepScope) {
144 | currentStepScope.$destroy();
145 | }
146 | // Leave current step if any
147 | if (currentStepElement) {
148 | currentLeaveAnimation = $animate.leave(currentStepElement);
149 | }
150 | // Enter new step when new step element is ready
151 | newStepElement
152 | .then(function (step) {
153 | onStepChange();
154 | currentStepScope = step.scope;
155 | currentStepElement = step.element;
156 | currentStepElement.scrollTop = 0;
157 | stepContainer.scrollTop = 0;
158 | currentEnterAnimation = $animate.enter(currentStepElement, stepContainer);
159 | }, function () {
160 | throw new Error('Could not load step ' + step);
161 | });
162 | });
163 |
164 | // Initialise currentStep
165 | multiStepFormInstance.setInitialIndex(initialStep);
166 |
167 | // Default resolution function
168 | function defaultResolve() {
169 | $animate.leave(element);
170 | isDeferredResolved = true;
171 | scope.$destroy();
172 | }
173 |
174 | // On promise resolution
175 | function resolve(fn) {
176 | return function() {
177 | isDeferredResolved = true;
178 | scope.$eval(fn);
179 | };
180 | }
181 | }
182 | }
183 | };
184 | }
185 |
--------------------------------------------------------------------------------
/src/directives/step-container.js:
--------------------------------------------------------------------------------
1 | export default stepContainer;
2 |
3 | /**
4 | * @ngdoc directive
5 | * @name multiStepForm:stepContainer
6 | *
7 | * @requires multiStepForm:stepContainer
8 | *
9 | * @restrict A
10 | * @description The container for form steps. It registers itself with the multi step container.
11 | * {@link multiStepForm:multiStepContainer multiStepContainer}
12 | */
13 | function stepContainer() {
14 | return {
15 | restrict: 'EA',
16 | require: '^^multiStepContainer',
17 | scope: false,
18 | link: function postLink(scope, element, attrs, multiStepCtrl) {
19 | element.addClass('multi-step-body');
20 | multiStepCtrl.setStepContainer(element);
21 | }
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import multiStepContainer from './directives/multi-step-container';
4 | import formStepValidity from './directives/form-step-validity';
5 | import stepContainer from './directives/step-container';
6 | import formStepElement from './services/form-step-element-factory';
7 | import FormStep from './services/form-step-object';
8 | import multiStepForm from './services/multi-step-form-factory';
9 |
10 | /**
11 | * @ngdoc module
12 | * @name multiStepForm
13 | */
14 | const multiStepFormModule = angular.module('multiStepForm', [/*'ngAnimate'*/]);
15 |
16 | multiStepFormModule
17 | .directive('formStepValidity', formStepValidity)
18 | .directive('multiStepContainer', multiStepContainer)
19 | .directive('stepContainer', stepContainer)
20 | .factory('formStepElement', formStepElement)
21 | .factory('FormStep', FormStep)
22 | .factory('multiStepForm', multiStepForm);
23 |
24 | export default multiStepFormModule;
25 |
--------------------------------------------------------------------------------
/src/services/form-step-element-factory.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | export default [ '$compile', '$controller', '$http', '$injector', '$q', '$templateCache', formStepElement ];
4 |
5 | /**
6 | * @ngdoc function
7 | * @name multiStepForm:formStepElement
8 | *
9 | * @requires $rootScope
10 | * @requires $controller
11 | *
12 | * @description A factory function for creating form step elements
13 | * (using controller, template and resolve)
14 | */
15 | function formStepElement($compile, $controller, $http, $injector, $q, $templateCache) {
16 | /**
17 | * Resolve the template of a form step
18 | * @param {FormStep} formStep The form step object
19 | * @return {Promise|String} A promise containing the template
20 | */
21 | function resolveTemplate(formStep) {
22 | if (formStep.template) {
23 | // If function or array, use $injector to get template value
24 | return angular.isFunction(formStep.template) || angular.isArray(formStep.template) ?
25 | $injector.$invoke(formStep.template) :
26 | formStep.template;
27 | }
28 | // Use templateUrl
29 | const templateUrl =
30 | // If function or array, use $injector to get templateUrl value
31 | angular.isFunction(formStep.templateUrl) || angular.isArray(formStep.templateUrl) ?
32 | $injector.$invoke(formStep.templateUrl) :
33 | formStep.templateUrl;
34 | // Request templateUrl using $templateCache
35 | return $http.get(templateUrl, {cache: $templateCache});
36 | }
37 |
38 | /**
39 | * Create a new scope with the multiStepContainer being the parent scope.
40 | * augmented with multi step form control methods.
41 | * @param {FormStep} formStep The form step object
42 | * @param {FormStep} formStep The form step object
43 | * @return {Object} The form step scope
44 | */
45 | function getScope(scope, formStep, multiStepFormInstance) {
46 | const stepScope = scope.$new(formStep.isolatedScope);
47 | // Augment scope with multi step form instance methods
48 | multiStepFormInstance.augmentScope(stepScope);
49 |
50 | return stepScope;
51 | }
52 |
53 | /**
54 | * Create a form step element, compiled with controller and dependencies resolved
55 | *
56 | * @param {FormStep} formStep The form step object
57 | * @param {Object} multiStepFormInstance The multi step form instance
58 | * @param {Object} multiStepFormScope The scope instance of the multi step form
59 | * @return {Promise} A promise containing the form step element
60 | */
61 | return function formStepElementFactory(formStep, multiStepFormInstance, multiStepFormScope) {
62 | const formStepElement = angular.element('')
63 | .addClass('form-step');
64 |
65 | let controller,
66 | template,
67 | promisesHash = {};
68 |
69 | // Get template
70 | promisesHash.$template = resolveTemplate(formStep);
71 |
72 |
73 | // Get resolve
74 | angular.forEach(formStep.resolve, function (resolveVal, resolveName) {
75 | promisesHash[resolveName] =
76 | // angular.isString(resolveVal) ?
77 | // $injector.get(resolveVal) :
78 | $injector.invoke(resolveVal);
79 | });
80 |
81 | // After all locals are resolved (template and "resolves") //
82 | return $q.all(promisesHash)
83 | .then(function (locals) {
84 | // Extend formStep locals with resolved locals
85 | locals = angular.extend({}, formStep.locals, locals);
86 | // Load template inside element
87 | locals.$template = locals.$template.data || locals.$template;
88 | formStepElement.html(locals.$template);
89 | // Create scope
90 | const formStepScope = getScope(multiStepFormScope, formStep, multiStepFormInstance);
91 |
92 | if (formStep.controller) {
93 | // Create form step scope
94 | locals.$scope = formStepScope;
95 | // Add multi step form service instance to local injectables
96 | locals.multiStepFormInstance = multiStepFormInstance;
97 | // Add multi step form scope to local injectables if isolated
98 | if (formStep.isolatedScope) {
99 | locals.multiStepFormScope = multiStepFormScope;
100 | }
101 | // Instanciate controller
102 | controller = $controller(formStep.controller, locals);
103 | // controllerAs
104 | if (formStep.controllerAs) {
105 | formStepScope[formStep.controllerAs] = controller;
106 | }
107 | formStepElement.data('$stepController', controller);
108 | // formStepElement.children().data('$ngControllerController', controller);
109 | }
110 |
111 | // Compile form step element and link with scope
112 | $compile(formStepElement)(formStepScope);
113 |
114 | // Return element and scope
115 | return {
116 | element: formStepElement,
117 | scope: formStepScope
118 | };
119 | });
120 | };
121 | }
122 |
--------------------------------------------------------------------------------
/src/services/form-step-object.js:
--------------------------------------------------------------------------------
1 | export default FormStep;
2 |
3 | /**
4 | * @ngdoc object
5 | * @name multiStepForm:FormStep
6 | *
7 | * @description A constructor for creating form steps
8 | * @error If no template or templateUrl properties are supplied
9 | */
10 | function FormStep() {
11 | return function FormStep (config) {
12 | if (!config.template && !config.templateUrl) {
13 | throw new Error('Either template or templateUrl properties have to be provided for' +
14 | ' multi step form' + config.title);
15 | }
16 |
17 | /**
18 | * @ngdoc property
19 | * @propertyOf multiStepForm:FormStep
20 | *
21 | * @description The form step title
22 | * @type {String}
23 | */
24 | this.title = config.title;
25 |
26 | /**
27 | * @ngdoc property
28 | * @propertyOf multiStepForm:FormStep
29 | *
30 | * @description The form step additional data
31 | * @type {Object}
32 | */
33 | this.data = config.data || {};
34 |
35 | /**
36 | * @ngdoc property
37 | * @propertyOf multiStepForm:FormStep
38 | *
39 | * @description The form step controller
40 | * @type {String|Function|Array}
41 | */
42 | this.controller = config.controller;
43 |
44 | /**
45 | * @ngdoc property
46 | * @propertyOf multiStepForm:FormStep
47 | *
48 | * @description An identifier name for a reference to the controller
49 | * @type {String}
50 | */
51 | this.controllerAs = config.controllerAs;
52 |
53 | /**
54 | * @ngdoc property
55 | * @propertyOf multiStepForm:FormStep
56 | *
57 | * @description The form step template
58 | * @type {String}
59 | */
60 | this.template = config.template;
61 |
62 | /**
63 | * @ngdoc property
64 | * @propertyOf multiStepForm:FormStep
65 | *
66 | * @description The form step template URL
67 | * @type {String}
68 | */
69 | this.templateUrl = config.templateUrl;
70 |
71 | /**
72 | * Whether or not the form step should have an isolated scope
73 | * @type {Boolean}
74 | */
75 | this.isolatedScope = config.isolatedScope || false;
76 |
77 | /**
78 | * @ngdoc property
79 | * @propertyOf multiStepForm:resolve
80 | *
81 | * @description The form step resolve map (same use than for routes)
82 | * @type {Object}
83 | */
84 | this.resolve = config.resolve || {};
85 |
86 | /**
87 | * @ngdoc property
88 | * @propertyOf multiStepForm:resolve
89 | *
90 | * @description The form step locals map (same than resolve but for non deferred values)
91 | * Note: resolve also works with non deferred values
92 | * @type {Object}
93 | */
94 | this.locals = config.locals || {};
95 |
96 | /**
97 | * @ngdoc property
98 | * @propertyOf multiStepForm:FormStep
99 | *
100 | * @description Whether or not this form step contains a form
101 | * @type {Boolean}
102 | */
103 | this.hasForm = config.hasForm || false;
104 |
105 | /**
106 | * @ngdoc property
107 | * @propertyOf multiStepForm:FormStep
108 | *
109 | * @description Whether or not this form step is valid.
110 | * Form validity can been fed back using a specific directive.
111 | * {@link multiStepForm:formStepValidity formStepValidity}
112 | * @type {Boolean}
113 | */
114 | this.valid = false;
115 |
116 | /**
117 | * @ngdoc property
118 | * @propertyOf multiStepForm:FormStep
119 | *
120 | * @description Whether or not this step has been visited
121 | * (i.e. the user has moved to the next step)
122 | * @type {Boolean}
123 | */
124 | this.visited = false;
125 | };
126 | }
127 |
--------------------------------------------------------------------------------
/src/services/multi-step-form-factory.js:
--------------------------------------------------------------------------------
1 | export default [ '$q', '$location', '$rootScope', multiStepForm ];
2 |
3 | /**
4 | * @ngdoc function
5 | * @name multiStepForm:multiStepForm
6 | *
7 | * @requires $q
8 | * @requires multiStepForm:FormStep
9 | *
10 | * @description A service returning an instance per multi step form.
11 | * The instance of the service is injected in each step controller.
12 | */
13 | function multiStepForm($q, $location, $rootScope) {
14 | function MultiFormStep(searchId) {
15 | /**
16 | * @ngdoc property
17 | * @propertyOf multiStepForm:multiStepForm
18 | *
19 | * @description The location search property name to store the active step index.
20 | * @type {String}
21 | */
22 | this.searchId = searchId;
23 | // If the search id is defined,
24 | if (angular.isDefined(searchId)) {
25 | $rootScope.$on('$locationChangeSuccess', (event) => {
26 | const searchIndex = parseInt($location.search()[this.searchId]);
27 |
28 | if (!isNaN(searchIndex) && this.activeIndex !== searchIndex) {
29 | // Synchronise
30 | this.setActiveIndex(parseInt(searchIndex));
31 | }
32 | });
33 | }
34 |
35 | /**
36 | * @ngdoc property
37 | * @propertyOf multiStepForm:multiStepForm
38 | *
39 | * @description The form steps
40 | * @type {Array}
41 | */
42 | this.steps = [];
43 |
44 | /**
45 | * @ngdoc property
46 | * @propertyOf multiStepForm:multiStepForm
47 | *
48 | * @description Return the form steps
49 | * @return {Array}
50 | */
51 | this.getSteps = function () {
52 | return this.steps;
53 | };
54 |
55 | /**
56 | * @ngdoc property
57 | * @propertyOf multiStepForm:multiStepForm
58 | *
59 | * @description The multi-step form deferred object
60 | * @type {Deferred}
61 | */
62 | this.deferred = $q.defer();
63 |
64 | /**
65 | * @ngdoc method
66 | * @methodOf multiStepForm:multiStepForm
67 | *
68 | * @description Initialise the form steps and start
69 | * @error If no steps are provided
70 | * @param {Array} steps The form steps
71 | * @return {Promise} A promise which will be resolved when all steps are passed,
72 | * and rejected if the user cancel the multi step form.
73 | */
74 | this.start = function (steps) {
75 | if (!steps || !steps.length) {
76 | throw new Error('At least one step has to be defined');
77 | }
78 | // Initialise steps
79 | this.steps = steps;
80 | // Return promise
81 | return this.deferred.promise;
82 | };
83 |
84 | /**
85 | * @ngdoc method
86 | * @methodOf multiStepForm:multiStepForm
87 | *
88 | * @description Cancel this multi step form
89 | * @type {Array}
90 | */
91 | this.cancel = function () {
92 | this.deferred.reject('cancelled');
93 | };
94 |
95 | /**
96 | * @ngdoc method
97 | * @methodOf multiStepForm:multiStepForm
98 | *
99 | * @description Finish this multi step form (success)
100 | */
101 | this.finish = function () {
102 | this.deferred.resolve();
103 | };
104 |
105 | /**
106 | * @ngdoc method
107 | * @methodOf multiStepForm:multiStepForm
108 | *
109 | * @description Return the multi form current step
110 | * @return {Number} The active step index (starting at 1)
111 | */
112 | this.getActiveIndex = function () {
113 | return this.activeIndex;
114 | };
115 |
116 | /**
117 | * @ngdoc method
118 | * @methodOf multiStepForm:multiStepForm
119 | *
120 | * @description Initialise the multi step form instance with
121 | * an initial index
122 | * @param {Number} step The index to start with
123 | */
124 | this.setInitialIndex = function (initialStep) {
125 | let searchIndex;
126 | // Initial step in markup has the priority
127 | // to override any manually entered URL
128 | if (angular.isDefined(initialStep)) {
129 | return this.setActiveIndex(initialStep);
130 | }
131 | // Otherwise use search ID if applicable
132 | if (this.searchId) {
133 | searchIndex = parseInt($location.search()[this.searchId]);
134 | if (!isNaN(searchIndex)) {
135 | return this.setActiveIndex(searchIndex);
136 | }
137 | }
138 | // Otherwise set to 1
139 | this.setActiveIndex(1);
140 | };
141 |
142 | /**
143 | * @ngdoc method
144 | * @methodOf multiStepForm:multiStepForm
145 | *
146 | * @description Set the current step to the provided value and notify
147 | * @param {Number} step The step index (starting at 1)
148 | */
149 | this.setActiveIndex = function (step) {
150 | if (this.searchId) {
151 | // Update $location
152 | if (this.activeIndex) {
153 | $location.search(this.searchId, step);
154 | } else {
155 | // Replace current one
156 | $location.search(this.searchId, step).replace();
157 | }
158 | }
159 | // Notify deferred object
160 | this.deferred.notify({
161 | newStep: step,
162 | oldStep: this.activeIndex
163 | });
164 | // Update activeIndex
165 | this.activeIndex = step;
166 | };
167 |
168 | /**
169 | * @ngdoc method
170 | * @methodOf multiStepForm:multiStepForm
171 | *
172 | * @description Return the active form step object
173 | * @return {FormStep} The active form step
174 | */
175 | this.getActiveStep = function () {
176 | if (this.activeIndex) {
177 | return this.steps[this.activeIndex - 1];
178 | }
179 | };
180 |
181 | /**
182 | * @ngdoc method
183 | * @methodOf multiStepForm:multiStepForm
184 | *
185 | * @description Check if the current step is the first step
186 | * @return {Boolean} Whether or not the current step is the first step
187 | */
188 | this.isFirst = function () {
189 | return this.activeIndex === 1;
190 | };
191 |
192 | /**
193 | * @ngdoc method
194 | * @methodOf multiStepForm:multiStepForm
195 | *
196 | * @description Check if the current step is the last step
197 | * @return {Boolean} Whether or not the current step is the last step
198 | */
199 | this.isLast = function () {
200 | return this.activeIndex === this.steps.length;
201 | };
202 |
203 | /**
204 | * @ngdoc method
205 | * @methodOf multiStepForm:multiStepForm
206 | *
207 | * @description Go to the next step, if not the last step
208 | */
209 | this.nextStep = function () {
210 | if (!this.isLast()) {
211 | this.setActiveIndex(this.activeIndex + 1);
212 | }
213 | };
214 |
215 | /**
216 | * @ngdoc method
217 | * @methodOf multiStepForm:multiStepForm
218 | *
219 | * @description Go to the next step, if not the first step
220 | */
221 | this.previousStep = function () {
222 | if (!this.isFirst()) {
223 | this.setActiveIndex(this.activeIndex - 1);
224 | }
225 | };
226 |
227 | /**
228 | * @ngdoc method
229 | * @methodOf multiStepForm:multiStepForm
230 | *
231 | * @description Go to the next step, if not the first step
232 | */
233 | this.setValidity = function (isValid, stepIndex) {
234 | const step = this.steps[(stepIndex || this.activeIndex) - 1];
235 |
236 | if (step) {
237 | step.valid = isValid;
238 | }
239 | };
240 |
241 |
242 | /**
243 | * @ngdoc method
244 | * @methodOf multiStepForm:multiStepForm
245 | *
246 | * @description Augment a scope with useful methods from this object
247 | * @param {Object} scope The scope to augment
248 | */
249 | this.augmentScope = function (scope) {
250 | ['cancel', 'finish', 'getActiveIndex', 'setActiveIndex', 'getActiveStep',
251 | 'getSteps', 'nextStep', 'previousStep', 'isFirst', 'isLast', 'setValidity']
252 | .forEach((method) => {
253 | scope['$' + method] = this[method].bind(this);
254 | });
255 | };
256 | }
257 |
258 | return function multiStepFormProvider(searchId) {
259 | return new MultiFormStep(searchId);
260 | };
261 | }
262 |
--------------------------------------------------------------------------------
/tests/form-step-element-factory.spec.js:
--------------------------------------------------------------------------------
1 | describe('formStepElement factory:', function () {
2 | var $rootScope,
3 | controller,
4 | step,
5 | stepIsolated,
6 | multiStepForm,
7 | formStepElement;
8 |
9 | function controllerIsolated (multiStepFormScope) {
10 | this.multiStepFormScope = multiStepFormScope;
11 | }
12 |
13 | beforeEach(module('multiStepForm'));
14 |
15 | beforeEach(inject(function(_$rootScope_, _multiStepForm_, FormStep, _formStepElement_) {
16 | controller = [
17 | 'multiStepFormInstance',
18 | function (multiStepFormInstance) {
19 | this.name = "It's me, Mario!";
20 | this.multiStepFormInstance = multiStepFormInstance;
21 | }
22 | ];
23 | $rootScope = _$rootScope_;
24 | // To check inheritance
25 | $rootScope.boolProp = true;
26 | // Multi step form factory (function)
27 | multiStepForm = _multiStepForm_;
28 | // Form step element factory
29 | formStepElement = _formStepElement_;
30 | // Create steps using FormStep factory
31 | step = new FormStep({title: 'Step 1', template: '
Step 1
', controller: controller});
32 | stepIsolated = new FormStep({title: 'Step 1', template: 'Step 1', isolatedScope: true, controller: controllerIsolated});
33 | }));
34 |
35 | /////////////////////////////////////////////////////////////////
36 | // Check an element is generated with the right scope //
37 | /////////////////////////////////////////////////////////////////
38 | describe('the scope of a generated step', function () {
39 | it('should inherit its parent scope by default', function () {
40 | var stepScope;
41 |
42 | formStepElement(step, multiStepForm(), $rootScope)
43 | .then(function (data) {
44 | stepScope = data.scope;
45 | });
46 |
47 | $rootScope.$digest();
48 | expect(stepScope.boolProp).toBe(true);
49 | });
50 |
51 |
52 | it('should be isolated if specified', function () {
53 | var stepScope;
54 |
55 | formStepElement(stepIsolated, multiStepForm(), $rootScope)
56 | .then(function (data) {
57 | stepScope = data.scope;
58 | });
59 |
60 | $rootScope.$digest();
61 | expect(stepScope.boolProp).toBeUndefined();
62 | });
63 |
64 | it('should be augmented with multiStepForm instance functions', function () {
65 | var stepScope;
66 |
67 | formStepElement(step, multiStepForm(), $rootScope)
68 | .then(function (data) {
69 | stepScope = data.scope;
70 | });
71 |
72 | $rootScope.$digest();
73 | expect(stepScope.$finish).toBeDefined();
74 | expect(stepScope.$cancel).toBeDefined();
75 | expect(stepScope.$nextStep).toBeDefined();
76 | expect(stepScope.$previousStep).toBeDefined();
77 | });
78 | });
79 |
80 | ////////////////////////////////////////////////////////////////////////////
81 | // Check an element is generated with the right controller and markup //
82 | ////////////////////////////////////////////////////////////////////////////
83 | describe('the markup of a generated step', function () {
84 | var stepElement;
85 |
86 | it('should be the provided template', function () {
87 | formStepElement(step, multiStepForm(), $rootScope)
88 | .then(function (data) {
89 | stepElement = data.element;
90 | });
91 |
92 | $rootScope.$digest();
93 | expect(stepElement.html()).toEqual('Step 1
');
94 | });
95 |
96 | it('should be controlled by the provided controller', function () {
97 | expect(stepElement.data('$stepController').name).toBe("It's me, Mario!");
98 | });
99 | });
100 |
101 | describe('the controller of a generated step', function () {
102 | var stepElement;
103 |
104 | it('should have the multi step form instance as a dependency', function () {
105 | formStepElement(step, multiStepForm(), $rootScope)
106 | .then(function (data) {
107 | stepElement = data.element;
108 | });
109 |
110 | $rootScope.$digest();
111 | expect(stepElement.data('$stepController').multiStepFormInstance).toBeDefined();
112 | });
113 |
114 | it('should have its parent scope as a dependency if isolated', function () {
115 | formStepElement(stepIsolated, multiStepForm(), $rootScope)
116 | .then(function (data) {
117 | stepElement = data.element;
118 | });
119 |
120 | $rootScope.$digest();
121 | expect(stepElement.data('$stepController').multiStepFormScope).toBeDefined();
122 | });
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/tests/helpers.js:
--------------------------------------------------------------------------------
1 | angular.module('helpers', []).controller('CustomController', function($scope, multiStepFormInstance) {
2 | $scope.parentObject.text = 'hello';
3 | });
4 |
--------------------------------------------------------------------------------
/tests/multi-step-container.spec.js:
--------------------------------------------------------------------------------
1 | describe('multiStepContainer directive:', function() {
2 | beforeEach(module('multiStepForm'));
3 | beforeEach(module('helpers'));
4 |
5 | var $rootScope,
6 | $compile,
7 | $location,
8 | $q,
9 | $log,
10 | scope;
11 |
12 | var template1 = 'Step 1
',
13 | template2 = 'Step 2
' +
14 | '';
17 |
18 | beforeEach(inject(function(_$compile_, _$rootScope_, _$location_, $templateCache, _$q_, _$log_) {
19 | $compile = _$compile_;
20 | $rootScope = _$rootScope_;
21 | $location = _$location_;
22 | $q = _$q_;
23 | $log = _$log_;
24 |
25 | scope = $rootScope.$new();
26 | scope.parentObject = {};
27 | scope.steps = [
28 | {
29 | title: 'Step 1',
30 | template: template1
31 | },
32 | {
33 | title: 'Step 2',
34 | templateUrl: 'tpl/template2.html',
35 | resolve: {
36 | data: [function () {
37 | return 'data'
38 | }]
39 | },
40 | locals: {
41 | local: 123
42 | },
43 | controller: ['$scope', 'data', 'local', function ($scope, data, local) {
44 | $scope.data = data;
45 | $scope.local = local;
46 | }],
47 | hasForm: true
48 | }
49 | ];
50 |
51 | $templateCache.put('tpl/template2.html', template2);
52 | }));
53 |
54 | function compileDirective(config) {
55 | var element = angular.element('');
56 | element.append('');
57 |
58 | if (angular.isObject(config)) {
59 | if (config.html) {
60 | element.append(config.html);
61 | }
62 | if (config.initialStep) {
63 | element.attr('initial-step', config.initialStep);
64 | }
65 | if (config.searchId) {
66 | element.attr('search-id', config.searchId);
67 | }
68 | }
69 |
70 | $compile(element)(scope);
71 | $rootScope.$digest();
72 |
73 | return element;
74 | }
75 |
76 | it('should throw an error if no step container is supplied', function () {
77 | var element = angular.element('');
78 | expect(function () {
79 | $compile(element)(scope);
80 | $rootScope.$digest();
81 | }).toThrow();
82 | });
83 |
84 | it('should start on the first step by default', function () {
85 | element = compileDirective();
86 | expect(element.children().eq(0).html()).toContain('Step 1');
87 | });
88 |
89 | it('should start on the specified initial step if provided', function () {
90 | element = compileDirective({initialStep: 2});
91 | expect(element.children().eq(0).html()).toContain('Step 2');
92 | });
93 |
94 | it('should have its header augmented with multiStepForm functions', function () {
95 | element = compileDirective();
96 | // Go to next
97 | element.scope().$nextStep();
98 | scope.$digest();
99 | expect(element.children().eq(0).html()).toContain('Step 2');
100 | // Go to previous
101 | element.scope().$previousStep();
102 | scope.$digest();
103 | expect(element.children().eq(0).html()).toContain('Step 1');
104 | // Go to a specific step
105 | element.scope().$setActiveIndex(2);
106 | scope.$digest();
107 | expect(element.children().eq(0).html()).toContain('Step 2');
108 | });
109 |
110 | it('should update the location if a search ID is provided', function () {
111 | element = compileDirective({searchId: "'multi1'"});
112 | expect($location.search().multi1).toEqual(1);
113 | });
114 |
115 | it('should navigate to the desired step if the location search ID is modified', function () {
116 | element = compileDirective({searchId: "'multi1'"});
117 | $location.search('multi1', 2);
118 | $rootScope.$emit('$locationChangeSuccess');
119 | scope.$digest();
120 | expect(element.children().eq(0).html()).toContain('Step 2');
121 | });
122 |
123 | it('should start with the step provided in URL if no intialStep defined', function () {
124 | $location.search('multi1', 2);
125 | $rootScope.$emit('$locationChangeSuccess');
126 | scope.$digest();
127 | element = compileDirective({searchId: "'multi1'"});
128 | expect(element.children().eq(0).html()).toContain('Step 2');
129 | expect($location.search().multi1).toEqual(2);
130 | });
131 |
132 | it('should force the initial step to be the one provided to the directive', function () {
133 | $location.search('multi1', 2);
134 | element = compileDirective({searchId: "'multi1'", initialStep: 1});
135 | expect(element.children().eq(0).html()).toContain('Step 1');
136 | expect($location.search().multi1).toEqual(1);
137 | });
138 |
139 | it('should destroy itself when cancelled', function () {
140 | element = compileDirective();
141 | element.scope().$cancel();
142 | scope.$digest();
143 | expect(element.scope()).toBeUndefined();
144 | });
145 |
146 | it('should destroy itself when finished', function () {
147 | element = compileDirective();
148 | element.scope().$finish();
149 | scope.$digest();
150 | expect(element.scope()).toBeUndefined();
151 | });
152 |
153 | it('should have resolved and locals injected in controller', function () {
154 | element = compileDirective({initialStep: 2});
155 | expect(element.scope().$$childTail.data).toEqual('data');
156 | expect(element.scope().$$childTail.local).toEqual(123);
157 | });
158 |
159 | it('should inform the main directive of a step validity if a form is present', function () {
160 | element = compileDirective();
161 | // Go to next
162 | element.scope().$nextStep();
163 | scope.$digest();
164 | // Step should be invalid
165 | expect(element.scope().$getActiveStep().valid).toBe(false);
166 | // Change model value to make form valid
167 | element.scope().model = 'aaa';
168 | scope.$digest();
169 | expect(element.scope().$getActiveStep().valid).toBe(true);
170 | });
171 |
172 | it('should throw an error if a step cannot be loaded', function () {
173 | scope.steps = [
174 | {
175 | title: 'Step 1',
176 | template: 'Step 1',
177 | resolve: {
178 | data: function () {
179 | return $q.reject();
180 | }
181 | }
182 | }
183 | ];
184 |
185 | expect(function () {
186 | compileDirective()
187 | }).toThrow();
188 | });
189 |
190 | it('should support controllerAs syntax', function () {
191 | scope.steps = [
192 | {
193 | title: 'Step 1',
194 | template: 'Step 1',
195 | controller: function () {
196 | this.name = 'name';
197 | },
198 | controllerAs: 'step'
199 | }
200 | ];
201 | element = compileDirective();
202 | expect(element.scope().$$childHead.step.name).toEqual('name');
203 | });
204 |
205 | it('should invoke a provided callback on transition', function () {
206 | scope.onStepChange = jasmine.createSpy('onStepChange');
207 | var element = angular.element('');
208 | element.append('');
209 |
210 | $compile(element)(scope);
211 | $rootScope.$digest();
212 | element.scope().$nextStep();
213 | expect(scope.onStepChange).toHaveBeenCalled();
214 |
215 | element.scope().$nextStep();
216 | $rootScope.$digest();
217 | expect(scope.onStepChange.calls.count()).toBe(2);
218 | });
219 |
220 | it('should instantiate a custom controller', function () {
221 | var element = angular.element('');
222 | expect(function() {
223 | $compile(element)(scope);
224 | }).not.toThrow();
225 | expect(scope.parentObject.text).toBe('hello');
226 | });
227 | });
228 |
--------------------------------------------------------------------------------
/tests/multi-step-form-factory.spec.js:
--------------------------------------------------------------------------------
1 | describe('multiStepForm factory:', function () {
2 | var $rootScope,
3 | FormStep,
4 | steps,
5 | multiStepForm;
6 |
7 | beforeEach(module('multiStepForm'));
8 |
9 | beforeEach(inject(function(_$rootScope_, _multiStepForm_, _FormStep_) {
10 | $rootScope = _$rootScope_;
11 | FormStep = _FormStep_;
12 | // Multi step form factory (function)
13 | multiStepForm = _multiStepForm_;
14 | // Create steps using FormStep factory
15 | steps = [
16 | {title: 'Step 1', template: 'Step 1', data: {first: true}},
17 | {title: 'Step 2', template: 'Step 2'},
18 | {title: 'Step 3', template: 'Step 3'},
19 | {title: 'Step 4', template: 'Step 4'},
20 | {title: 'Step 5', template: 'Step 5'}
21 | ].map(function (step) {
22 | return new FormStep(step);
23 | });
24 | }));
25 |
26 | /////////////////////////////////////////////////////////////////
27 | // Check a new instance is generated each time it is requested //
28 | /////////////////////////////////////////////////////////////////
29 | describe('each instance', function () {
30 | it('should throw an error if no template is specified', function () {
31 | expect(function() {
32 | new FormStep({title: 'Step 1'})
33 | }).toThrow();
34 | });
35 |
36 | it('should be unique', function () {
37 | expect(multiStepForm()).not.toBe(multiStepForm());
38 | });
39 |
40 | it('should have a start function returning a promise', function () {
41 | expect(typeof multiStepForm().start(steps).then).toEqual('function');
42 | });
43 |
44 | it('should fail if no steps are provided', function () {
45 | expect(multiStepForm().start).toThrow();
46 | });
47 | });
48 |
49 |
50 | ///////////////////////////////////////////////////////
51 | // Check a multi step form process can be controlled //
52 | ///////////////////////////////////////////////////////
53 | describe('once a multi step form instance is started', function () {
54 | var multiStepFormInstance,
55 | multiStepFormPromise;
56 |
57 | it('should allow the active step to be set', function () {
58 | multiStepFormInstance = multiStepForm();
59 | multiStepFormPromise = multiStepFormInstance.start(steps);
60 | multiStepFormInstance.setActiveIndex(4);
61 | expect(multiStepFormInstance.getActiveIndex()).toEqual(4);
62 | });
63 |
64 | it('can go to the next step within range', function () {
65 | multiStepFormInstance.nextStep();
66 | expect(multiStepFormInstance.getActiveIndex()).toEqual(5);
67 | expect(multiStepFormInstance.isLast()).toBe(true);
68 | multiStepFormInstance.nextStep();
69 | expect(multiStepFormInstance.getActiveIndex()).toEqual(5);
70 | });
71 |
72 | it('can go to the previous step within range', function () {
73 | multiStepFormInstance.setActiveIndex(2);
74 | multiStepFormInstance.previousStep();
75 | expect(multiStepFormInstance.getActiveIndex()).toEqual(1);
76 | expect(multiStepFormInstance.isFirst()).toBe(true);
77 | multiStepFormInstance.previousStep();
78 | expect(multiStepFormInstance.getActiveIndex()).toEqual(1);
79 | });
80 |
81 | it('should return the list of steps', function () {
82 | expect(multiStepFormInstance.getSteps().length).toEqual(5);
83 | });
84 | });
85 | });
86 |
--------------------------------------------------------------------------------