├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── browser │ ├── angular-multi-step-form.js │ └── angular-multi-step-form.min.js ├── commonjs │ ├── directives │ │ ├── form-step-validity.js │ │ ├── multi-step-container.js │ │ └── step-container.js │ ├── index.js │ └── services │ │ ├── form-step-element-factory.js │ │ ├── form-step-object.js │ │ └── multi-step-form-factory.js └── umd │ ├── angular-multi-step-form.js │ └── angular-multi-step-form.min.js ├── docs ├── advanced-guide.md ├── configuring-steps.md ├── migrating-to-1.1.x.md ├── multi-step-container.md ├── multi-step-instance.md ├── scopes.md └── steps-lifecycle.md ├── karma.config.js ├── package.json ├── rollup.config.js ├── src ├── directives │ ├── form-step-validity.js │ ├── multi-step-container.js │ └── step-container.js ├── index.js └── services │ ├── form-step-element-factory.js │ ├── form-step-object.js │ └── multi-step-form-factory.js └── tests ├── form-step-element-factory.spec.js ├── helpers.js ├── multi-step-container.spec.js └── multi-step-form-factory.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Parser 3 | "parser": "babel-eslint", 4 | // ECMA Features 5 | "ecmaFeatures": { 6 | "arrowFunctions": true, 7 | "blockBindings": true, 8 | "classes": true, 9 | "defaultParams": true, 10 | "destructuring": true, 11 | "modules": true, 12 | "objectLiteralComputedProperties": true, 13 | "templateStrings": true 14 | }, 15 | "rules": { 16 | // Possible Errors 17 | "no-dupe-args": 2, 18 | "no-dupe-keys": 2, 19 | "no-empty": 2, 20 | "no-func-assign": 2, 21 | "no-inner-declarations": 2, 22 | "no-unreachable": 2, 23 | "no-unexpected-multiline": 2, 24 | // Best practices 25 | "consistent-return": 0, 26 | "curly": [2, "multi-line"], 27 | "eqeqeq": 2, 28 | "no-else-return": 2, 29 | "no-multi-spaces": 0, 30 | // Strict mode 31 | "strict": 0, 32 | // Variables 33 | "no-shadow": 0, 34 | "no-unused-vars": 0, 35 | "no-use-before-define": 0, 36 | // Style 37 | "brace-style": [2, "1tbs"], 38 | "comma-spacing": [2, {"before": false, "after": true}], 39 | "comma-style": [2, "last"], 40 | "consistent-this": [2, "that"], 41 | "lines-around-comment": 0, 42 | "key-spacing": 0, 43 | "new-parens": 0, 44 | "quotes": [2, "single", "avoid-escape"], 45 | "no-underscore-dangle": 0, 46 | "no-unneeded-ternary": 2, 47 | "semi": 2, 48 | // ES6 49 | "no-var": 2, 50 | "no-this-before-super": 2, 51 | "object-shorthand": 2, 52 | }, 53 | "env": { 54 | "node": true, 55 | "browser": true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | coverage/ 4 | src/ 5 | docs/ 6 | npm-debug.log 7 | gulpfile.js 8 | bower.json 9 | karma.config.js 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | node_modules 3 | *.log 4 | *.md 5 | *.json 6 | karma.config.js 7 | bower_components 8 | docs 9 | src 10 | tests 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.0' 4 | before_install: 5 | - npm install -g babel-cli 6 | before_script: 7 | - npm install -g bower 8 | - bower install -f 9 | - export DISPLAY=:99.0 10 | - sh -e /etc/init.d/xvfb start 11 | script: 12 | - npm run lint 13 | - npm run build 14 | - npm test 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [1.3.0](https://github.com/troch/angular-multi-step-form/compare/v1.2.9...v1.3.0) (2016-07-24) 3 | 4 | 5 | ### Features 6 | 7 | * add support for directive level controller ([7fa5587](https://github.com/troch/angular-multi-step-form/commit/7fa5587)) 8 | 9 | 10 | 11 | 12 | ## [1.2.9](https://github.com/troch/angular-multi-step-form/compare/v1.2.8...v1.2.9) (2016-05-17) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * reject multi step promise when scope is destroyed ([9968704](https://github.com/troch/angular-multi-step-form/commit/9968704)), closes [#41](https://github.com/troch/angular-multi-step-form/issues/41) 18 | 19 | 20 | 21 | 22 | ## [1.2.8](https://github.com/troch/angular-multi-step-form/compare/v1.2.7...v1.2.8) (2016-02-27) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * Resolve multi step instance promise on destroy ([cb56567](https://github.com/troch/angular-multi-step-form/commit/cb56567)) 28 | 29 | 30 | 31 | 32 | ## [1.2.7](https://github.com/troch/angular-multi-step-form/compare/v1.2.6...v1.2.7) (2015-12-30) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * fix issue with invoking custom callbacks with angular 1.4+ ([c2a7f8e](https://github.com/troch/angular-multi-step-form/commit/c2a7f8e)) 38 | 39 | 40 | 41 | 42 | ## [1.2.5](https://github.com/troch/angular-multi-step-form/compare/v1.2.4...v1.2.5) (2015-12-21) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * Fix dev dependency issue with gulp ([6216723](https://github.com/troch/angular-multi-step-form/commit/6216723)) 48 | 49 | 50 | 51 | 52 | ### 1.2.4 (2015-11-11) 53 | 54 | 55 | #### Bug Fixes 56 | 57 | * set main file configuration in bower package to non minified version ([a5e55ba9](https://github.com/troch/angular-multi-step-form/commit/a5e55ba9)) 58 | 59 | 60 | 61 | ### 1.2.3 (2015-11-11) 62 | 63 | 64 | #### Bug Fixes 65 | 66 | * annotate distributed sources ([6d8a4c04](https://github.com/troch/angular-multi-step-form/commit/6d8a4c04)) 67 | 68 | 69 | 70 | ### 1.2.2 (2015-11-08) 71 | 72 | 73 | #### Bug Fixes 74 | 75 | * fix assignment of custom data to steps ([b2707ad8](https://github.com/troch/angular-multi-step-form/commit/b2707ad8)) 76 | 77 | 78 | #### Features 79 | 80 | * use ES6 modules for npm package. Main bower file is now in `dist/browser` and not `dist` ([9deeee20](https://github.com/troch/angular-multi-step-form/commit/9deeee20)) 81 | 82 | 83 | 84 | ## 1.2.0 (2015-09-04) 85 | 86 | 87 | #### Features 88 | 89 | * add 'onStepChange' callback ([d9c188b7](https://github.com/troch/angular-multi-step-form/commit/d9c188b7)) 90 | 91 | 92 | 93 | ### 1.1.2 (2015-09-04) 94 | 95 | 96 | #### Features 97 | 98 | * scroll to top of form set container on step change ([72cca503](https://github.com/troch/angular-multi-step-form/commit/72cca503)) 99 | 100 | 101 | 102 | ## 1.1.0 (2015-06-19) 103 | 104 | 105 | #### Bug Fixes 106 | 107 | * remove ngAnimate dependency ([6bf652e5](https://github.com/troch/angular-multi-step-form/commit/6bf652e5)) 108 | 109 | 110 | #### Features 111 | 112 | * add support for both header and footer. ([e80b944d](https://github.com/troch/angular-multi-step-form/commit/e80b944d), closes [#2](https://github.com/troch/angular-multi-step-form/issues/2)) 113 | 114 | 115 | #### Breaking Changes 116 | 117 | * prior to this, there could only be a header or a footer. Now there can be both, as long as an element with the `stepContainer` directive is inside the `multiStepContainer` directive. See [migration guide](./docs/migrating-to-1.1.x.md) 118 | 119 | Fixes #2 120 | 121 | ([e80b944d](https://github.com/troch/angular-multi-step-form/commit/e80b944d)) 122 | 123 | 124 | 125 | 126 | ## 1.1.0 (2015-06-19) 127 | 128 | 129 | 130 | 131 | ### 1.0.5 (2015-06-17) 132 | 133 | 134 | #### Features 135 | 136 | * **controllerAs:** support controllerAs syntax for steps ([9568676f](https://github.com/troch/angular-multi-step-form/commit/9568676f), closes [#1](https://github.com/troch/angular-multi-step-form/issues/1)) 137 | 138 | 139 | 140 | ### 1.0.4 (2015-06-16) 141 | 142 | 143 | #### Bug Fixes 144 | 145 | * **navigation:** replace current location with searchId when loading first step ([546ae30b](https://github.com/troch/angular-multi-step-form/commit/546ae30b)) 146 | 147 | 148 | #### Features 149 | 150 | * **MultiStepForm:** add getSteps() method, add it to scopes (`$getSteps()`) ([72504619](https://github.com/troch/angular-multi-step-form/commit/72504619)) 151 | 152 | 153 | 154 | ### 1.0.3 (2015-06-16) 155 | 156 | 157 | #### Bug Fixes 158 | 159 | * **multiStepContainer:** remove step-initial class before transition ([1570290f](https://github.com/troch/angular-multi-step-form/commit/1570290f)) 160 | 161 | #### Features 162 | 163 | * **multiStepContainer:** use `main` rather than `article` in HTML template ([ba9680f](https://github.com/troch/angular-multi-step-form/commit/ba9680f)) 164 | 165 | 166 | ### 1.0.2 (2015-06-14) 167 | 168 | 169 | #### Features 170 | 171 | * **multiStepContainer:** add support for `use-footer` attribute to transclude in footer rather than in header ([269855d](https://github.com/troch/angular-multi-step-form/commit/269855d)) 172 | 173 | 174 | 175 | ### 1.0.1 (2015-06-12) 176 | 177 | 178 | 179 | ### 1.0.0 (2015-06-12) 180 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Thomas Roch <> 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/angular-multi-step-form.svg)](https://badge.fury.io/js/angular-multi-step-form) 2 | [![Build Status](https://travis-ci.org/troch/angular-multi-step-form.svg?branch=master)](https://travis-ci.org/troch/angular-multi-step-form) 3 | [![Coverage Status](https://coveralls.io/repos/troch/angular-multi-step-form/badge.svg?branch=master)](https://coveralls.io/r/troch/angular-multi-step-form?branch=master) 4 | 5 | # Angular Multi step form 6 | 7 | `multiStepForm` is an angular module to create multi step forms and wizards. Create your steps like your would 8 | create your views with ngRoute or ui-router! 9 | 10 | It is lightweight (6kb minified) but extremely versatile and powerful. 11 | 12 | 13 | ## Requirements 14 | 15 | - Angular 1.3+ 16 | 17 | 18 | ## Features 19 | 20 | - Steps are controlled views and are easily configured 21 | - Directive controller 22 | - Asynchronous loading of steps (`templateUrl` and `resolve`) 23 | - Forward and backward animations 24 | - Isolated or non isolated scopes for steps 25 | - Track step validity if it contains a form 26 | - `onCancel`, `onFinish` and `onStepChange` callbacks 27 | - Browser navigation with search URL parameter 28 | - You decide what level of control you expose to the user: next, previous, jump to state, finish, cancel, etc... 29 | - Place summary, controls, etc... in header or footer 30 | - Support for multiple components per view 31 | 32 | 33 | ## Breaking changes with 1.1.x 34 | 35 | See changelog and migration guide: 36 | 37 | - [Changelog](./CHANGELOG.md) 38 | - [Migration to 1.1.x](./docs/migrating-to-1.1.x.md) 39 | 40 | ## Examples 41 | 42 | - [Getting started](http://blog.reactandbethankful.com/angular-multi-step-form/#/getting-started) 43 | - [Using forms](http://blog.reactandbethankful.com/angular-multi-step-form/#/using-forms) 44 | - [Saving data](http://blog.reactandbethankful.com/angular-multi-step-form/#/saving-data) 45 | - [CSS transitions](http://blog.reactandbethankful.com/angular-multi-step-form/#/css-transitions) 46 | - [Cancel and Finish](http://blog.reactandbethankful.com/angular-multi-step-form/#/cancel-finish) 47 | - [Browser navigation](http://blog.reactandbethankful.com/angular-multi-step-form/#/browser-navigation) 48 | - [Inside a modal](http://blog.reactandbethankful.com/angular-multi-step-form/#/inside-modal) 49 | 50 | 51 | ## Docs 52 | 53 | - [Configuring your steps](./docs/configuring-steps.md) 54 | - [The multiStepContainer directive](./docs/multi-step-container.md) 55 | - [Steps and directive scopes](./docs/scopes.md) 56 | - [Multi step form instance object](./docs/multi-step-instance.md) 57 | - [Animations, navigation, callbacks](./docs/steps-lifecycle.md) 58 | - [Advanced guide](./docs/advanced-guide.md) 59 | 60 | 61 | ## Getting started 62 | 63 | Grab the sources with bower, npm or download from Github: [https://github.com/troch/angular-multi-step-form/tree/master/dist](./dist): 64 | 65 | ```sh 66 | $ npm install --save angular-multi-step-form; 67 | $ bower install --save angular-multi-step-form 68 | ``` 69 | 70 | Include `multiStepForm` module in your app: 71 | 72 | ```javascript 73 | angular.module('yourApp', [ 74 | 'multiStepForm' 75 | ]); 76 | ``` 77 | 78 | Or (with npm): 79 | 80 | ```javascript 81 | import multiStepForm from 'angular-multi-step-form'; 82 | 83 | angular.module('yourApp', [ 84 | multiStepForm.name 85 | ]); 86 | ``` 87 | 88 | You can then configure your steps 89 | 90 | ```javascript 91 | $scope.steps = [ 92 | { 93 | template: 'Hello ' 94 | }, 95 | { 96 | template: 'World ' 97 | } 98 | ]; 99 | ``` 100 | 101 | And start your multiple step form / wizard: 102 | - Use the `multiStepContainer` directive 103 | - You need to use the `stepContainer` inside `multiStepContainer` to tell it where to load steps. 104 | 105 | ```html 106 | 107 | 108 | 109 | ``` 110 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-multi-step-form", 3 | "version": "1.3.0", 4 | "homepage": "https://github.com/troch/angular-multi-step-form", 5 | "authors": [ 6 | "Thomas Roch " 7 | ], 8 | "description": "An Angular module for creating wizards and multi step forms", 9 | "main": "dist/browser/angular-multi-step-form.js", 10 | "license": "ISC", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "coverage", 16 | "src", 17 | "tests", 18 | "gulpfile.js", 19 | "karma.config.js", 20 | "package.json", 21 | "npm-shrinkwrap.json" 22 | ], 23 | "dependencies": { 24 | "angular": ">=1.3.0" 25 | }, 26 | "devDependencies": { 27 | "angular": "~1.3.0", 28 | "angular-animate": "~1.3.0", 29 | "angular-mocks": "~1.3.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/browser/angular-multi-step-form.js: -------------------------------------------------------------------------------- 1 | var angularMultiStepForm = (function (angular$1) { 2 | 'use strict'; 3 | 4 | angular$1 = 'default' in angular$1 ? angular$1['default'] : angular$1; 5 | 6 | var multiStepContainer = ['$animate', '$q', '$controller', 'multiStepForm', 'FormStep', 'formStepElement', multiStepContainer$1]; 7 | 8 | /** 9 | * @ngdoc directive 10 | * @name multiStepForm:multiStepContainer 11 | * 12 | * @requires $scope 13 | * @requires $q 14 | * @requires $controller 15 | * @requires multiStepForm:multiStepForm 16 | * @requires multiStepForm:FormStep 17 | * @requires multiStepForm:formStepElement 18 | * 19 | * @scope 20 | * @description Multi step directive (overall container) 21 | */ 22 | function multiStepContainer$1($animate, $q, $controller, multiStepForm, FormStep, formStepElement) { 23 | return { 24 | restrict: 'EA', 25 | scope: true, 26 | /** 27 | * @ngdoc controller 28 | * @name multiStepForm:MultiStepFormCtrl 29 | * 30 | * @requires $scope 31 | * @requires multiStepForm:multiStepForm 32 | * 33 | * @description Controls a multi step form and track progress. 34 | */ 35 | controller: ['$scope', function ($scope) { 36 | /** 37 | * @ngdoc function 38 | * @methodOf multiStepForm:MultiStepFormCtrl 39 | * 40 | * @description Register the step container element. This method 41 | * is invoked in the post link function of directive 42 | * {@link multiStepForm:formStepContainer formStepContainer} 43 | * @param {JQLite} elm The step form container 44 | */ 45 | this.setStepContainer = function (elm) { 46 | this.stepContainer = elm; 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 | var onFinish = attrs.onFinish ? resolve(attrs.onFinish) : defaultResolve; 76 | var onCancel = attrs.onCancel ? resolve(attrs.onCancel) : defaultResolve; 77 | var onStepChange = attrs.onStepChange ? function () { 78 | return scope.$eval(attrs.onStepChange); 79 | } : angular$1.noop; 80 | // Step container (populated by child post link function) 81 | var stepContainer = controller.stepContainer; 82 | var multiStepFormInstance = multiStepForm(scope.$eval(attrs.searchId)); 83 | 84 | // Augment scope 85 | multiStepFormInstance.augmentScope(scope); 86 | 87 | // Controller 88 | if (attrs.controller) { 89 | var customController = $controller(attrs.controller, { 90 | $scope: scope, 91 | $element: element, 92 | multiStepFormInstance: multiStepFormInstance 93 | }); 94 | // controllerAs 95 | if (attrs.controllerAs) { 96 | scope[attrs.controllerAs] = customController; 97 | } 98 | } 99 | 100 | // Initial step 101 | var initialStep = scope.$eval(attrs.initialStep); 102 | 103 | var currentLeaveAnimation = void 0, 104 | currentEnterAnimation = void 0, 105 | currentStepScope = void 0, 106 | currentStepElement = void 0, 107 | isDeferredResolved = false; 108 | 109 | // Resolve any outstanding promises on destroy 110 | scope.$on('$destroy', function () { 111 | if (!isDeferredResolved) { 112 | isDeferredResolved = true; 113 | multiStepFormInstance.deferred.reject(); 114 | } 115 | }); 116 | 117 | // Initialise and start the multi step form 118 | multiStepFormInstance.start(scope.formSteps).then(onFinish, onCancel, function (data) { 119 | var step = data.newStep; 120 | var previousStep = data.oldStep; 121 | var direction = angular$1.isDefined(previousStep) ? step < previousStep ? 'step-backward' : 'step-forward' : 'step-initial'; 122 | 123 | var formStep = scope.formSteps[step - 1]; 124 | // Create new step element (promise) 125 | var newStepElement = formStepElement(formStep, multiStepFormInstance, scope); 126 | 127 | // Add direction class to the parent container; 128 | stepContainer.removeClass('step-forward step-backward step-initial').addClass(direction); 129 | 130 | // Cancel current leave animation if any 131 | if (currentLeaveAnimation) { 132 | $animate.cancel(currentLeaveAnimation); 133 | } 134 | // Cancel current enter animation if any 135 | if (currentEnterAnimation) { 136 | $animate.cancel(currentEnterAnimation); 137 | } 138 | // Destroy current scope 139 | if (currentStepScope) { 140 | currentStepScope.$destroy(); 141 | } 142 | // Leave current step if any 143 | if (currentStepElement) { 144 | currentLeaveAnimation = $animate.leave(currentStepElement); 145 | } 146 | // Enter new step when new step element is ready 147 | newStepElement.then(function (step) { 148 | onStepChange(); 149 | currentStepScope = step.scope; 150 | currentStepElement = step.element; 151 | currentStepElement.scrollTop = 0; 152 | stepContainer.scrollTop = 0; 153 | currentEnterAnimation = $animate.enter(currentStepElement, stepContainer); 154 | }, function () { 155 | throw new Error('Could not load step ' + step); 156 | }); 157 | }); 158 | 159 | // Initialise currentStep 160 | multiStepFormInstance.setInitialIndex(initialStep); 161 | 162 | // Default resolution function 163 | function defaultResolve() { 164 | $animate.leave(element); 165 | isDeferredResolved = true; 166 | scope.$destroy(); 167 | } 168 | 169 | // On promise resolution 170 | function resolve(fn) { 171 | return function () { 172 | isDeferredResolved = true; 173 | scope.$eval(fn); 174 | }; 175 | } 176 | } 177 | } 178 | }; 179 | } 180 | 181 | var formStepValidity = ['$parse', formStepValidity$1]; 182 | 183 | /** 184 | * @ngdoc directive 185 | * @name multiStepForm:formStepValidity 186 | * 187 | * @restrict A 188 | * @description Notify the multi step form instance of a change of validity of a step. 189 | * This directive can be used on a form element or within a form. 190 | */ 191 | function formStepValidity$1($parse) { 192 | return { 193 | restrict: 'A', 194 | require: '^form', 195 | link: function postLink(scope, element, attrs, formCtrl) { 196 | // The callback to call when a change of validity 197 | // is detected 198 | var validtyChangeCallback = attrs.formStepValidity ? $parse(attrs.formStepValidity).bind(scope, scope) : scope.$setValidity; 199 | 200 | // Watch the form validity 201 | scope.$watch(function () { 202 | return formCtrl.$valid; 203 | }, function (val) { 204 | // Check if defined 205 | if (angular$1.isDefined(val)) { 206 | validtyChangeCallback(val); 207 | } 208 | }); 209 | } 210 | }; 211 | } 212 | 213 | /** 214 | * @ngdoc directive 215 | * @name multiStepForm:stepContainer 216 | * 217 | * @requires multiStepForm:stepContainer 218 | * 219 | * @restrict A 220 | * @description The container for form steps. It registers itself with the multi step container. 221 | * {@link multiStepForm:multiStepContainer multiStepContainer} 222 | */ 223 | function stepContainer() { 224 | return { 225 | restrict: 'EA', 226 | require: '^^multiStepContainer', 227 | scope: false, 228 | link: function postLink(scope, element, attrs, multiStepCtrl) { 229 | element.addClass('multi-step-body'); 230 | multiStepCtrl.setStepContainer(element); 231 | } 232 | }; 233 | } 234 | 235 | var formStepElement = ['$compile', '$controller', '$http', '$injector', '$q', '$templateCache', formStepElement$1]; 236 | 237 | /** 238 | * @ngdoc function 239 | * @name multiStepForm:formStepElement 240 | * 241 | * @requires $rootScope 242 | * @requires $controller 243 | * 244 | * @description A factory function for creating form step elements 245 | * (using controller, template and resolve) 246 | */ 247 | function formStepElement$1($compile, $controller, $http, $injector, $q, $templateCache) { 248 | /** 249 | * Resolve the template of a form step 250 | * @param {FormStep} formStep The form step object 251 | * @return {Promise|String} A promise containing the template 252 | */ 253 | function resolveTemplate(formStep) { 254 | if (formStep.template) { 255 | // If function or array, use $injector to get template value 256 | return angular$1.isFunction(formStep.template) || angular$1.isArray(formStep.template) ? $injector.$invoke(formStep.template) : formStep.template; 257 | } 258 | // Use templateUrl 259 | var templateUrl = 260 | // If function or array, use $injector to get templateUrl value 261 | angular$1.isFunction(formStep.templateUrl) || angular$1.isArray(formStep.templateUrl) ? $injector.$invoke(formStep.templateUrl) : formStep.templateUrl; 262 | // Request templateUrl using $templateCache 263 | return $http.get(templateUrl, { cache: $templateCache }); 264 | } 265 | 266 | /** 267 | * Create a new scope with the multiStepContainer being the parent scope. 268 | * augmented with multi step form control methods. 269 | * @param {FormStep} formStep The form step object 270 | * @param {FormStep} formStep The form step object 271 | * @return {Object} The form step scope 272 | */ 273 | function getScope(scope, formStep, multiStepFormInstance) { 274 | var stepScope = scope.$new(formStep.isolatedScope); 275 | // Augment scope with multi step form instance methods 276 | multiStepFormInstance.augmentScope(stepScope); 277 | 278 | return stepScope; 279 | } 280 | 281 | /** 282 | * Create a form step element, compiled with controller and dependencies resolved 283 | * 284 | * @param {FormStep} formStep The form step object 285 | * @param {Object} multiStepFormInstance The multi step form instance 286 | * @param {Object} multiStepFormScope The scope instance of the multi step form 287 | * @return {Promise} A promise containing the form step element 288 | */ 289 | return function formStepElementFactory(formStep, multiStepFormInstance, multiStepFormScope) { 290 | var formStepElement = angular$1.element('
').addClass('form-step'); 291 | 292 | var controller = void 0, 293 | template = void 0, 294 | promisesHash = {}; 295 | 296 | // Get template 297 | promisesHash.$template = resolveTemplate(formStep); 298 | 299 | // Get resolve 300 | angular$1.forEach(formStep.resolve, function (resolveVal, resolveName) { 301 | promisesHash[resolveName] = 302 | // angular.isString(resolveVal) ? 303 | // $injector.get(resolveVal) : 304 | $injector.invoke(resolveVal); 305 | }); 306 | 307 | // After all locals are resolved (template and "resolves") // 308 | return $q.all(promisesHash).then(function (locals) { 309 | // Extend formStep locals with resolved locals 310 | locals = angular$1.extend({}, formStep.locals, locals); 311 | // Load template inside element 312 | locals.$template = locals.$template.data || locals.$template; 313 | formStepElement.html(locals.$template); 314 | // Create scope 315 | var formStepScope = getScope(multiStepFormScope, formStep, multiStepFormInstance); 316 | 317 | if (formStep.controller) { 318 | // Create form step scope 319 | locals.$scope = formStepScope; 320 | // Add multi step form service instance to local injectables 321 | locals.multiStepFormInstance = multiStepFormInstance; 322 | // Add multi step form scope to local injectables if isolated 323 | if (formStep.isolatedScope) { 324 | locals.multiStepFormScope = multiStepFormScope; 325 | } 326 | // Instanciate controller 327 | controller = $controller(formStep.controller, locals); 328 | // controllerAs 329 | if (formStep.controllerAs) { 330 | formStepScope[formStep.controllerAs] = controller; 331 | } 332 | formStepElement.data('$stepController', controller); 333 | // formStepElement.children().data('$ngControllerController', controller); 334 | } 335 | 336 | // Compile form step element and link with scope 337 | $compile(formStepElement)(formStepScope); 338 | 339 | // Return element and scope 340 | return { 341 | element: formStepElement, 342 | scope: formStepScope 343 | }; 344 | }); 345 | }; 346 | } 347 | 348 | /** 349 | * @ngdoc object 350 | * @name multiStepForm:FormStep 351 | * 352 | * @description A constructor for creating form steps 353 | * @error If no template or templateUrl properties are supplied 354 | */ 355 | function FormStep() { 356 | return function FormStep(config) { 357 | if (!config.template && !config.templateUrl) { 358 | throw new Error('Either template or templateUrl properties have to be provided for' + ' multi step form' + config.title); 359 | } 360 | 361 | /** 362 | * @ngdoc property 363 | * @propertyOf multiStepForm:FormStep 364 | * 365 | * @description The form step title 366 | * @type {String} 367 | */ 368 | this.title = config.title; 369 | 370 | /** 371 | * @ngdoc property 372 | * @propertyOf multiStepForm:FormStep 373 | * 374 | * @description The form step additional data 375 | * @type {Object} 376 | */ 377 | this.data = config.data || {}; 378 | 379 | /** 380 | * @ngdoc property 381 | * @propertyOf multiStepForm:FormStep 382 | * 383 | * @description The form step controller 384 | * @type {String|Function|Array} 385 | */ 386 | this.controller = config.controller; 387 | 388 | /** 389 | * @ngdoc property 390 | * @propertyOf multiStepForm:FormStep 391 | * 392 | * @description An identifier name for a reference to the controller 393 | * @type {String} 394 | */ 395 | this.controllerAs = config.controllerAs; 396 | 397 | /** 398 | * @ngdoc property 399 | * @propertyOf multiStepForm:FormStep 400 | * 401 | * @description The form step template 402 | * @type {String} 403 | */ 404 | this.template = config.template; 405 | 406 | /** 407 | * @ngdoc property 408 | * @propertyOf multiStepForm:FormStep 409 | * 410 | * @description The form step template URL 411 | * @type {String} 412 | */ 413 | this.templateUrl = config.templateUrl; 414 | 415 | /** 416 | * Whether or not the form step should have an isolated scope 417 | * @type {Boolean} 418 | */ 419 | this.isolatedScope = config.isolatedScope || false; 420 | 421 | /** 422 | * @ngdoc property 423 | * @propertyOf multiStepForm:resolve 424 | * 425 | * @description The form step resolve map (same use than for routes) 426 | * @type {Object} 427 | */ 428 | this.resolve = config.resolve || {}; 429 | 430 | /** 431 | * @ngdoc property 432 | * @propertyOf multiStepForm:resolve 433 | * 434 | * @description The form step locals map (same than resolve but for non deferred values) 435 | * Note: resolve also works with non deferred values 436 | * @type {Object} 437 | */ 438 | this.locals = config.locals || {}; 439 | 440 | /** 441 | * @ngdoc property 442 | * @propertyOf multiStepForm:FormStep 443 | * 444 | * @description Whether or not this form step contains a form 445 | * @type {Boolean} 446 | */ 447 | this.hasForm = config.hasForm || false; 448 | 449 | /** 450 | * @ngdoc property 451 | * @propertyOf multiStepForm:FormStep 452 | * 453 | * @description Whether or not this form step is valid. 454 | * Form validity can been fed back using a specific directive. 455 | * {@link multiStepForm:formStepValidity formStepValidity} 456 | * @type {Boolean} 457 | */ 458 | this.valid = false; 459 | 460 | /** 461 | * @ngdoc property 462 | * @propertyOf multiStepForm:FormStep 463 | * 464 | * @description Whether or not this step has been visited 465 | * (i.e. the user has moved to the next step) 466 | * @type {Boolean} 467 | */ 468 | this.visited = false; 469 | }; 470 | } 471 | 472 | var multiStepForm = ['$q', '$location', '$rootScope', multiStepForm$1]; 473 | 474 | /** 475 | * @ngdoc function 476 | * @name multiStepForm:multiStepForm 477 | * 478 | * @requires $q 479 | * @requires multiStepForm:FormStep 480 | * 481 | * @description A service returning an instance per multi step form. 482 | * The instance of the service is injected in each step controller. 483 | */ 484 | function multiStepForm$1($q, $location, $rootScope) { 485 | function MultiFormStep(searchId) { 486 | var _this = this; 487 | 488 | /** 489 | * @ngdoc property 490 | * @propertyOf multiStepForm:multiStepForm 491 | * 492 | * @description The location search property name to store the active step index. 493 | * @type {String} 494 | */ 495 | this.searchId = searchId; 496 | // If the search id is defined, 497 | if (angular.isDefined(searchId)) { 498 | $rootScope.$on('$locationChangeSuccess', function (event) { 499 | var searchIndex = parseInt($location.search()[_this.searchId]); 500 | 501 | if (!isNaN(searchIndex) && _this.activeIndex !== searchIndex) { 502 | // Synchronise 503 | _this.setActiveIndex(parseInt(searchIndex)); 504 | } 505 | }); 506 | } 507 | 508 | /** 509 | * @ngdoc property 510 | * @propertyOf multiStepForm:multiStepForm 511 | * 512 | * @description The form steps 513 | * @type {Array} 514 | */ 515 | this.steps = []; 516 | 517 | /** 518 | * @ngdoc property 519 | * @propertyOf multiStepForm:multiStepForm 520 | * 521 | * @description Return the form steps 522 | * @return {Array} 523 | */ 524 | this.getSteps = function () { 525 | return this.steps; 526 | }; 527 | 528 | /** 529 | * @ngdoc property 530 | * @propertyOf multiStepForm:multiStepForm 531 | * 532 | * @description The multi-step form deferred object 533 | * @type {Deferred} 534 | */ 535 | this.deferred = $q.defer(); 536 | 537 | /** 538 | * @ngdoc method 539 | * @methodOf multiStepForm:multiStepForm 540 | * 541 | * @description Initialise the form steps and start 542 | * @error If no steps are provided 543 | * @param {Array} steps The form steps 544 | * @return {Promise} A promise which will be resolved when all steps are passed, 545 | * and rejected if the user cancel the multi step form. 546 | */ 547 | this.start = function (steps) { 548 | if (!steps || !steps.length) { 549 | throw new Error('At least one step has to be defined'); 550 | } 551 | // Initialise steps 552 | this.steps = steps; 553 | // Return promise 554 | return this.deferred.promise; 555 | }; 556 | 557 | /** 558 | * @ngdoc method 559 | * @methodOf multiStepForm:multiStepForm 560 | * 561 | * @description Cancel this multi step form 562 | * @type {Array} 563 | */ 564 | this.cancel = function () { 565 | this.deferred.reject('cancelled'); 566 | }; 567 | 568 | /** 569 | * @ngdoc method 570 | * @methodOf multiStepForm:multiStepForm 571 | * 572 | * @description Finish this multi step form (success) 573 | */ 574 | this.finish = function () { 575 | this.deferred.resolve(); 576 | }; 577 | 578 | /** 579 | * @ngdoc method 580 | * @methodOf multiStepForm:multiStepForm 581 | * 582 | * @description Return the multi form current step 583 | * @return {Number} The active step index (starting at 1) 584 | */ 585 | this.getActiveIndex = function () { 586 | return this.activeIndex; 587 | }; 588 | 589 | /** 590 | * @ngdoc method 591 | * @methodOf multiStepForm:multiStepForm 592 | * 593 | * @description Initialise the multi step form instance with 594 | * an initial index 595 | * @param {Number} step The index to start with 596 | */ 597 | this.setInitialIndex = function (initialStep) { 598 | var searchIndex = void 0; 599 | // Initial step in markup has the priority 600 | // to override any manually entered URL 601 | if (angular.isDefined(initialStep)) { 602 | return this.setActiveIndex(initialStep); 603 | } 604 | // Otherwise use search ID if applicable 605 | if (this.searchId) { 606 | searchIndex = parseInt($location.search()[this.searchId]); 607 | if (!isNaN(searchIndex)) { 608 | return this.setActiveIndex(searchIndex); 609 | } 610 | } 611 | // Otherwise set to 1 612 | this.setActiveIndex(1); 613 | }; 614 | 615 | /** 616 | * @ngdoc method 617 | * @methodOf multiStepForm:multiStepForm 618 | * 619 | * @description Set the current step to the provided value and notify 620 | * @param {Number} step The step index (starting at 1) 621 | */ 622 | this.setActiveIndex = function (step) { 623 | if (this.searchId) { 624 | // Update $location 625 | if (this.activeIndex) { 626 | $location.search(this.searchId, step); 627 | } else { 628 | // Replace current one 629 | $location.search(this.searchId, step).replace(); 630 | } 631 | } 632 | // Notify deferred object 633 | this.deferred.notify({ 634 | newStep: step, 635 | oldStep: this.activeIndex 636 | }); 637 | // Update activeIndex 638 | this.activeIndex = step; 639 | }; 640 | 641 | /** 642 | * @ngdoc method 643 | * @methodOf multiStepForm:multiStepForm 644 | * 645 | * @description Return the active form step object 646 | * @return {FormStep} The active form step 647 | */ 648 | this.getActiveStep = function () { 649 | if (this.activeIndex) { 650 | return this.steps[this.activeIndex - 1]; 651 | } 652 | }; 653 | 654 | /** 655 | * @ngdoc method 656 | * @methodOf multiStepForm:multiStepForm 657 | * 658 | * @description Check if the current step is the first step 659 | * @return {Boolean} Whether or not the current step is the first step 660 | */ 661 | this.isFirst = function () { 662 | return this.activeIndex === 1; 663 | }; 664 | 665 | /** 666 | * @ngdoc method 667 | * @methodOf multiStepForm:multiStepForm 668 | * 669 | * @description Check if the current step is the last step 670 | * @return {Boolean} Whether or not the current step is the last step 671 | */ 672 | this.isLast = function () { 673 | return this.activeIndex === this.steps.length; 674 | }; 675 | 676 | /** 677 | * @ngdoc method 678 | * @methodOf multiStepForm:multiStepForm 679 | * 680 | * @description Go to the next step, if not the last step 681 | */ 682 | this.nextStep = function () { 683 | if (!this.isLast()) { 684 | this.setActiveIndex(this.activeIndex + 1); 685 | } 686 | }; 687 | 688 | /** 689 | * @ngdoc method 690 | * @methodOf multiStepForm:multiStepForm 691 | * 692 | * @description Go to the next step, if not the first step 693 | */ 694 | this.previousStep = function () { 695 | if (!this.isFirst()) { 696 | this.setActiveIndex(this.activeIndex - 1); 697 | } 698 | }; 699 | 700 | /** 701 | * @ngdoc method 702 | * @methodOf multiStepForm:multiStepForm 703 | * 704 | * @description Go to the next step, if not the first step 705 | */ 706 | this.setValidity = function (isValid, stepIndex) { 707 | var step = this.steps[(stepIndex || this.activeIndex) - 1]; 708 | 709 | if (step) { 710 | step.valid = isValid; 711 | } 712 | }; 713 | 714 | /** 715 | * @ngdoc method 716 | * @methodOf multiStepForm:multiStepForm 717 | * 718 | * @description Augment a scope with useful methods from this object 719 | * @param {Object} scope The scope to augment 720 | */ 721 | this.augmentScope = function (scope) { 722 | var _this2 = this; 723 | 724 | ['cancel', 'finish', 'getActiveIndex', 'setActiveIndex', 'getActiveStep', 'getSteps', 'nextStep', 'previousStep', 'isFirst', 'isLast', 'setValidity'].forEach(function (method) { 725 | scope['$' + method] = _this2[method].bind(_this2); 726 | }); 727 | }; 728 | } 729 | 730 | return function multiStepFormProvider(searchId) { 731 | return new MultiFormStep(searchId); 732 | }; 733 | } 734 | 735 | /** 736 | * @ngdoc module 737 | * @name multiStepForm 738 | */ 739 | var multiStepFormModule = angular$1.module('multiStepForm', [/*'ngAnimate'*/]); 740 | 741 | multiStepFormModule.directive('formStepValidity', formStepValidity).directive('multiStepContainer', multiStepContainer).directive('stepContainer', stepContainer).factory('formStepElement', formStepElement).factory('FormStep', FormStep).factory('multiStepForm', multiStepForm); 742 | 743 | return multiStepFormModule; 744 | 745 | }(angular)); -------------------------------------------------------------------------------- /dist/browser/angular-multi-step-form.min.js: -------------------------------------------------------------------------------- 1 | var angularMultiStepForm=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),C=!0,i.$destroy()}function p(t){return function(){C=!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,C=!1;i.$on("$destroy",function(){C||(C=!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}(angular); -------------------------------------------------------------------------------- /dist/commonjs/directives/form-step-validity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _angular = require('angular'); 8 | 9 | var _angular2 = _interopRequireDefault(_angular); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | exports.default = ['$parse', formStepValidity]; 14 | 15 | /** 16 | * @ngdoc directive 17 | * @name multiStepForm:formStepValidity 18 | * 19 | * @restrict A 20 | * @description Notify the multi step form instance of a change of validity of a step. 21 | * This directive can be used on a form element or within a form. 22 | */ 23 | 24 | function formStepValidity($parse) { 25 | return { 26 | restrict: 'A', 27 | require: '^form', 28 | link: function postLink(scope, element, attrs, formCtrl) { 29 | // The callback to call when a change of validity 30 | // is detected 31 | var validtyChangeCallback = attrs.formStepValidity ? $parse(attrs.formStepValidity).bind(scope, scope) : scope.$setValidity; 32 | 33 | // Watch the form validity 34 | scope.$watch(function () { 35 | return formCtrl.$valid; 36 | }, function (val) { 37 | // Check if defined 38 | if (_angular2.default.isDefined(val)) { 39 | validtyChangeCallback(val); 40 | } 41 | }); 42 | } 43 | }; 44 | } -------------------------------------------------------------------------------- /dist/commonjs/directives/multi-step-container.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _angular = require('angular'); 8 | 9 | var _angular2 = _interopRequireDefault(_angular); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | exports.default = ['$animate', '$q', '$controller', 'multiStepForm', 'FormStep', 'formStepElement', multiStepContainer]; 14 | 15 | /** 16 | * @ngdoc directive 17 | * @name multiStepForm:multiStepContainer 18 | * 19 | * @requires $scope 20 | * @requires $q 21 | * @requires $controller 22 | * @requires multiStepForm:multiStepForm 23 | * @requires multiStepForm:FormStep 24 | * @requires multiStepForm:formStepElement 25 | * 26 | * @scope 27 | * @description Multi step directive (overall container) 28 | */ 29 | 30 | function multiStepContainer($animate, $q, $controller, multiStepForm, FormStep, formStepElement) { 31 | return { 32 | restrict: 'EA', 33 | scope: true, 34 | /** 35 | * @ngdoc controller 36 | * @name multiStepForm:MultiStepFormCtrl 37 | * 38 | * @requires $scope 39 | * @requires multiStepForm:multiStepForm 40 | * 41 | * @description Controls a multi step form and track progress. 42 | */ 43 | controller: ['$scope', function ($scope) { 44 | /** 45 | * @ngdoc function 46 | * @methodOf multiStepForm:MultiStepFormCtrl 47 | * 48 | * @description Register the step container element. This method 49 | * is invoked in the post link function of directive 50 | * {@link multiStepForm:formStepContainer formStepContainer} 51 | * @param {JQLite} elm The step form container 52 | */ 53 | this.setStepContainer = function (elm) { 54 | this.stepContainer = elm; 55 | }; 56 | }], 57 | link: { 58 | pre: function preLink(scope, element, attrs) { 59 | /** 60 | * @ngdoc property 61 | * @propertyOf multiStepForm:MultiStepFormContainer 62 | * @description The form steps 63 | * @type {FormStep[]} 64 | */ 65 | scope.formSteps = scope.$eval(attrs.steps).map(function (step) { 66 | return new FormStep(step); 67 | }); 68 | 69 | /** 70 | * @ngdoc property 71 | * @propertyOf multiStepForm:MultiStepFormContainer 72 | * @description The form step titles 73 | * @type {String[]} 74 | */ 75 | scope.stepTitles = scope.formSteps.map(function (step) { 76 | return step.title; 77 | }); 78 | }, 79 | post: function postLink(scope, element, attrs, controller) { 80 | // Add .multi-step-container class 81 | element.addClass('multi-step-container'); 82 | // Callbacks 83 | var onFinish = attrs.onFinish ? resolve(attrs.onFinish) : defaultResolve; 84 | var onCancel = attrs.onCancel ? resolve(attrs.onCancel) : defaultResolve; 85 | var onStepChange = attrs.onStepChange ? function () { 86 | return scope.$eval(attrs.onStepChange); 87 | } : _angular2.default.noop; 88 | // Step container (populated by child post link function) 89 | var stepContainer = controller.stepContainer; 90 | var multiStepFormInstance = multiStepForm(scope.$eval(attrs.searchId)); 91 | 92 | // Augment scope 93 | multiStepFormInstance.augmentScope(scope); 94 | 95 | // Controller 96 | if (attrs.controller) { 97 | var customController = $controller(attrs.controller, { 98 | $scope: scope, 99 | $element: element, 100 | multiStepFormInstance: multiStepFormInstance 101 | }); 102 | // controllerAs 103 | if (attrs.controllerAs) { 104 | scope[attrs.controllerAs] = customController; 105 | } 106 | } 107 | 108 | // Initial step 109 | var initialStep = scope.$eval(attrs.initialStep); 110 | 111 | var currentLeaveAnimation = void 0, 112 | currentEnterAnimation = void 0, 113 | currentStepScope = void 0, 114 | currentStepElement = void 0, 115 | isDeferredResolved = false; 116 | 117 | // Resolve any outstanding promises on destroy 118 | scope.$on('$destroy', function () { 119 | if (!isDeferredResolved) { 120 | isDeferredResolved = true; 121 | multiStepFormInstance.deferred.reject(); 122 | } 123 | }); 124 | 125 | // Initialise and start the multi step form 126 | multiStepFormInstance.start(scope.formSteps).then(onFinish, onCancel, function (data) { 127 | var step = data.newStep; 128 | var previousStep = data.oldStep; 129 | var direction = _angular2.default.isDefined(previousStep) ? step < previousStep ? 'step-backward' : 'step-forward' : 'step-initial'; 130 | 131 | var formStep = scope.formSteps[step - 1]; 132 | // Create new step element (promise) 133 | var newStepElement = formStepElement(formStep, multiStepFormInstance, scope); 134 | 135 | // Add direction class to the parent container; 136 | stepContainer.removeClass('step-forward step-backward step-initial').addClass(direction); 137 | 138 | // Cancel current leave animation if any 139 | if (currentLeaveAnimation) { 140 | $animate.cancel(currentLeaveAnimation); 141 | } 142 | // Cancel current enter animation if any 143 | if (currentEnterAnimation) { 144 | $animate.cancel(currentEnterAnimation); 145 | } 146 | // Destroy current scope 147 | if (currentStepScope) { 148 | currentStepScope.$destroy(); 149 | } 150 | // Leave current step if any 151 | if (currentStepElement) { 152 | currentLeaveAnimation = $animate.leave(currentStepElement); 153 | } 154 | // Enter new step when new step element is ready 155 | newStepElement.then(function (step) { 156 | onStepChange(); 157 | currentStepScope = step.scope; 158 | currentStepElement = step.element; 159 | currentStepElement.scrollTop = 0; 160 | stepContainer.scrollTop = 0; 161 | currentEnterAnimation = $animate.enter(currentStepElement, stepContainer); 162 | }, function () { 163 | throw new Error('Could not load step ' + step); 164 | }); 165 | }); 166 | 167 | // Initialise currentStep 168 | multiStepFormInstance.setInitialIndex(initialStep); 169 | 170 | // Default resolution function 171 | function defaultResolve() { 172 | $animate.leave(element); 173 | isDeferredResolved = true; 174 | scope.$destroy(); 175 | } 176 | 177 | // On promise resolution 178 | function resolve(fn) { 179 | return function () { 180 | isDeferredResolved = true; 181 | scope.$eval(fn); 182 | }; 183 | } 184 | } 185 | } 186 | }; 187 | } -------------------------------------------------------------------------------- /dist/commonjs/directives/step-container.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = stepContainer; 7 | 8 | /** 9 | * @ngdoc directive 10 | * @name multiStepForm:stepContainer 11 | * 12 | * @requires multiStepForm:stepContainer 13 | * 14 | * @restrict A 15 | * @description The container for form steps. It registers itself with the multi step container. 16 | * {@link multiStepForm:multiStepContainer multiStepContainer} 17 | */ 18 | 19 | function stepContainer() { 20 | return { 21 | restrict: 'EA', 22 | require: '^^multiStepContainer', 23 | scope: false, 24 | link: function postLink(scope, element, attrs, multiStepCtrl) { 25 | element.addClass('multi-step-body'); 26 | multiStepCtrl.setStepContainer(element); 27 | } 28 | }; 29 | } -------------------------------------------------------------------------------- /dist/commonjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _angular = require('angular'); 8 | 9 | var _angular2 = _interopRequireDefault(_angular); 10 | 11 | var _multiStepContainer = require('./directives/multi-step-container'); 12 | 13 | var _multiStepContainer2 = _interopRequireDefault(_multiStepContainer); 14 | 15 | var _formStepValidity = require('./directives/form-step-validity'); 16 | 17 | var _formStepValidity2 = _interopRequireDefault(_formStepValidity); 18 | 19 | var _stepContainer = require('./directives/step-container'); 20 | 21 | var _stepContainer2 = _interopRequireDefault(_stepContainer); 22 | 23 | var _formStepElementFactory = require('./services/form-step-element-factory'); 24 | 25 | var _formStepElementFactory2 = _interopRequireDefault(_formStepElementFactory); 26 | 27 | var _formStepObject = require('./services/form-step-object'); 28 | 29 | var _formStepObject2 = _interopRequireDefault(_formStepObject); 30 | 31 | var _multiStepFormFactory = require('./services/multi-step-form-factory'); 32 | 33 | var _multiStepFormFactory2 = _interopRequireDefault(_multiStepFormFactory); 34 | 35 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 36 | 37 | /** 38 | * @ngdoc module 39 | * @name multiStepForm 40 | */ 41 | var multiStepFormModule = _angular2.default.module('multiStepForm', [/*'ngAnimate'*/]); 42 | 43 | multiStepFormModule.directive('formStepValidity', _formStepValidity2.default).directive('multiStepContainer', _multiStepContainer2.default).directive('stepContainer', _stepContainer2.default).factory('formStepElement', _formStepElementFactory2.default).factory('FormStep', _formStepObject2.default).factory('multiStepForm', _multiStepFormFactory2.default); 44 | 45 | exports.default = multiStepFormModule; -------------------------------------------------------------------------------- /dist/commonjs/services/form-step-element-factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _angular = require('angular'); 8 | 9 | var _angular2 = _interopRequireDefault(_angular); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | exports.default = ['$compile', '$controller', '$http', '$injector', '$q', '$templateCache', formStepElement]; 14 | 15 | /** 16 | * @ngdoc function 17 | * @name multiStepForm:formStepElement 18 | * 19 | * @requires $rootScope 20 | * @requires $controller 21 | * 22 | * @description A factory function for creating form step elements 23 | * (using controller, template and resolve) 24 | */ 25 | 26 | function formStepElement($compile, $controller, $http, $injector, $q, $templateCache) { 27 | /** 28 | * Resolve the template of a form step 29 | * @param {FormStep} formStep The form step object 30 | * @return {Promise|String} A promise containing the template 31 | */ 32 | function resolveTemplate(formStep) { 33 | if (formStep.template) { 34 | // If function or array, use $injector to get template value 35 | return _angular2.default.isFunction(formStep.template) || _angular2.default.isArray(formStep.template) ? $injector.$invoke(formStep.template) : formStep.template; 36 | } 37 | // Use templateUrl 38 | var templateUrl = 39 | // If function or array, use $injector to get templateUrl value 40 | _angular2.default.isFunction(formStep.templateUrl) || _angular2.default.isArray(formStep.templateUrl) ? $injector.$invoke(formStep.templateUrl) : formStep.templateUrl; 41 | // Request templateUrl using $templateCache 42 | return $http.get(templateUrl, { cache: $templateCache }); 43 | } 44 | 45 | /** 46 | * Create a new scope with the multiStepContainer being the parent scope. 47 | * augmented with multi step form control methods. 48 | * @param {FormStep} formStep The form step object 49 | * @param {FormStep} formStep The form step object 50 | * @return {Object} The form step scope 51 | */ 52 | function getScope(scope, formStep, multiStepFormInstance) { 53 | var stepScope = scope.$new(formStep.isolatedScope); 54 | // Augment scope with multi step form instance methods 55 | multiStepFormInstance.augmentScope(stepScope); 56 | 57 | return stepScope; 58 | } 59 | 60 | /** 61 | * Create a form step element, compiled with controller and dependencies resolved 62 | * 63 | * @param {FormStep} formStep The form step object 64 | * @param {Object} multiStepFormInstance The multi step form instance 65 | * @param {Object} multiStepFormScope The scope instance of the multi step form 66 | * @return {Promise} A promise containing the form step element 67 | */ 68 | return function formStepElementFactory(formStep, multiStepFormInstance, multiStepFormScope) { 69 | var formStepElement = _angular2.default.element('
').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 |
63 | This my header content 64 |
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 | '
' + 15 | '' + 16 | '
'; 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 | --------------------------------------------------------------------------------