Isolating The ngModel Two-Way Data Binding Life-Cycle In AngularJS
In AngularJS, I love the two-way data binding that the ngModel directive provides. But, I don't necessarily want the two-way data binding to directly affect all my data. I know that might sound contradictory. But, instead of having ngModel alter my core data, I like to isolate the two-way data binding life-cycle behind a form object that can evolve independently, alongside my core data.
Run this demo in my JavaScript Demos project on GitHub.
That's a complicated way of saying that I use a plain-old JavaScript object - "form" - to house my ngModel bindings. When I need to start using an ngModel binding, I move data into my form object. Then, when I need to process the results of my ngModel bindings, I move the data out of my form and back into my core data, if necessary.
This adds a bit of indirection. But, it's a pattern that I've come to really appreciate. It allows me to play with my form data without having to be concerned about my core data. I can reset the form or update the form based on changes with relative ease. All while leaving the core data completely isolated.
To see this in action, I've created a very simple view / edit demo. When I move into the edit mode, you can see that I copy data into a vm.form object. And, when it's time to process the form, I copy the data out of my vm.form object and back into my core data model.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Isolating The ngModel Life-Cycle In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController as vm">
<h1>
Isolating The ngModel Life-Cycle In AngularJS
</h1>
<p class="nav">
<a ng-click="vm.viewFriend()">View</a> —
<a ng-click="vm.editFriend()">Edit</a>
</p>
<div ng-switch="vm.isEditing">
<!-- BEGIN: Viewing Interface. -->
<div ng-switch-when="false">
<p>
<strong>Name:</strong> {{ vm.friend.name }}
</p>
<p>
<strong>Description:</strong> {{ vm.friend.description }}
</p>
</div>
<!-- END: Viewing Interface. -->
<!-- BEGIN: Editing Interface. -->
<form ng-switch-when="true" ng-submit="vm.processForm()">
<p>
<label>Name:</label>
<input type="text" ng-model="vm.form.name" />
</p>
<p>
<label>Description:</label>
<input type="text" ng-model="vm.form.description" />
</p>
<p>
<button type="submit">Save Friend</button>
<a ng-click="vm.viewFriend()">Cancel</a>
</p>
</form>
<!-- END: Editing Interface. -->
</div>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root for the application.
angular.module( "Demo" ).controller(
"AppController",
function( $scope ) {
var vm = this;
vm.isEditing = false;
// I am the friend that is being viewed. This object WILL NOT BE connected
// to the ng-model bindings. Editing will be managed through the vm.form
// ng-model bindings.
vm.friend = {
name: "Kim",
description: "One of the coolest people I know!"
};
// I hold the ngModel bindings so that form-interactions can be
// encapsulated and managed without model-changes leaking into the rest
// of the view-model.
vm.form = {};
// Expose public methods.
vm.editFriend = editFriend;
vm.processForm = processForm;
vm.viewFriend = viewFriend;
// ---
// PUBLIC METHODS.
// ---
// I show the edit form.
function editFriend() {
vm.isEditing = true;
// When we enter the editing mode, we want to move the current values
// from the Friend into the Form model so that the form / ng-model
// bindings can be edited independently of the friend object.
vm.form.name = vm.friend.name;
vm.form.description = vm.friend.description;
}
// I process the edit form, saving the changes.
function processForm() {
vm.isEditing = false;
// At this point, the ng-model bindings have changed the isolated
// form object. Now, when the form is being saved, we can move the
// isolated changes back into the Friend if they are valid.
if ( vm.form.name ) {
vm.friend.name = vm.form.name;
vm.friend.description = vm.form.description;
}
}
// I show the detail page.
function viewFriend() {
vm.isEditing = false;
}
}
);
</script>
</body>
</html>
As you can see, the .editFriend() method moves data into the form object. And then, the .processForm() method moves data out of the form and back into the core data, if necessary.
If you hook your ngModel bindings directly up to your core data, I find that it makes the state of the core data harder to reason about because it can change at any moment. Plus, it makes reverting changes or updating the core model in the background harder since you're directly mutating the core data. By using a intermediary "form" object, I can completely isolate the ngModel-based changes. And, I find this gives me excellent control over how data flows through my controller.
Want to use code from this post? Check out the license.
Reader Comments
As always nice article Ben !
If your object have numerous properties it could be boring to set all one by one:
I prefer to use the copy function from angular:
vm.form = angular.copy(vm.friend);
It's shorter ;-)
@Simon,
Ah, totally true. I'm a huge fan of ng.copy(). Excellent tip.
I do something very similar. I have a service I can create off any data, which generally is used the same way.
Keeping the data separate allows for some advanced functionality, too:
- I use the original data and the form-data object to test to see if there are any changes (so we can perform notifications and onunload warnings)
- I keep a 2nd copy of the data, in it's original state, which I can use to perform "merges" of server-side changes. If a change is pushed from the server, but the user hasn't changed the data (1st copy == 2nd copy, or 1st copy == original), then I update both copies to match, which they'll see reflected in the form. If they have changed it, we can (potentially) use this information to notify them that someone else has made a change.
@Phil,
I like the idea of keeping a second copy of the original to compare against server-sent changes. That's one thing that I haven't really dealt with yet.
I use a combination of what Simon and Phil have already commented on.
I use a "formData" object tied to my ng-model and make a copy of formData as something like formDataOriginal. I then update formDataOriginal to formData upon successful validation and server-side saving and use formDataOriginal when I want to cancel or if something failed.
As Simon mentioned, making a copy like this seems like less work.
Like Phil, I also compare the 2 variables before saving to reduce unnecessary work. The push change concept from Phil is something interesting to consider.
@Chris,
I like it. And with angular.copy() and angular.equals(), it certainly makes creating and comparing objects very simple to do.
Maybe you should switch to backbone.js if you don't like angular binding. 2-way data binding is a core feature of angular. The angular team was trying to help the web act more like a desktop MVC app where you don't need to worry about the view and controller having out-of-sync data. Desktop apps have had 2-way data binding forever. The idea is to persist data between the controller and view. I recommend a different approach to your problem.
1. Copy the model when you start editing to a backup.
2. Editing still should happen to your main data because that's the point of MVC.
3. The save button simply leaves editing mode.
4. The cancel button reverts the model from your backup.
I use this style in all of my applications. I force other developers I work with to even instantly persists data even from directives. The data should always be 2-way data bound.
Great read Ben. I do the same thing for forms.
In fact, I started to find the act of creating a form object and merging it back into your main data very similar to the git workflow. For example:
`branch` - create a copy of your main data for a form object
`merge`- merge changes in your form object back to your main data
`revert` - clear the form
`commit` - cement a change so it won't be reverted when you clear the form
`rebase` - As Phil DeJarnett, when upstream data changes and needs to be merged into the current form object
Anyway, I've formalized some of that thinking into a little library called https://github.com/JasonStoltz/branch-js.
EDIT: That links was broken: https://github.com/JasonStoltz/branch-js
@Matt,
AngularJS is a full-featured solution. I don't think trying to get around this one feature is worth moving to something like Backbone.js. And, I'm not actually preventing two-way data binding - I'm just isolating a bit more so that I don't get unexpected side-effects. For me, its easier to reason about the state of the application since I know that changes are being locally quarantined until the controller logic determines that it should be persisted further.
@Jason,
That's a really interesting idea! I'll have to dig into your code a bit - am curious how it's implemented.
Definitely simpler to use `angular.copy()` Do the editing on copy, then use `angular.extend()` or `angular.merge()` as save mechanism to update original. Would remove a lot of code used in this post
Wow, this is very interesting! Your thoughts have brought the topic to limelight. Thank you for sharing it. You might want to read this also: http://goo.gl/3jEmfd I look forward to more articles based on this topic. Thank you again.