├── .gitignore ├── README.md ├── bower.json ├── lazyModel.js └── test ├── angular-1.2.26.js ├── angular-1.3.0-rc.5.js ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | dev.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lazy-model 2 | ========== 3 | 4 | AngularJS directive that works like `ng-model` but accept changes only when form is submitted (otherwise changes are cancelled). 5 | 6 | -------------------- 7 | 8 | ##UPDATE!!! 9 | As of Angular 1.3 there is [ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions) directive that allows to achive the same behavior natively 10 | ````html 11 |
12 | 13 | 14 | 15 |
16 | ```` 17 | jsfiddle: http://jsfiddle.net/8btk5/104/ 18 | So you don't need lazy-model any more! 19 | ---------------------- 20 | 21 | ### Why this is needed? 22 | AngularJS 2-way binding is good feature: you change model - and all views are updated instantly. 23 | But when dealing with forms I often need more transactional way: input something and accept changes or decline it. Official way to do it requires additional code in controller: create copy of model, link it with form and write changes back to original model when form is submitted (see http://docs.angularjs.org/guide/forms). 24 | Being too lazy, I tried to put all that stuff into **lazy-model** directive. 25 | 26 | ### How to use it? 27 | 1. Create form with **submit** and **reset** buttons 28 | 2. In controls use `lazy-model` instead of `ng-model` 29 | 30 | ````html 31 |
32 | 33 | 34 | 35 |
36 | ```` 37 | 38 | Now you can change username, but it will be saved to model only when you press **save**. 39 | If you press **cancel** - your changes will be declined. 40 | Try out demo: http://jsfiddle.net/8btk5/6/ 41 | 42 | It can be useful for **popup forms** and **modal dialogs.** 43 | For example, popup form: 44 | ````html 45 |
46 | 47 | 48 | 49 |
50 | 51 | ```` 52 | Live demo: http://jsfiddle.net/8btk5/7/ 53 | 54 | ### How to validate? 55 | Basically there are two ways of validation: 56 | 57 | #### 1. On-change validation (instant) 58 | Use normal AngularJS validation [described in docs](http://docs.angularjs.org/guide/forms). 59 | For example, `ng-maxlength` validator: 60 | ````html 61 |
62 | ... 63 | 64 | ```` 65 | And check `form.$valid` in submit handler in controller: 66 | ````js 67 | $scope.submit = function() { 68 | if ($scope.frm.$valid) { 69 | $scope.formVisible = false; 70 | } 71 | }; 72 | ```` 73 | Live demo: http://jsfiddle.net/8btk5/8/ 74 | 75 | #### 2. On-submit validation 76 | Alternatively, you can perform all validations inside `submit` handler and accept or decline 77 | changes by setting validity via `$setValidity` method. Don't forget to define `name` attribute 78 | of form and controls. 79 | 80 | ````html 81 | 82 | ... 83 | 84 | ... 85 | 86 | ```` 87 | In controller you should define both `submit` and `cancel` handlers: 88 | ````js 89 | $scope.submit = function() { 90 | if ($scope.frm.username.$modelValue.length > 10) { 91 | $scope.frm.username.$setValidity('maxlength', false); 92 | } else { 93 | $scope.frm.username.$setValidity('maxlength', true); 94 | $scope.formVisible = false; 95 | } 96 | }; 97 | 98 | $scope.cancel = function() { 99 | $scope.frm.username.$setValidity('maxlength', true); 100 | $scope.formVisible = false; 101 | } 102 | 103 | ```` 104 | 105 | Live demo: http://jsfiddle.net/8btk5/10/ 106 | 107 | ### How to send data on server? 108 | Please note that in `ng-submit` hook original models are not updated yet. 109 | You may use it for validation but not for sending data on server. 110 | To send data there is special attribute of `` called `lazy-submit`. 111 | Inside this hook models are updated and you can freely manipulate your models. 112 | 113 | ````html 114 | 115 | ... 116 | 117 | ```` 118 | 119 | In controller: 120 | ````js 121 | $scope.save = function() { 122 | $scope.formVisible = false; 123 | sendToServer($scope.user); 124 | }; 125 | ```` 126 | 127 | Live demo: http://jsfiddle.net/8btk5/12/ 128 | 129 | ### How to include it in my project? 130 | 1. Install via [bower](http://bower.io): 131 | ```` 132 | bower install lazy-model 133 | ```` 134 | or dowload manually [lazyModel.js](http://vitalets.github.io/lazy-model/lazyModel.js). 135 | 136 | 2. Include **lazyModel.js** and set app dependency: 137 | 138 | ````js 139 | var app = angular.module("app", ["lazyModel"]); 140 | ```` 141 | 142 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lazy-model", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/vitalets/lazy-model", 5 | "authors": [ 6 | "vitalets " 7 | ], 8 | "description": "AngularJS directive that works like ng-model but accept changes only when form is submitted", 9 | "main": "lazyModel.js", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /lazyModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lazy-model directive 3 | * 4 | * AngularJS directive that works like `ng-model` but saves changes 5 | * only when form is submitted (otherwise changes are canceled) 6 | */ 7 | 8 | angular.module('lazyModel', []) 9 | 10 | // lazy-model 11 | .directive('lazyModel', ['$compile', '$timeout', 12 | function($compile, $timeout) { 13 | return { 14 | restrict: 'A', 15 | priority: 500, 16 | terminal: true, 17 | require: ['lazyModel', '^form', '?^lazySubmit'], 18 | scope: true, 19 | controller: ['$scope', '$element', '$attrs', '$parse', 20 | function($scope, $element, $attrs, $parse) { 21 | if ($attrs.lazyModel === '') { 22 | throw '`lazy-model` should have a value.'; 23 | } 24 | 25 | // getter and setter for original model 26 | var ngModelGet = $parse($attrs.lazyModel); 27 | var ngModelSet = ngModelGet.assign; 28 | 29 | // accept changes 30 | this.accept = function() { 31 | ngModelSet($scope.$parent, $scope.buffer); 32 | }; 33 | 34 | // reset changes 35 | this.reset = function() { 36 | $scope.buffer = ngModelGet($scope.$parent); 37 | }; 38 | 39 | // watch for original model change (and initialization also) 40 | $scope.$watch($attrs.lazyModel, angular.bind(this, function (newValue, oldValue) { 41 | this.reset(); 42 | })); 43 | }], 44 | compile: function compile(elem, attr) { 45 | // set ng-model to buffer in directive scope (nested) 46 | elem.attr('ng-model', 'buffer'); 47 | // remove lazy-model attribute to exclude recursion 48 | elem.removeAttr("lazy-model"); 49 | // store compiled fn 50 | var compiled = $compile(elem); 51 | return { 52 | pre: function(scope, elem) { 53 | // compile element with ng-model directive poining to `scope.buffer` 54 | compiled(scope); 55 | }, 56 | post: function postLink(scope, elem, attr, ctrls) { 57 | var lazyModelCtrl = ctrls[0]; 58 | var formCtrl = ctrls[1]; 59 | var lazySubmitCtrl = ctrls[2]; 60 | // parentCtrl may be formCtrl or lazySubmitCtrl 61 | var parentCtrl = lazySubmitCtrl || formCtrl; 62 | 63 | // for the first time attach hooks 64 | if (parentCtrl.$lazyControls === undefined) { 65 | parentCtrl.$lazyControls = []; 66 | 67 | // find form element 68 | var form = elem.parent(); 69 | while (form[0].tagName !== 'FORM') { 70 | form = form.parent(); 71 | } 72 | 73 | // bind submit 74 | form.bind('submit', function() { 75 | // this submit handler must be called LAST after all other `submit` handlers 76 | // to get final value of formCtrl.$valid. The only way - is to call it in 77 | // the next tick via $timeout 78 | $timeout(function() { 79 | if (formCtrl.$valid) { 80 | // form valid - accept new values 81 | for (var i = 0; i < parentCtrl.$lazyControls.length; i++) { 82 | parentCtrl.$lazyControls[i].accept(); 83 | } 84 | 85 | // call final hook `lazy-submit` 86 | if (lazySubmitCtrl) { 87 | lazySubmitCtrl.finalSubmit(); 88 | } 89 | } 90 | }); 91 | }); 92 | 93 | // bind reset 94 | form.bind('reset', function(e) { 95 | e.preventDefault(); 96 | $timeout(function() { 97 | // reset changes 98 | for (var i = 0; i < parentCtrl.$lazyControls.length; i++) { 99 | parentCtrl.$lazyControls[i].reset(); 100 | } 101 | }); 102 | }); 103 | 104 | } 105 | 106 | // add to collection 107 | parentCtrl.$lazyControls.push(lazyModelCtrl); 108 | 109 | // remove from collection on destroy 110 | scope.$on('$destroy', function() { 111 | for (var i = parentCtrl.$lazyControls.length; i--;) { 112 | if (parentCtrl.$lazyControls[i] === lazyModelCtrl) { 113 | parentCtrl.$lazyControls.splice(i, 1); 114 | } 115 | } 116 | }); 117 | 118 | } 119 | }; 120 | } 121 | }; 122 | } 123 | ]) 124 | 125 | // lazy-submit 126 | .directive('lazySubmit', function() { 127 | return { 128 | restrict: 'A', 129 | require: ['lazySubmit', 'form'], 130 | controller: ['$element', '$attrs', '$scope', '$parse', 131 | function($element, $attrs, $scope, $parse) { 132 | var finalHook = $attrs.lazySubmit ? $parse($attrs.lazySubmit) : angular.noop; 133 | this.finalSubmit = function() { 134 | finalHook($scope); 135 | }; 136 | } 137 | ] 138 | }; 139 | }); -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | lazy-model 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 |
 {{user || json}} 
16 |
17 | 18 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("app", ["lazyModel"]); 2 | 3 | app.controller('Ctrl', function($scope) { 4 | $scope.user = { 5 | name: 'vitalets' 6 | }; 7 | }); --------------------------------------------------------------------------------