English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

AngularJS Book - 1

Chapter 3. The Model in Detail

What is a model? Some frameworks make you use a JavaScript class or extend some other base class. Part of the beauty (I say that hesitantly) of JavaScript is that you do not need classes (traditional-OOP-style-classes) if you do not need classes. To JavaScript, what you really have are objects. If you want to write a class with prototype methods and all that, by all means do it! But you don't have to. A model can be a simple JS object: {}. A model, in fact, can be any variable, any object, any data structure; a model is your data. Other frameworks may have a Model class of some sort you need to extend; not so with AngularJS.

AngularJS Models

Models become useful when they are attached to a Scope object. A Scope object is a special object that is created by AngularJS, and (in addition to other things) provides data to your view. Putting your model inside of a Scope object ensures that AngularJS is observing the model and will take any actions necessary if that model changes. You can write a controller, and in that controller you can have var foo = 'bar';, but AngularJS will absolutely not care if you change the value to 'baz'. Once you do this, however: $scope.foo = 'bar';, you now have something AngularJS can work with. If you have <div>{{foo}}</div> in the view, <div> will be updated once $scope.foo has a value of baz. This is, of course, Angular's data-binding.

But what if you want to execute some custom code whenever $scope.foo changes? Yes, you can do that by using a $watch. Observe the observation in this $watch example with test:

// note no module here; not necessary for the example
function SomeCtrl($scope, $log) {
$scope.foo = 'bar';
$scope.$watch('foo', function(new_val, old_val) {
if (new_val !== old_val) {
$log.log("foo used to be '" + old_val + "', but is now '" + new_val + "'");
}
});
}
describe('SomeCtrl', function () {
var scope;
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new(); // make brand new child scope off of root
}));
describe('the watch on foo', function () {
it('should fire when foo changes', function () {
inject(function ($controller, $log) {
spyOn($log, 'log');
scope.$apply(function () {
$controller(SomeCtrl, {
$scope: scope,
$log: $log
});
});
expect(scope.foo).toBe('bar');
scope.$apply("foo = 'baz'");
expect(scope.foo).toBe('baz');
expect($log.log).toHaveBeenCalled();
});
});
});
});

Now, any time foo changes, the JavaScript console will let you know. Note the comparison of new_val and old_val the $watch will fire automatically when foo is set for the first time, which generally isn't the desired behavior (but might be). So we use the conditional.

The first parameter to $watch is actually an AngularJS expression. We're asking our Scope object to observe the value of some expression and execute a callback function when it changes. When we attached foo to the $scope, we created the opportunity to use the foo expression to be evaluated against that Scope. Since this can be any expression, we can even $watch some arbitrary boolean; foo == 'baz'; when that conditional is evaluated and the evaluated value changes, we will execute our callback function. This is useful if you want some code to fire only when a model reaches a certain numeric value, for example.

Now, you can $watch entire objects or arrays if you like. Take a look at the following nice model:


$scope.foo = {
bar: 'baz'
};

$watch-ing foo itself will not trigger our callback function if the value of foo.bar changes. Why? Because, by default, $watch works by reference, not by value. If you are watching foo and the reference to foo doesn't change--and if we update foo.bar, it doesn't--the $watch will not fire.

Luckily AngularJS provides functionality to accomplish this sort of task. It is expensive, but you can $watch by value. Simply pass true as the third parameter to a $watch call:

describe('watches', function () {
var scope;
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
scope.$apply('foo = {bar: "baz"}');
}));

it('when watching by reference, should not fire when object member updated',
inject(function ($log) {
spyOn($log, 'log');
scope.$apply(function () {
scope.$watch('foo', function (new_val, old_val) {
if (new_val !== old_val) {
$log.log("foo used to be '" + angular.toJson(old_val)
+ "', but is now '" + angular.toJson(new_val) + "'");
}
});
});
scope.$apply('foo.bar = "spam"');
expect($log.log).not.toHaveBeenCalled();
}));
it('when watching by value, should fire when object member updated', inject(function ($log) {
spyOn($log, 'log');
scope.$apply(function () {
scope.$watch('foo', function (new_val, old_val) {
if (new_val !== old_val) {
$log.log("foo used to be '" + angular.toJson(old_val) + "',
but is now '" + angular.toJson(new_val) + "'");
}
}, true);
});
scope.$apply('foo.bar = "spam"');
expect($log.log).toHaveBeenCalled();
}));
});

