├── .bowerrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── dist ├── auto-save-form.css ├── auto-save-form.js ├── index.html ├── scripts │ ├── app.js │ └── vendor.js └── styles │ ├── app.css │ └── vendor.css ├── gulp ├── .eslintrc ├── build.js ├── conf.js ├── e2e-tests.js ├── inject.js ├── scripts.js ├── server.js ├── unit-tests.js └── watch.js ├── gulpfile.js ├── index.js ├── karma.conf.js ├── package.json ├── protractor.conf.js └── src ├── app ├── index.config.js ├── index.controller.js ├── index.css ├── index.mock.js └── index.module.js ├── auto-save-form ├── auto-save-form.css └── auto-save-form.js └── index.html /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "plugins": [ 4 | "angular" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "jasmine": true 9 | }, 10 | "globals": { 11 | "angular": true, 12 | "module": true, 13 | "inject": true, 14 | "_": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | .idea/ 4 | .tmp/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tiberiu Zuld 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-auto-save-form 2 | ============== 3 | [![npm version](https://badge.fury.io/js/angular-auto-save-form.svg)](https://badge.fury.io/js/angular-auto-save-form) 4 | [![dependencies Status](https://david-dm.org/tiberiuzuld/angular-auto-save-form/status.svg)](https://david-dm.org/tiberiuzuld/angular-auto-save-form) 5 | [![devDependencies Status](https://david-dm.org/tiberiuzuld/angular-auto-save-form/dev-status.svg)](https://david-dm.org/tiberiuzuld/angular-auto-save-form?type=dev) 6 | [![downloads](https://img.shields.io/npm/dm/angular-auto-save-form.svg)](https://www.npmjs.com/package/angular-auto-save-form) 7 | 8 | ## Description 9 | 10 | Angular auto save form changed inputs. 11 | The directive will call the callback function with a parameter object containing only the inputs that are $dirty. 12 | 13 | #### [Demo](http://tiberiuzuld.github.io/angular-auto-save-form) 14 | 15 | #### Install with Bower 16 | ```bash 17 | bower install angular-auto-save-form --save 18 | ``` 19 | 20 | #### Install with npm 21 | ```bash 22 | npm install angular-auto-save-form --save 23 | ``` 24 | 25 | Then add a ` 30 | 31 | 32 | ``` 33 | 34 | Include 'angular-auto-save-form' as a dependency of your module like this: 35 | ```JavaScript 36 | var module = angular.module("example", ["angular-auto-save-form"]); 37 | ``` 38 | 39 | ## Usage 40 | 41 | ### Default usage: 42 | 43 | Directive requires that form and input elements to have [name] attribute 44 | 45 | ```html 46 | 47 | 48 | 49 | 50 | ``` 51 | 52 | Which expects a scope setup like the following: 53 | ```JavaScript 54 | $scope.user = { name: "Jon Doe", email: "jon.doe@domain.com" }; 55 | 56 | //changing input user.name the callback function will be called with parameter object 57 | $scope.callback = function(controls){ // controls = {'name': 'Jon Doe'} 58 | return $http.post('saveDataUrl', controls); 59 | }; 60 | ``` 61 | 62 | For radio inputs or if you want to group inputs on the same property use the [auto-save-form-property] attribute 63 | on one of the inputs and prefix the input name with a group name 64 | 65 | ```html 66 | 67 | Male 69 | Female 70 | 71 | ``` 72 | The object will look like this: 73 | 74 | ```JavaScript 75 | //{'gender': 'male'} 76 | ``` 77 | 78 | #### Optional attributes: 79 | 80 | If you want to change locally debounce timer 81 | ```html 82 | auto-save-form-debounce="number" default:500 83 | ``` 84 | 85 | If you want to change the debounce at input level use [ng-model-options] directive 86 | 87 | Loading spinner in top right corner of the form enabled by default if callback promise returns a promise. 88 | ```html 89 | auto-save-form-spinner="boolean" default:true 90 | ``` 91 | 92 | ```html 93 | auto-save-form-spinner-position="string" default:'top right' 94 | ``` 95 | 96 | Possible combinations: 'top right', 'top left', 'bottom left', 'bottom right'. 97 | It is possible to add your own class without your desired position. 98 | Example: 99 | ```css 100 | [auto-save-form] .spinner.my-class { 101 | top: 50%; 102 | left: 50%; 103 | } 104 | ``` 105 | ```html 106 | auto-save-form-spinner-position="my-class" 107 | ``` 108 | 109 | 110 | ##### The directive supports nested objects like: 111 | ```JavaScript 112 | user = { 113 | name: 'Jon Doe', 114 | country: { 115 | name: 'French', 116 | city: 'Paris' 117 | } 118 | } 119 | ``` 120 | 121 | ```HTML 122 | 123 | ``` 124 | 125 | ```JavaScript 126 | //callback object 127 | { 128 | country: { 129 | name: 'French' 130 | } 131 | } 132 | ``` 133 | 134 | ### Alternatively, disable auto save usage: 135 | 136 | ###### Warning: Mode false works only with form tag see [this issue](https://github.com/angular/angular.js/issues/2513) 137 | 138 | ```html 139 |
140 | 141 |
142 | ``` 143 | 144 | Which expects a scope setup like the following: 145 | ```JavaScript 146 | $scope.username = "Jon Doe"; 147 | 148 | $scope.callback = function(controls, $event){ // controls = {'user': 'Jon Doe'}, $event={formSubmitEvent} 149 | $http.post('saveDataUrl', controls); 150 | }; 151 | ``` 152 | #### Optional attribute: 153 | 154 | It is optional if the property is set to false globally 155 | ```html 156 | auto-save-form-mode="boolean" 157 | ``` 158 | 159 | ### Trigger form submission, useful when the button is outside of the form 160 | 161 | ```javascript 162 | $scope.autoSaveFormSubmit($event); 163 | ``` 164 | 165 | ### Global configuration 166 | 167 | In config phase add autoSaveFormProvider 168 | 169 | ```js 170 | autoSaveFormProvider.setDebounce(500); //change globaly default debounce timer 171 | autoSaveFormProvider.setAutoSaveMode(true); //change globaly default auto save mode 172 | autoSaveFormProvider.setSpinner(true); //change globaly default spinner 173 | autoSaveFormProvider.setSpinnerPosition('top right'); //change globaly default position of the spinner 174 | ``` 175 | ### License 176 | The MIT License 177 | 178 | Copyright (c) 2017 Tiberiu Zuld 179 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-auto-save-form", 3 | "version": "1.5.1", 4 | "main": [ 5 | "dist/auto-save-form.js", 6 | "dist/auto-save-form.css" 7 | ], 8 | "dependencies": { 9 | "angular": ">=1.6.x", 10 | "lodash": ">=4.x" 11 | }, 12 | "devDependencies": { 13 | "angular-material": "1.1.4", 14 | "angular-mocks": "~1.x", 15 | "angular-mocke2e-maydelay": "~1.0.2" 16 | }, 17 | "homepage": "https://github.io/tiberiuzuld/angular-auto-save-form", 18 | "bugs": { 19 | "url": "https://github.com/tiberiuzuld/angular-auto-save-form/issues" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/tiberiuzuld/angular-auto-save-form.git" 24 | }, 25 | "description": "Angular auto save form changed inputs", 26 | "keywords": [ 27 | "angular", 28 | "angularjs", 29 | "auto save form", 30 | "debounce", 31 | "changed fields" 32 | ], 33 | "authors": [ 34 | "Tiberiu Zuld" 35 | ], 36 | "license": "MIT", 37 | "ignore": [ 38 | "dist/scripts", 39 | "dist/styles", 40 | "dist/index.html", 41 | "gulp", 42 | "src", 43 | ".bowerrc", 44 | ".editorconfig", 45 | ".eslintrc", 46 | ".gitignore", 47 | "gulpfile.js", 48 | "karma.conf.js", 49 | "protractor.conf.js" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /dist/auto-save-form.css: -------------------------------------------------------------------------------- 1 | [auto-save-form] { 2 | position: relative; 3 | } 4 | 5 | [auto-save-form] .spinner { 6 | font-size: 3px; 7 | position: absolute; 8 | } 9 | 10 | [auto-save-form] .spinner.top { 11 | top: 10px; 12 | } 13 | 14 | [auto-save-form] .spinner.right { 15 | right: 10px; 16 | } 17 | 18 | [auto-save-form] .spinner.left { 19 | left: 10px; 20 | } 21 | 22 | [auto-save-form] .spinner.bottom { 23 | bottom: 10px; 24 | } 25 | 26 | [auto-save-form] .spinner, 27 | [auto-save-form] .spinner.spin:after { 28 | border-radius: 50%; 29 | width: 5em; 30 | height: 5em; 31 | } 32 | 33 | [auto-save-form] .spinner.spin { 34 | border-top: 1.1em solid rgba(0, 0, 0, 0.2); 35 | border-right: 1.1em solid rgba(0, 0, 0, 0.2); 36 | border-bottom: 1.1em solid rgba(0, 0, 0, 0.2); 37 | border-left: 1.1em solid rgba(0, 0, 0, 0.7); 38 | -webkit-animation: spinnerAnimation 1.1s infinite linear; 39 | animation: spinnerAnimation 1.1s infinite linear; 40 | } 41 | 42 | @-webkit-keyframes spinnerAnimation { 43 | 0% { 44 | -webkit-transform: rotate(0deg); 45 | transform: rotate(0deg); 46 | } 47 | 100% { 48 | -webkit-transform: rotate(360deg); 49 | transform: rotate(360deg); 50 | } 51 | } 52 | 53 | @keyframes spinnerAnimation { 54 | 0% { 55 | -webkit-transform: rotate(0deg); 56 | transform: rotate(0deg); 57 | } 58 | 100% { 59 | -webkit-transform: rotate(360deg); 60 | transform: rotate(360deg); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /dist/auto-save-form.js: -------------------------------------------------------------------------------- 1 | /* 2 | Angular Auto Save Form 3 | (c) 2017 Tiberiu Zuld 4 | License: MIT 5 | */ 6 | 7 | (function () { 8 | 'use strict'; 9 | 10 | autoSaveForm.$inject = ["$parse", "autoSaveForm", "$log"]; 11 | angular.module('angular-auto-save-form', []) 12 | .provider('autoSaveForm', autoSaveFormProvider) 13 | .directive('autoSaveForm', autoSaveForm) 14 | .directive('autoSaveFormProperty', autoSaveFormProperty); 15 | 16 | /** @ngInject */ 17 | function autoSaveFormProvider() { 18 | var debounce = 500; 19 | var autoSaveMode = true; 20 | var spinner = true; 21 | var spinnerPosition = 'top right'; 22 | 23 | return { 24 | setDebounce: function (value) { 25 | if (angular.isNumber(value)) { 26 | debounce = value; 27 | } 28 | }, 29 | setAutoSaveMode: function (value) { 30 | if (angular.isDefined(value)) { 31 | autoSaveMode = value; 32 | } 33 | }, 34 | setSpinner: function (value) { 35 | if (angular.isDefined(value)) { 36 | spinner = value; 37 | } 38 | }, 39 | setSpinnerPosition: function (value) { 40 | if (angular.isDefined(value)) { 41 | spinnerPosition = value; 42 | } 43 | }, 44 | $get: function () { 45 | return { 46 | debounce: debounce, 47 | autoSaveMode: autoSaveMode, 48 | spinner: spinner, 49 | spinnerPosition: spinnerPosition 50 | }; 51 | } 52 | } 53 | } 54 | 55 | /** @ngInject */ 56 | function autoSaveForm($parse, autoSaveForm, $log) { 57 | var spinnerTemplate = '
'; 58 | 59 | function saveFormLink(scope, element, attributes) { 60 | var formModel = scope.$eval(attributes.name); 61 | var saveFormAuto = scope.$eval(attributes.autoSaveFormMode); 62 | var saveFormDebounce = scope.$eval(attributes.autoSaveFormDebounce); 63 | var saveFormSpinner = scope.$eval(attributes.autoSaveFormSpinner); 64 | var saveFormSpinnerPosition = scope.$eval(attributes.autoSaveFormSpinnerPosition); 65 | var saveFormSpinnerElement; 66 | scope.autoSaveFormSubmit = getChangedControls; 67 | if (angular.isUndefined(saveFormAuto)) { 68 | saveFormAuto = autoSaveForm.autoSaveMode; 69 | } 70 | 71 | if (angular.isUndefined(saveFormSpinner)) { 72 | saveFormSpinner = autoSaveForm.spinner; 73 | } 74 | 75 | if (saveFormSpinner) { 76 | if (angular.isUndefined(saveFormSpinnerPosition)) { 77 | saveFormSpinnerPosition = autoSaveForm.spinnerPosition; 78 | } 79 | element.append(spinnerTemplate); 80 | saveFormSpinnerElement = angular.element(element[0].lastChild); 81 | saveFormSpinnerElement.addClass(saveFormSpinnerPosition); 82 | } 83 | 84 | if (saveFormAuto) { 85 | if (angular.isUndefined(saveFormDebounce)) { 86 | saveFormDebounce = autoSaveForm.debounce; 87 | } 88 | var debounce = _.debounce(getChangedControls, saveFormDebounce); 89 | scope.$watch(function () { 90 | return formModel.$dirty && formModel.$valid; 91 | }, function (newValue) { 92 | if (newValue) { 93 | debounce(); 94 | formModel.$valid = false; 95 | } 96 | }); 97 | } else { 98 | element.on('submit', function (event) { 99 | event.preventDefault(); 100 | getChangedControls(event); 101 | }); 102 | } 103 | 104 | function getChangedControls(event) { 105 | if (formModel.$invalid || formModel.$pristine) { 106 | return; 107 | } 108 | var controls = {}; 109 | 110 | cycleForm(formModel); 111 | 112 | var invoker = $parse(attributes.autoSaveForm); 113 | var promise = invoker(scope, { 114 | controls: controls, 115 | $event: event 116 | }); 117 | if (promise && !saveFormAuto) { 118 | if (saveFormSpinner) { 119 | saveFormSpinnerElement.addClass('spin'); 120 | } 121 | promise 122 | .then(function () { 123 | formModel.$setPristine(); 124 | }, $log.error) 125 | .finally(function () { 126 | if (saveFormSpinner) { 127 | saveFormSpinnerElement.removeClass('spin'); 128 | } 129 | }); 130 | } else { 131 | formModel.$setPristine(); 132 | } 133 | 134 | function cycleForm(formModel) { 135 | angular.forEach(formModel.$$controls, checkForm); 136 | } 137 | 138 | function checkForm(value) { 139 | if (value.$dirty) { 140 | if (value.hasOwnProperty('$submitted')) { //check nestedForm 141 | cycleForm(value); 142 | } else { 143 | var keys = value.$name.split(/\./); 144 | if (scope.autoSaveFormProperties && scope.autoSaveFormProperties[keys[0]]) { 145 | keys = scope.autoSaveFormProperties[keys[0]].split(/\./); 146 | } 147 | constructControlsObject(keys, value.$modelValue, controls); 148 | } 149 | } 150 | } 151 | 152 | function constructControlsObject(keys, value, controls) { 153 | var key = keys.shift(); 154 | 155 | if (keys.length) { 156 | if (!controls.hasOwnProperty(key)) { 157 | controls[key] = {}; 158 | } 159 | constructControlsObject(keys, value, controls[key]); 160 | } else { 161 | controls[key] = value; 162 | } 163 | } 164 | } 165 | } 166 | 167 | return { 168 | restrict: 'A', 169 | link: saveFormLink 170 | }; 171 | } 172 | 173 | /** @ngInject */ 174 | function autoSaveFormProperty() { 175 | 176 | function saveFormLink(scope, element, attributes) { 177 | if (attributes.autoSaveFormProperty) { 178 | if (angular.isUndefined(scope.autoSaveFormProperties)) { 179 | scope.autoSaveFormProperties = {}; 180 | } 181 | var keys = attributes.autoSaveFormProperty.split(/\./); 182 | scope.autoSaveFormProperties[keys.splice(0, 1)] = keys.join('.'); 183 | } 184 | } 185 | 186 | return { 187 | restrict: 'A', 188 | link: saveFormLink 189 | }; 190 | } 191 | })(); 192 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Auto Save Form 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Male 39 | Female 40 | 41 | 42 | 43 | 44 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Male 61 | Female 62 | 63 | Save 65 | 66 |
67 |
68 |
69 | 70 | 71 | 72 | 73 |
Examples simulate $http calls
74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /dist/scripts/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('autoSaveFormApp', ['angular-auto-save-form', 'ngMockE2E', 'mayDelay', 'ngMaterial']); 5 | })(); 6 | 7 | (function () { 8 | 'use strict'; 9 | 10 | IndexMocks.$inject = ["$httpBackend"]; 11 | angular.module('autoSaveFormApp').run(IndexMocks); 12 | 13 | /** @ngInject */ 14 | function IndexMocks($httpBackend) { 15 | var delay = 500; 16 | var user = { 17 | name: 'Jon Doe', 18 | city: 'New York', 19 | country: 'United States of America', 20 | gender: 'male' 21 | }; 22 | 23 | var userNormal = { 24 | name: 'Doe Joe', 25 | city: 'Paris', 26 | country: 'France', 27 | gender: 'female' 28 | }; 29 | 30 | $httpBackend.whenPOST(/updateDataNormal/).respond(function (method, url, data) { 31 | return [200, data]; 32 | }, delay); 33 | 34 | $httpBackend.whenPOST(/updateData/).respond(function (method, url, data) { 35 | return [200, data]; 36 | }, delay); 37 | 38 | $httpBackend.whenGET(/getDataNormal/).respond(userNormal); 39 | 40 | $httpBackend.whenGET(/getData/).respond(user); 41 | } 42 | })(); 43 | 44 | (function () { 45 | 'use strict'; 46 | 47 | IndexController.$inject = ["$http", "$log"]; 48 | angular.module('autoSaveFormApp').controller('IndexController', IndexController); 49 | 50 | /** @ngInject */ 51 | function IndexController($http, $log) { 52 | var vm = this; 53 | 54 | vm.languages = ['English', 'German', 'French']; 55 | vm.saveInProgress = false; 56 | vm.normalSaveInProgress = false; 57 | 58 | $http.get('getData').then(function (response) { 59 | vm.user = response.data; 60 | }, $log.error); 61 | 62 | $http.get('getDataNormal').then(function (response) { 63 | vm.userNormal = response.data; 64 | }, $log.error); 65 | 66 | vm.updateForm = function (formControls) { 67 | vm.savedObject = angular.toJson(formControls, true); 68 | return $http.post('/updateData', formControls); 69 | }; 70 | 71 | vm.updateNormalForm = function (formControls) { 72 | vm.savedObject = angular.toJson(formControls, true); 73 | return $http.post('/updateDataNormal', formControls); 74 | } 75 | } 76 | })(); 77 | 78 | (function () { 79 | 'use strict'; 80 | 81 | config.$inject = ["$logProvider", "$compileProvider", "autoSaveFormProvider"]; 82 | angular.module('autoSaveFormApp').config(config); 83 | 84 | /** @ngInject */ 85 | function config($logProvider, $compileProvider, autoSaveFormProvider) { 86 | // Disable debug 87 | $logProvider.debugEnabled(false); 88 | $compileProvider.debugInfoEnabled(true); 89 | 90 | autoSaveFormProvider.setDebounce(500); 91 | autoSaveFormProvider.setAutoSaveMode(true); 92 | autoSaveFormProvider.setSpinner(true); 93 | autoSaveFormProvider.setSpinnerPosition('top right'); 94 | } 95 | 96 | })(); 97 | -------------------------------------------------------------------------------- /dist/styles/app.css: -------------------------------------------------------------------------------- 1 | md-whiteframe { 2 | min-width: 300px; 3 | height: 50%; 4 | word-wrap: break-word; 5 | overflow: hidden; 6 | } 7 | 8 | md-whiteframe span { 9 | background: #d5d5d5; 10 | } 11 | -------------------------------------------------------------------------------- /gulp/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var $ = require('gulp-load-plugins')({ 8 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 9 | }); 10 | 11 | gulp.task('partials', function () { 12 | return gulp.src([ 13 | path.join(conf.paths.src, '/app/**/*.html'), 14 | path.join(conf.paths.tmp, '/serve/app/**/*.html') 15 | ]) 16 | .pipe($.minifyHtml({ 17 | empty: true, 18 | spare: true, 19 | quotes: true 20 | })) 21 | .pipe($.angularTemplatecache('templateCacheHtml.js', { 22 | module: 'autoSaveFormApp', 23 | root: 'app' 24 | })) 25 | .pipe(gulp.dest(conf.paths.tmp + '/partials/')); 26 | }); 27 | 28 | gulp.task('html', ['inject', 'partials'], function () { 29 | var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), {read: false}); 30 | var partialsInjectOptions = { 31 | starttag: '', 32 | ignorePath: path.join(conf.paths.tmp, '/partials'), 33 | addRootSlash: false 34 | }; 35 | 36 | //var htmlFilter = $.filter(path.join(conf.paths.tmp, '/**/*.html'), {restore: true}); 37 | var jsFilter = $.filter(path.join(conf.paths.tmp, '/**/*.js'), {restore: true}); 38 | //var cssFilter = $.filter(path.join(conf.paths.tmp, '/**/*.css'), {restore: true}); 39 | 40 | return gulp.src(path.join(conf.paths.tmp, '/serve/*.html')) 41 | .pipe($.inject(partialsInjectFile, partialsInjectOptions)) 42 | .pipe($.useref()) 43 | .pipe(jsFilter) 44 | //.pipe($.sourcemaps.init()) 45 | .pipe($.ngAnnotate()) 46 | //.pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify')) 47 | //.pipe($.rev()) 48 | //.pipe($.sourcemaps.write('maps')) 49 | .pipe(jsFilter.restore) 50 | //.pipe(cssFilter) 51 | //.pipe($.sourcemaps.init()) 52 | //.pipe($.minifyCss({ processImport: false })) 53 | //.pipe($.rev()) 54 | //.pipe($.sourcemaps.write('maps')) 55 | //.pipe(cssFilter.restore) 56 | //.pipe($.revReplace()) 57 | //.pipe(htmlFilter) 58 | //.pipe($.minifyHtml({ 59 | // empty: true, 60 | // spare: true, 61 | // quotes: true, 62 | // conditionals: true 63 | //})) 64 | //.pipe(htmlFilter.restore) 65 | .pipe(gulp.dest(path.join(conf.paths.dist, '/'))) 66 | .pipe($.size({title: path.join(conf.paths.dist, '/'), showFiles: true})); 67 | }); 68 | 69 | // Only applies for fonts from bower dependencies 70 | // Custom fonts are handled by the "other" task 71 | gulp.task('fonts', function () { 72 | return gulp.src($.mainBowerFiles()) 73 | .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}')) 74 | .pipe($.flatten()) 75 | .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/'))); 76 | }); 77 | 78 | gulp.task('other', function () { 79 | var fileFilter = $.filter(function (file) { 80 | return file.stat.isFile(); 81 | }); 82 | 83 | return gulp.src([ 84 | path.join(conf.paths.src, '/**/*'), 85 | path.join('!' + conf.paths.src, '/**/*.{html,css,js}') 86 | ]) 87 | .pipe(fileFilter) 88 | .pipe(gulp.dest(path.join(conf.paths.dist, '/'))); 89 | }); 90 | 91 | gulp.task('clean', function () { 92 | return $.del([path.join(conf.paths.dist, '/'), path.join(conf.paths.tmp, '/')]); 93 | }); 94 | 95 | gulp.task('build', ['html', 'fonts', 'other']); 96 | -------------------------------------------------------------------------------- /gulp/conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the variables used in other gulp files 3 | * which defines tasks 4 | * By design, we only put there very generic config values 5 | * which are used in several places to keep good readability 6 | * of the tasks 7 | */ 8 | 9 | var gutil = require('gulp-util'); 10 | 11 | /** 12 | * The main paths of your project handle these with care 13 | */ 14 | exports.paths = { 15 | src: 'src', 16 | dist: 'dist', 17 | tmp: '.tmp', 18 | e2e: 'e2e' 19 | }; 20 | 21 | /** 22 | * Wiredep is the lib which inject bower dependencies in your project 23 | * Mainly used to inject script tags in the index.html but also used 24 | * to inject css preprocessor deps and js files in karma 25 | */ 26 | exports.wiredep = { 27 | exclude: [/jquery/], 28 | directory: 'bower_components' 29 | }; 30 | 31 | /** 32 | * Common implementation for an error handler of a Gulp plugin 33 | */ 34 | exports.errorHandler = function (title) { 35 | 'use strict'; 36 | 37 | return function (err) { 38 | gutil.log(gutil.colors.red('[' + title + ']'), err.toString()); 39 | this.emit('end'); 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /gulp/e2e-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | // Downloads the selenium webdriver 12 | gulp.task('webdriver-update', $.protractor.webdriver_update); 13 | 14 | gulp.task('webdriver-standalone', $.protractor.webdriver_standalone); 15 | 16 | function runProtractor(done) { 17 | var params = process.argv; 18 | var args = params.length > 3 ? [params[3], params[4]] : []; 19 | 20 | gulp.src(path.join(conf.paths.e2e, '/**/*.js')) 21 | .pipe($.protractor.protractor({ 22 | configFile: 'protractor.conf.js', 23 | args: args 24 | })) 25 | .on('error', function (err) { 26 | // Make sure failed tests cause gulp to exit non-zero 27 | throw err; 28 | }) 29 | .on('end', function () { 30 | // Close browser sync server 31 | browserSync.exit(); 32 | done(); 33 | }); 34 | } 35 | 36 | gulp.task('protractor', ['protractor:src']); 37 | gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor); 38 | gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor); 39 | -------------------------------------------------------------------------------- /gulp/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var $ = require('gulp-load-plugins')(); 8 | 9 | var wiredep = require('wiredep').stream; 10 | var _ = require('lodash'); 11 | 12 | var browserSync = require('browser-sync'); 13 | 14 | gulp.task('inject-reload', ['inject'], function () { 15 | browserSync.reload(); 16 | }); 17 | 18 | gulp.task('inject', ['scripts'], function () { 19 | var injectStylesDirective = gulp.src([ 20 | path.join(conf.paths.src, '/auto-save-form/**/*.css') 21 | ], {read: false}); 22 | 23 | var stylesInjectOptionsDirective = { 24 | starttag: '', 25 | ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], 26 | addRootSlash: false 27 | }; 28 | 29 | var injectScriptsDirective = gulp.src([ 30 | path.join(conf.paths.src, '/auto-save-form/**/*.module.js'), 31 | path.join(conf.paths.src, '/auto-save-form/**/*.js'), 32 | path.join('!' + conf.paths.src, '/auto-save-form/**/*.spec.js') 33 | ]) 34 | .pipe($.angularFilesort()).on('error', conf.errorHandler('AngularFilesort')); 35 | 36 | var scriptsInjectOptionsDirective = { 37 | starttag: '', 38 | ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], 39 | addRootSlash: false 40 | }; 41 | 42 | var injectStyles = gulp.src([ 43 | path.join(conf.paths.src, '/app/**/*.css') 44 | ], {read: false}); 45 | 46 | var injectScripts = gulp.src([ 47 | path.join(conf.paths.src, '/app/**/*.module.js'), 48 | path.join(conf.paths.src, '/app/**/*.js'), 49 | path.join('!' + conf.paths.src, '/app/**/*.spec.js') 50 | ]) 51 | .pipe($.angularFilesort()).on('error', conf.errorHandler('AngularFilesort')); 52 | 53 | var injectOptions = { 54 | ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], 55 | addRootSlash: false 56 | }; 57 | 58 | return gulp.src(path.join(conf.paths.src, '/*.html')) 59 | .pipe($.inject(injectStylesDirective, stylesInjectOptionsDirective)) 60 | .pipe($.inject(injectStyles, injectOptions)) 61 | .pipe($.inject(injectScriptsDirective, scriptsInjectOptionsDirective)) 62 | .pipe($.inject(injectScripts, injectOptions)) 63 | .pipe(wiredep(_.extend({}, conf.wiredep, {dependencies: true, devDependencies: true}))) 64 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve'))); 65 | }); 66 | -------------------------------------------------------------------------------- /gulp/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | gulp.task('scripts-reload', function () { 12 | return buildScripts() 13 | .pipe(browserSync.stream()); 14 | }); 15 | 16 | gulp.task('scripts', function () { 17 | return buildScripts(); 18 | }); 19 | 20 | function buildScripts() { 21 | return gulp.src(path.join(conf.paths.src, '/**/*.js')) 22 | .pipe($.eslint()) 23 | .pipe($.eslint.format()) 24 | .pipe($.size()) 25 | } 26 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | var browserSyncSpa = require('browser-sync-spa'); 9 | 10 | var util = require('util'); 11 | 12 | var proxyMiddleware = require('http-proxy-middleware'); 13 | 14 | function browserSyncInit(baseDir, browser) { 15 | browser = browser === undefined ? 'default' : browser; 16 | 17 | var routes = null; 18 | if (baseDir === conf.paths.src || (util.isArray(baseDir) && baseDir.indexOf(conf.paths.src) !== -1)) { 19 | routes = { 20 | '/bower_components': 'bower_components' 21 | }; 22 | } 23 | 24 | var server = { 25 | baseDir: baseDir, 26 | routes: routes 27 | }; 28 | 29 | /* 30 | * You can add a proxy to your backend by uncommenting the line below. 31 | * You just have to configure a context which will we redirected and the target url. 32 | * Example: $http.get('/users') requests will be automatically proxified. 33 | * 34 | * For more details and option, https://github.com/chimurai/http-proxy-middleware/blob/v0.0.5/README.md 35 | */ 36 | // server.middleware = proxyMiddleware('/users', {target: 'http://jsonplaceholder.typicode.com', proxyHost: 'jsonplaceholder.typicode.com'}); 37 | 38 | browserSync.instance = browserSync.init({ 39 | startPath: '/', 40 | server: server, 41 | browser: browser, 42 | ghostMode: false, 43 | notify: false 44 | }); 45 | } 46 | 47 | browserSync.use(browserSyncSpa({ 48 | selector: '[ng-app]'// Only needed for angular apps 49 | })); 50 | 51 | gulp.task('serve', ['watch'], function () { 52 | browserSyncInit([path.join(conf.paths.tmp, '/serve'), conf.paths.src]); 53 | }); 54 | 55 | gulp.task('serve:dist', ['build'], function () { 56 | browserSyncInit(conf.paths.dist); 57 | }); 58 | 59 | gulp.task('serve:e2e', ['inject'], function () { 60 | browserSyncInit([conf.paths.tmp + '/serve', conf.paths.src], []); 61 | }); 62 | 63 | gulp.task('serve:e2e-dist', ['build'], function () { 64 | browserSyncInit(conf.paths.dist, []); 65 | }); 66 | -------------------------------------------------------------------------------- /gulp/unit-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var karma = require('karma'); 8 | 9 | var pathSrcHtml = [ 10 | path.join(conf.paths.src, '/**/*.html') 11 | ]; 12 | 13 | var pathSrcJs = [ 14 | path.join(conf.paths.src, '/**/!(*.spec).js') 15 | ]; 16 | 17 | function runTests(singleRun, done) { 18 | var reporters = ['progress']; 19 | var preprocessors = {}; 20 | 21 | pathSrcHtml.forEach(function (path) { 22 | preprocessors[path] = ['ng-html2js']; 23 | }); 24 | 25 | if (singleRun) { 26 | pathSrcJs.forEach(function (path) { 27 | preprocessors[path] = ['coverage']; 28 | }); 29 | reporters.push('coverage') 30 | } 31 | 32 | var localConfig = { 33 | configFile: path.join(__dirname, '/../karma.conf.js'), 34 | singleRun: singleRun, 35 | autoWatch: !singleRun, 36 | reporters: reporters, 37 | preprocessors: preprocessors 38 | }; 39 | 40 | var server = new karma.Server(localConfig, function (failCount) { 41 | done(failCount ? new Error("Failed " + failCount + " tests.") : null); 42 | }); 43 | server.start(); 44 | } 45 | 46 | gulp.task('test', ['scripts'], function (done) { 47 | runTests(true, done); 48 | }); 49 | 50 | gulp.task('test:auto', ['watch'], function (done) { 51 | runTests(false, done); 52 | }); 53 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | function isOnlyChange(event) { 10 | return event.type === 'changed'; 11 | } 12 | 13 | gulp.task('watch', ['inject'], function () { 14 | 15 | gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject-reload']); 16 | 17 | gulp.watch(path.join(conf.paths.src, '/app/**/*.css', '/auto-save-form/**/*.css'), function (event) { 18 | if (isOnlyChange(event)) { 19 | browserSync.reload(event.path); 20 | } else { 21 | gulp.start('inject-reload'); 22 | } 23 | }); 24 | 25 | gulp.watch(path.join(conf.paths.src, '/app/**/*.js', '/auto-save-form/**/*.js'), function (event) { 26 | if (isOnlyChange(event)) { 27 | gulp.start('scripts-reload'); 28 | } else { 29 | gulp.start('inject-reload'); 30 | } 31 | }); 32 | 33 | gulp.watch(path.join(conf.paths.src, '/app/**/*.html'), function (event) { 34 | browserSync.reload(event.path); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your gulpfile! 3 | * The gulp tasks are splitted in several files in the gulp directory 4 | * because putting all here was really too long 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var gulp = require('gulp'); 10 | var wrench = require('wrench'); 11 | 12 | /** 13 | * This will load all js or coffee files in the gulp directory 14 | * in order to load all gulp tasks 15 | */ 16 | wrench.readdirSyncRecursive('./gulp').filter(function(file) { 17 | return (/\.(js|coffee)$/i).test(file); 18 | }).map(function(file) { 19 | require('./gulp/' + file); 20 | }); 21 | 22 | 23 | /** 24 | * Default task clean temporaries directories and launch the 25 | * main optimization build task 26 | */ 27 | gulp.task('default', ['clean'], function () { 28 | gulp.start('build'); 29 | }); 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/auto-save-form.js'); 2 | module.exports = 'angular-auto-save-form'; 3 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var conf = require('./gulp/conf'); 5 | 6 | var _ = require('lodash'); 7 | var wiredep = require('wiredep'); 8 | 9 | var pathSrcHtml = [ 10 | path.join(conf.paths.src, '/**/*.html') 11 | ]; 12 | 13 | function listFiles() { 14 | var wiredepOptions = _.extend({}, conf.wiredep, { 15 | dependencies: true, 16 | devDependencies: true 17 | }); 18 | 19 | return wiredep(wiredepOptions).js 20 | .concat([ 21 | path.join(conf.paths.src, '/app/**/*.module.js'), 22 | path.join(conf.paths.src, '/app/**/*.js'), 23 | path.join(conf.paths.src, '/**/*.spec.js'), 24 | path.join(conf.paths.src, '/**/*.mock.js'), 25 | ]) 26 | .concat(pathSrcHtml); 27 | } 28 | 29 | module.exports = function(config) { 30 | 31 | var configuration = { 32 | files: listFiles(), 33 | 34 | singleRun: true, 35 | 36 | autoWatch: false, 37 | 38 | ngHtml2JsPreprocessor: { 39 | stripPrefix: conf.paths.src + '/', 40 | moduleName: 'saveForm' 41 | }, 42 | 43 | logLevel: 'WARN', 44 | 45 | frameworks: ['jasmine', 'angular-filesort'], 46 | 47 | angularFilesort: { 48 | whitelist: [path.join(conf.paths.src, '/**/!(*.html|*.spec|*.mock).js')] 49 | }, 50 | 51 | browsers : ['PhantomJS'], 52 | 53 | plugins : [ 54 | 'karma-phantomjs-launcher', 55 | 'karma-angular-filesort', 56 | 'karma-coverage', 57 | 'karma-jasmine', 58 | 'karma-ng-html2js-preprocessor' 59 | ], 60 | 61 | coverageReporter: { 62 | type : 'html', 63 | dir : 'coverage/' 64 | }, 65 | 66 | reporters: ['progress'] 67 | }; 68 | 69 | // This is the default preprocessors configuration for a usage with Karma cli 70 | // The coverage preprocessor is added in gulp/unit-test.js only for single tests 71 | // It was not possible to do it there because karma doesn't let us now if we are 72 | // running a single test or not 73 | configuration.preprocessors = {}; 74 | pathSrcHtml.forEach(function(path) { 75 | configuration.preprocessors[path] = ['ng-html2js']; 76 | }); 77 | 78 | // This block is needed to execute Chrome on Travis 79 | // If you ever plan to use Chrome and Travis, you can keep it 80 | // If not, you can safely remove it 81 | // https://github.com/karma-runner/karma/issues/1144#issuecomment-53633076 82 | if(configuration.browsers[0] === 'Chrome' && process.env.TRAVIS) { 83 | configuration.customLaunchers = { 84 | 'chrome-travis-ci': { 85 | base: 'Chrome', 86 | flags: ['--no-sandbox'] 87 | } 88 | }; 89 | configuration.browsers = ['chrome-travis-ci']; 90 | } 91 | 92 | config.set(configuration); 93 | }; 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-auto-save-form", 3 | "version": "1.5.1", 4 | "main": "index.js", 5 | "dependencies": { 6 | "angular": ">=1.6.x", 7 | "lodash": ">=4.x" 8 | }, 9 | "devDependencies": { 10 | "browser-sync": "2.18.12", 11 | "browser-sync-spa": "1.0.3", 12 | "chalk": "1.1.3", 13 | "del": "2.2.2", 14 | "eslint-plugin-angular": "2.4.2", 15 | "estraverse": "4.2.0", 16 | "gulp": "3.9.1", 17 | "gulp-angular-filesort": "1.1.1", 18 | "gulp-angular-templatecache": "2.0.0", 19 | "gulp-autoprefixer": "4.0.0", 20 | "gulp-eslint": "3.0.1", 21 | "gulp-filter": "5.0.0", 22 | "gulp-flatten": "0.3.1", 23 | "gulp-inject": "4.2.0", 24 | "gulp-load-plugins": "1.5.0", 25 | "gulp-minify-css": "1.2.4", 26 | "gulp-minify-html": "1.0.6", 27 | "gulp-ng-annotate": "2.0.0", 28 | "gulp-protractor": "4.1.0", 29 | "gulp-rename": "1.2.2", 30 | "gulp-replace": "0.5.4", 31 | "gulp-rev": "7.1.2", 32 | "gulp-rev-replace": "0.4.3", 33 | "gulp-size": "2.1.0", 34 | "gulp-sourcemaps": "2.6.0", 35 | "gulp-uglify": "3.0.0", 36 | "gulp-useref": "3.1.2", 37 | "gulp-util": "3.0.8", 38 | "http-proxy-middleware": "0.17.4", 39 | "karma": "1.7.0", 40 | "karma-angular-filesort": "1.0.2", 41 | "karma-coverage": "1.1.1", 42 | "karma-jasmine": "1.1.0", 43 | "karma-ng-html2js-preprocessor": "1.0.0", 44 | "karma-phantomjs-launcher": "1.0.4", 45 | "lodash": "4.17.4", 46 | "main-bower-files": "2.13.1", 47 | "phantomjs-prebuilt": "2.1.14", 48 | "uglify-save-license": "0.4.1", 49 | "wiredep": "4.0.0", 50 | "wrench": "1.5.9" 51 | }, 52 | "scripts": { 53 | "test": "gulp test", 54 | "serve": "gulp serve", 55 | "build": "gulp" 56 | }, 57 | "engines": { 58 | "node": ">=4.2.1" 59 | }, 60 | "homepage": "https://github.io/tiberiuzuld/angular-auto-save-form", 61 | "bugs": { 62 | "url": "https://github.com/tiberiuzuld/angular-auto-save-form/issues" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "https://github.com/tiberiuzuld/angular-auto-save-form.git" 67 | }, 68 | "description": "Angular auto save form changed inputs", 69 | "keywords": [ 70 | "angular", 71 | "angularjs", 72 | "auto save form", 73 | "debounce", 74 | "changed fields" 75 | ], 76 | "authors": [ 77 | "Tiberiu Zuld" 78 | ], 79 | "license": "MIT", 80 | "files": [ 81 | "dist/auto-save-form.css", 82 | "dist/auto-save-form.js", 83 | "index.js" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var paths = require('./gulp/conf').paths; 4 | 5 | // An example configuration file. 6 | exports.config = { 7 | // The address of a running selenium server. 8 | //seleniumAddress: 'http://localhost:4444/wd/hub', 9 | //seleniumServerJar: deprecated, this should be set on node_modules/protractor/config.json 10 | 11 | // Capabilities to be passed to the webdriver instance. 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | 16 | baseUrl: 'http://localhost:3000', 17 | 18 | // Spec patterns are relative to the current working directory when 19 | // protractor is called. 20 | specs: [paths.e2e + '/**/*.js'], 21 | 22 | // Options to be passed to Jasmine-node. 23 | jasmineNodeOpts: { 24 | showColors: true, 25 | defaultTimeoutInterval: 30000 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/index.config.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('autoSaveFormApp').config(config); 5 | 6 | /** @ngInject */ 7 | function config($logProvider, $compileProvider, autoSaveFormProvider) { 8 | // Disable debug 9 | $logProvider.debugEnabled(false); 10 | $compileProvider.debugInfoEnabled(true); 11 | 12 | autoSaveFormProvider.setDebounce(500); 13 | autoSaveFormProvider.setAutoSaveMode(true); 14 | autoSaveFormProvider.setSpinner(true); 15 | autoSaveFormProvider.setSpinnerPosition('top right'); 16 | } 17 | 18 | })(); 19 | -------------------------------------------------------------------------------- /src/app/index.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('autoSaveFormApp').controller('IndexController', IndexController); 5 | 6 | /** @ngInject */ 7 | function IndexController($http, $log) { 8 | var vm = this; 9 | 10 | vm.languages = ['English', 'German', 'French']; 11 | vm.saveInProgress = false; 12 | vm.normalSaveInProgress = false; 13 | 14 | $http.get('getData').then(function (response) { 15 | vm.user = response.data; 16 | }, $log.error); 17 | 18 | $http.get('getDataNormal').then(function (response) { 19 | vm.userNormal = response.data; 20 | }, $log.error); 21 | 22 | vm.updateForm = function (formControls) { 23 | vm.savedObject = angular.toJson(formControls, true); 24 | return $http.post('/updateData', formControls); 25 | }; 26 | 27 | vm.updateNormalForm = function (formControls) { 28 | vm.savedObject = angular.toJson(formControls, true); 29 | return $http.post('/updateDataNormal', formControls); 30 | } 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /src/app/index.css: -------------------------------------------------------------------------------- 1 | md-whiteframe { 2 | min-width: 300px; 3 | height: 50%; 4 | word-wrap: break-word; 5 | overflow: hidden; 6 | } 7 | 8 | md-whiteframe span { 9 | background: #d5d5d5; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/index.mock.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('autoSaveFormApp').run(IndexMocks); 5 | 6 | /** @ngInject */ 7 | function IndexMocks($httpBackend) { 8 | var delay = 500; 9 | var user = { 10 | name: 'Jon Doe', 11 | city: 'New York', 12 | country: 'United States of America', 13 | gender: 'male' 14 | }; 15 | 16 | var userNormal = { 17 | name: 'Doe Joe', 18 | city: 'Paris', 19 | country: 'France', 20 | gender: 'female' 21 | }; 22 | 23 | $httpBackend.whenPOST(/updateDataNormal/).respond(function (method, url, data) { 24 | return [200, data]; 25 | }, delay); 26 | 27 | $httpBackend.whenPOST(/updateData/).respond(function (method, url, data) { 28 | return [200, data]; 29 | }, delay); 30 | 31 | $httpBackend.whenGET(/getDataNormal/).respond(userNormal); 32 | 33 | $httpBackend.whenGET(/getData/).respond(user); 34 | } 35 | })(); 36 | -------------------------------------------------------------------------------- /src/app/index.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('autoSaveFormApp', ['angular-auto-save-form', 'ngMockE2E', 'mayDelay', 'ngMaterial']); 5 | })(); 6 | -------------------------------------------------------------------------------- /src/auto-save-form/auto-save-form.css: -------------------------------------------------------------------------------- 1 | [auto-save-form] { 2 | position: relative; 3 | } 4 | 5 | [auto-save-form] .spinner { 6 | font-size: 3px; 7 | position: absolute; 8 | } 9 | 10 | [auto-save-form] .spinner.top { 11 | top: 10px; 12 | } 13 | 14 | [auto-save-form] .spinner.right { 15 | right: 10px; 16 | } 17 | 18 | [auto-save-form] .spinner.left { 19 | left: 10px; 20 | } 21 | 22 | [auto-save-form] .spinner.bottom { 23 | bottom: 10px; 24 | } 25 | 26 | [auto-save-form] .spinner, 27 | [auto-save-form] .spinner.spin:after { 28 | border-radius: 50%; 29 | width: 5em; 30 | height: 5em; 31 | } 32 | 33 | [auto-save-form] .spinner.spin { 34 | border-top: 1.1em solid rgba(0, 0, 0, 0.2); 35 | border-right: 1.1em solid rgba(0, 0, 0, 0.2); 36 | border-bottom: 1.1em solid rgba(0, 0, 0, 0.2); 37 | border-left: 1.1em solid rgba(0, 0, 0, 0.7); 38 | -webkit-animation: spinnerAnimation 1.1s infinite linear; 39 | animation: spinnerAnimation 1.1s infinite linear; 40 | } 41 | 42 | @-webkit-keyframes spinnerAnimation { 43 | 0% { 44 | -webkit-transform: rotate(0deg); 45 | transform: rotate(0deg); 46 | } 47 | 100% { 48 | -webkit-transform: rotate(360deg); 49 | transform: rotate(360deg); 50 | } 51 | } 52 | 53 | @keyframes spinnerAnimation { 54 | 0% { 55 | -webkit-transform: rotate(0deg); 56 | transform: rotate(0deg); 57 | } 58 | 100% { 59 | -webkit-transform: rotate(360deg); 60 | transform: rotate(360deg); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/auto-save-form/auto-save-form.js: -------------------------------------------------------------------------------- 1 | /* 2 | Angular Auto Save Form 3 | (c) 2017 Tiberiu Zuld 4 | License: MIT 5 | */ 6 | 7 | (function () { 8 | 'use strict'; 9 | 10 | angular.module('angular-auto-save-form', []) 11 | .provider('autoSaveForm', autoSaveFormProvider) 12 | .directive('autoSaveForm', autoSaveForm) 13 | .directive('autoSaveFormProperty', autoSaveFormProperty); 14 | 15 | /** @ngInject */ 16 | function autoSaveFormProvider() { 17 | var debounce = 500; 18 | var autoSaveMode = true; 19 | var spinner = true; 20 | var spinnerPosition = 'top right'; 21 | 22 | return { 23 | setDebounce: function (value) { 24 | if (angular.isNumber(value)) { 25 | debounce = value; 26 | } 27 | }, 28 | setAutoSaveMode: function (value) { 29 | if (angular.isDefined(value)) { 30 | autoSaveMode = value; 31 | } 32 | }, 33 | setSpinner: function (value) { 34 | if (angular.isDefined(value)) { 35 | spinner = value; 36 | } 37 | }, 38 | setSpinnerPosition: function (value) { 39 | if (angular.isDefined(value)) { 40 | spinnerPosition = value; 41 | } 42 | }, 43 | $get: function () { 44 | return { 45 | debounce: debounce, 46 | autoSaveMode: autoSaveMode, 47 | spinner: spinner, 48 | spinnerPosition: spinnerPosition 49 | }; 50 | } 51 | } 52 | } 53 | 54 | /** @ngInject */ 55 | function autoSaveForm($parse, autoSaveForm, $log) { 56 | var spinnerTemplate = '
'; 57 | 58 | function saveFormLink(scope, element, attributes) { 59 | var formModel = scope.$eval(attributes.name); 60 | var saveFormAuto = scope.$eval(attributes.autoSaveFormMode); 61 | var saveFormDebounce = scope.$eval(attributes.autoSaveFormDebounce); 62 | var saveFormSpinner = scope.$eval(attributes.autoSaveFormSpinner); 63 | var saveFormSpinnerPosition = scope.$eval(attributes.autoSaveFormSpinnerPosition); 64 | var saveFormSpinnerElement; 65 | scope.autoSaveFormSubmit = getChangedControls; 66 | if (angular.isUndefined(saveFormAuto)) { 67 | saveFormAuto = autoSaveForm.autoSaveMode; 68 | } 69 | 70 | if (angular.isUndefined(saveFormSpinner)) { 71 | saveFormSpinner = autoSaveForm.spinner; 72 | } 73 | 74 | if (saveFormSpinner) { 75 | if (angular.isUndefined(saveFormSpinnerPosition)) { 76 | saveFormSpinnerPosition = autoSaveForm.spinnerPosition; 77 | } 78 | element.append(spinnerTemplate); 79 | saveFormSpinnerElement = angular.element(element[0].lastChild); 80 | saveFormSpinnerElement.addClass(saveFormSpinnerPosition); 81 | } 82 | 83 | if (saveFormAuto) { 84 | if (angular.isUndefined(saveFormDebounce)) { 85 | saveFormDebounce = autoSaveForm.debounce; 86 | } 87 | var debounce = _.debounce(getChangedControls, saveFormDebounce); 88 | scope.$watch(function () { 89 | return formModel.$dirty && formModel.$valid; 90 | }, function (newValue) { 91 | if (newValue) { 92 | debounce(); 93 | formModel.$valid = false; 94 | } 95 | }); 96 | } else { 97 | element.on('submit', function (event) { 98 | event.preventDefault(); 99 | getChangedControls(event); 100 | }); 101 | } 102 | 103 | function getChangedControls(event) { 104 | if (formModel.$invalid || formModel.$pristine) { 105 | return; 106 | } 107 | var controls = {}; 108 | 109 | cycleForm(formModel); 110 | 111 | var invoker = $parse(attributes.autoSaveForm); 112 | var promise = invoker(scope, { 113 | controls: controls, 114 | $event: event 115 | }); 116 | if (promise && !saveFormAuto) { 117 | if (saveFormSpinner) { 118 | saveFormSpinnerElement.addClass('spin'); 119 | } 120 | promise 121 | .then(function () { 122 | formModel.$setPristine(); 123 | }, $log.error) 124 | .finally(function () { 125 | if (saveFormSpinner) { 126 | saveFormSpinnerElement.removeClass('spin'); 127 | } 128 | }); 129 | } else { 130 | formModel.$setPristine(); 131 | } 132 | 133 | function cycleForm(formModel) { 134 | angular.forEach(formModel.$$controls, checkForm); 135 | } 136 | 137 | function checkForm(value) { 138 | if (value.$dirty) { 139 | if (value.hasOwnProperty('$submitted')) { //check nestedForm 140 | cycleForm(value); 141 | } else { 142 | var keys = value.$name.split(/\./); 143 | if (scope.autoSaveFormProperties && scope.autoSaveFormProperties[keys[0]]) { 144 | keys = scope.autoSaveFormProperties[keys[0]].split(/\./); 145 | } 146 | constructControlsObject(keys, value.$modelValue, controls); 147 | } 148 | } 149 | } 150 | 151 | function constructControlsObject(keys, value, controls) { 152 | var key = keys.shift(); 153 | 154 | if (keys.length) { 155 | if (!controls.hasOwnProperty(key)) { 156 | controls[key] = {}; 157 | } 158 | constructControlsObject(keys, value, controls[key]); 159 | } else { 160 | controls[key] = value; 161 | } 162 | } 163 | } 164 | } 165 | 166 | return { 167 | restrict: 'A', 168 | link: saveFormLink 169 | }; 170 | } 171 | 172 | /** @ngInject */ 173 | function autoSaveFormProperty() { 174 | 175 | function saveFormLink(scope, element, attributes) { 176 | if (attributes.autoSaveFormProperty) { 177 | if (angular.isUndefined(scope.autoSaveFormProperties)) { 178 | scope.autoSaveFormProperties = {}; 179 | } 180 | var keys = attributes.autoSaveFormProperty.split(/\./); 181 | scope.autoSaveFormProperties[keys.splice(0, 1)] = keys.join('.'); 182 | } 183 | } 184 | 185 | return { 186 | restrict: 'A', 187 | link: saveFormLink 188 | }; 189 | } 190 | })(); 191 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Auto Save Form 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Male 51 | Female 52 | 53 | 54 | 55 | 56 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Male 73 | Female 74 | 75 | Save 77 | 78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 |
Examples simulate $http calls
86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | --------------------------------------------------------------------------------