├── .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 | [](https://badge.fury.io/js/angular-auto-save-form)
4 | [](https://david-dm.org/tiberiuzuld/angular-auto-save-form)
5 | [](https://david-dm.org/tiberiuzuld/angular-auto-save-form?type=dev)
6 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------