Note, the calls to angular.toJson() here; to output the contents of foo properly we must convert it to a string, or we will get the good ol' [Object object].

As this is an expensive operation (determining how exactly expensive is up to you and http://jsperf.com), it may be a good idea not to $watch huge objects or arrays. AngularJS was not designed to handle this gracefully. This kind of thing is generally best left up to MS Excel. Find out what you really need to be displaying and what you really need to edit in-line, and you will see increased performance.

Don't want your model to update immediately after a change?

An example of this tangent is a modal dialog in which some model is displayed, and you want the user to make changes to the model. However, you only want those changes applied when the user clicks "Save" and you want to throw away those changes if the user clicks "Cancel." There are several solutions to this, but one is to simply copy your model using angular.copy(), and work with a temp model, then copy back over it upon save. You could write a service or factory to take care of this in the general case. Or, you could even write a generic directive to mark a particular portion of the UI as "transactional" which will not "commit" until "Save." 

Take a look at this "transactional" directive w/ test: 

angular.module('app', []).directive('transactional', function($parse) {
return {
restrict: 'E',
scope: true,
link: function(scope, elm, attrs) {
// get a "getter" fn from the $parse service
var model = $parse(attrs.model);
// use the "setter" fn to put a copy of what's specified in the "model" attribute
// into this directive's scope, which does NOT inherit from the parent.
model.assign(scope, angular.copy(model(scope)));
// upon save, reassign the parent's model to the current value of this directive's
// model.
scope.$save = function() {
model.assign(scope.$parent, model(scope));
};
}
};
});

// ...and some unit tests
beforeEach(module('app'));
describe('transactional directive', function () {
var scope;
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new(); // make brand new child scope off of root
}));
describe('the model', function () {
it('should propagate to the parent only when $saved', inject(function ($compile) {
var template, compiled, isolateScope;
scope.foo = 'bar';
template = '<transactional model="foo"></transactional>';
compiled = $compile(template)(scope);
isolateScope = compiled.scope();
isolateScope.$apply('foo = "baz"');
expect(scope.foo).toBe('bar');
isolateScope.$apply('$save()');
expect(scope.foo).toBe('baz');
}));
});
});

To use this, do something like this usage of the "transactional" directive:

<div ng-controller="MyCtrl">
<p>Value of foo: {{protected.foo}}</p>
<transactional model="protected">
<p>Temp value of foo: <input type="text" ng-model="protected.foo"/></p>
<button ng-click="protected.increment()">increment</button>
<button ng-click="$save()">save</button>
</transactional>
</div>

Where we have a controller:

myApp.controller('MyCtrl', function($scope) {
$scope.protected = {
foo: 1,
increment: function() {
this.foo++;
}
};
});

This directive does something I'm not especially hot on--it stuffs something new into the scope. However, given that this is an isolate scope, it's OK, because it will not pollute any parent scopes with the $save function. If you are really cross-eyed about it, you can modify this directive to supply the save function via another attribute.  Also, there are a couple more limitations to this strategy.  You need a "top-level" model for this to work; you cannot use a model of foo.bar.  Furthermore, any pseudoclass you may have used within your object(s) will not be persisted, as angular.copy() does not do this.

Conclusion

While not very interesting in and of itself, the model is nonetheless a core component of AngularJS.  Unlike other frameworks, you do not need to extend any special class to create a model.  With AngularJS, you will put your data into a special Scope object instead.

Both the model and the view are good places to start designing web applications.  If your application has a complex UI; however, you may want to begin with the view, which we'll discuss in the next chapter.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.