In this Lesson, we will cover the following recipes:
$scope
inheritance<select>
and ngOptions
AngularJS provides faculties to manage data alteration throughout the application, largely based around the model modification architecture. AngularJS' powerful data binding affords you the ability to build robust tools on top of the architecture as well as channels of communication that can efficiently reach throughout the application.
AngularJS offers a powerful event infrastructure that affords you the ability to control the application in scenarios where data binding might not be suitable or pragmatic. Even with a rigorously organized application topology, there are lots of applications for events in AngularJS.
AngularJS events are identified by strings and carry with them a payload that can take the form of an object, a function, or a primitive. The event can either be delivered via a parent scope that invokes $scope.$broadcast()
, or a child scope (or the same scope) that invokes $scope.$emit()
.
The $scope.$on()
method can be used anywhere a scope object can be used, as shown here:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope, $log) { $scope.$on('myEvent', function(event, data) { $log.log(event.name + ' observed with payload ', data); }); });
The $scope.$broadcast()
method triggers the event in itself and all child scopes. The 1.2.7 release of AngularJS introduced an optimization for $scope.$broadcast()
, but since this action will still bubble down through the scope hierarchy to reach the listening child scopes, it is possible to introduce performance problems if this is overused. Broadcasting can be implemented as follows:
(app.js) angular.module('myApp', []) .directive('myListener', function($log) { return { restrict: 'E', // each directive should be given its own scope scope: true, link: function(scope, el, attrs) { // method to generate event scope.sendDown = function() { scope.$broadcast('myEvent', {origin: attrs.local}); }; // method to listen for event scope.$on('myEvent', function(event, data) { $log.log( event.name + ' observed in ' + attrs.local + ', originated from ' + data.origin ); }); } }; }); (index.html) <div ng-app="myApp"> <my-listener local="outer"> <button ng-click="sendDown()">Send Down</button> <my-listener local="middle"> <my-listener local="first inner"></my-listener> <my-listener local="second inner"></my-listener> </my-listener> </my-listener> </div>
In this setup, clicking on the Send Down button will log the following in the browser console:
myEvent observed in outer, originated from outer myEvent observed in middle, originated from outer myEvent observed in first inner, originated from outer myEvent observed in second inner, originated from outer
JSFiddle: http://jsfiddle.net/msfrisbie/dn0zjep9/
As you might expect, $scope.$emit()
does the opposite of $scope.$broadcast()
. It will trigger all listeners of the event that exist within that same scope, or any of the parent scopes along the prototype chain, all the way up to $rootScope
. This can be implemented as follows:
(app.js) angular.module('myApp', []) .directive('myListener', function($log) { return { restrict: 'E', // each directive should be given its own scope scope: true, link: function(scope, el, attrs) { // method to generate event scope.sendUp = function() { scope.$emit('myEvent', {origin: attrs.local}); }; // method to listen for event scope.$on('myEvent', function(event, data) { $log.log( event.name + ' observed in ' + attrs.local + ', originated from ' + data.origin ); }); } }; }); (index.html) <div ng-app="myApp"> <my-listener local="outer"> <my-listener local="middle"> <my-listener local="first inner"> <button ng-click="sendUp()"> Send First Up </button> </my-listener> <my-listener local="second inner"> <button ng-click="sendUp()"> Send Second Up </button> </my-listener> </my-listener> </my-listener> </div>
In this example, clicking on the Send First Up button will log the following to the browser console:
myEvent observed in first inner, originated from first inner myEvent observed in middle, originated from first inner myEvent observed in outer, originated from first inner
Clicking on the Send Second Up button will log the following to the browser console:
myEvent observed in second inner, originated from second inner myEvent observed in middle, originated from second inner myEvent observed in outer, originated from second inner
JSFiddle: http://jsfiddle.net/msfrisbie/a344o7vo/
Similar to $scope.$watch()
, once an event listener is created, it will last the lifetime of the scope object they are added in. The $scope.$on()
method returns the deregistration function, which must be captured upon declaration. Invoking this deregistration function will prevent the scope from evaluating the callback function for this event. This can be toggled with a setup/teardown pattern, as follows:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope, $log) { $scope.setup = function() { $scope.teardown = $scope.$on('myEvent',function(event, data) { $log.log(event.name + ' observed with payload ', data); }); }; });
Invoking $scope.setup()
will initialize the event binding, and invoking $scope.teardown()
will destroy that binding.
Scopes in AngularJS are bound to the same rules of prototypical inheritance as plain old JavaScript objects. When wielded properly, they can be used very effectively in your application, but there are some "gotchas" to be aware of that can be avoided by adhering to best practices.
Suppose that your application contained the following:
(app.js) angular.module('myApp', []) .controller('Ctrl', function() {}) (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl" ng-init="data=123"> <input ng-model="data" /> <div ng-controller="Ctrl"> <input ng-model="data" /> </div> <div ng-controller="Ctrl"> <input ng-model="data" /> </div> </div> </div>
In the current setup, the $scope
instances in the nested Ctrl
instances will prototypically inherit from the parent Ctrl $scope
. When the page is loaded, all three inputs will be filled with 123
, and when you change the value of the parent Ctrl <input>
, both inputs bound to the child $scope
instances will update in turn, as all three are bound to the same object. However, when you change the values of either input bound to a child $scope
object, the other inputs will not reflect that value, and the data binding from that input is broken until the application is reloaded.
To fix this, simply add an object that is nested to any primitive types on your scope. This can be accomplished in the following fashion:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl" ng-init="data.value=123"> <input ng-model="data.value" /> <div ng-controller="Ctrl"> <input ng-model="data.value" /> </div> <div ng-controller="Ctrl"> <input ng-model="data.value" /> </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/obe24zet/
Now, any of the three inputs can be altered, and the change will reflect in the other two. All three remain bound to the same $scope
object in the parent Ctrl $scope
object.
The rule of thumb is to always maintain one layer of object indirection for anything (especially primitive types) in your scope if you are relying on the $scope
inheritance in any way. This is colloquially referred to as "always using a dot."
When the value of a $scope
property is altered from an input, this performs an assignment on the $scope
property to which it is bound. As is the case with prototypical inheritance, assignment to an object property will follow the prototype chain all the way up to the original instance, but assignment to a primitive will create a new instance of the primitive in the local $scope
property. In the preceding example, before the .value
fix was added, the new local instance was detached from the ancestral value, which resulted in the dual $scope
property values.
The following two examples are considered to be bad practice (for hopefully obvious reasons), and it is much easier to just maintain at least one level of object indirection for any data that needs to be inherited down through the application's $scope
tree.
It's possible to reestablish this inheritance by removing the primitive property from the local $scope
object:
(app.js) angular.module('myApp', []) .controller('outerCtrl', function($scope) { $scope.data = 123; }) .controller('innerCtrl', function($scope) { $scope.reattach = function() { delete($scope.data); }; }); (index.html) <div ng-app="myApp"> <div ng-controller="outerCtrl"> <input ng-model="data" /> <div ng-controller="innerCtrl"> <input ng-model="data" /> </div> <div ng-controller="innerCtrl"> <input ng-model="data" /> <button ng-click="reattach()">Reattach</button> </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/r33nekbg/
It is also possible to directly access the parent $scope
object using $scope.$parent
and ignore the inheritance completely. This can be done as follows:
(app.js) angular.module('myApp', []) .controller('Ctrl', function() {}); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl" ng-init="data=123"> <input ng-model="data" /> <div ng-controller="Ctrl"> <input ng-model="$parent.data" /> </div> <div ng-controller="Ctrl"> <input ng-model="$parent.data" /> </div> </div> </div>
The preceding examples explicitly demonstrate nested scopes that prototypically inherit from the parent $scope
object. In a real application, this would likely be very easy to detect and debug. However, AngularJS comes bundled with a number of built-in directives that silently create their own scopes, and if prototypical scope inheritance is not heeded, this can cause problems. There are six built-in directives that create their own scope: ngController
, ngInclude
, ngView
, ngRepeat
, ngIf
, and ngSwitch
.
The following examples will interpolate the $scope $id
into the template to demonstrate the creation of a new scope.
The use of ngController
should be obvious, as your controller logic relies on attaching functions and data to the new child scope created by the ngController
directive.
Irrespective of the HTML content of whatever is being included, ng-include
will wrap it inside a new scope. As ng-include
is normally used to insert monolithic application components that do not depend on their surroundings, it is less likely that you would run into the $scope
inheritance problems using it.
The following is an incorrect solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.data = 123; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <input ng-model="data " /> <ng-include src="'innerTemplate.html'"></ng-include> </div> <script type="text/ng-template" id="innerTemplate.html"> <div> Scope id: {{ $id }} <input ng-model="data " /> </div> </script> </div>
The new scope inside the compiled ng-include
directive inherits from the controller $scope
, but binding to its primitive value sets up the same problem.
The following is the correct solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.data = { val: 123 }; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <input ng-model="data.val" /> <ng-include src="'innerTemplate.html'"></ng-include> </div> <script type="text/ng-template" id="innerTemplate.html"> <div> Scope id: {{ $id }} <input ng-model="data.val" /> </div> </script> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/c8nLk676/
With respect to prototypal inheritance, ng-view
operates identically to ng-include
. The inserted compiled template is provided its own new child $scope
, and correctly inheriting from the parent $scope
can be accomplished in the exact same fashion.
The ngRepeat
directive is the most problematic directive when it comes to incorrectly managing the $scope
inheritance. Each element that the repeater creates is given its own scope, and modifications to these child scopes (such as inline editing of data in a list) will not affect the original object if it is bound to primitives.
The following is an incorrect solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.names = [ 'Alshon Jeffrey', 'Brandon Marshall', 'Matt Forte', 'Martellus Bennett', 'Jay Cutler' ]; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <pre>{{ names | json }}</pre> <div ng-repeat="name in names"> Scope id: {{ $id }} <input ng-model="name" /> </div> </div> </div>
As described earlier, changing the value of the input fields only serves to modify the instance of the primitive in the child scope, not the original object. One way to fix this is to restructure the data object so that instead of iterating through primitive types, it iterates through objects wrapping the primitive types.
The following is the correct solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.players = [ { name: 'Alshon Jeffrey' }, { name: 'Brandon Marshall' }, { name: 'Matt Forte' }, { name: 'Martellus Bennett' }, { name: 'Jay Cutler' } ]; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <pre>{{ players | json }}</pre> <div ng-repeat="player in players"> Scope id: {{ $id }} <input ng-model="player.name" /> </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/zesj1gb6/
With this, the original array is being modified properly, and all is right with the world. However, sometimes restructuring an object is not a feasible solution for an application. In this case, changing an array of strings to an array of objects seems like an odd workaround. Ideally, you would prefer to be able to iterate through the string array without modifying it first. Using track by
as part of the ng-repeat
expression, this is possible.
The following is also a correct solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.players = [ 'Alshon Jeffrey', 'Brandon Marshall', 'Matt Forte', 'Martellus Bennett', 'Jay Cutler' ]; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <pre>{{ players | json }}</pre> <div ng-repeat="player in players track by $index"> Scope id: {{ $id }} <input ng-model="players[$index]" /> </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/ovas398h/
Now, even though the repeater is iterating through the players
array elements, as the child $scope
objects created for each element will still prototypically inherit the players array, it simply binds to the respective element in the array using the $index
repeater.
As primitive types are immutable in JavaScript, altering a primitive element in the array will replace it entirely. When this replacement occurs, as a vanilla utilization of ng-repeat
identifies array elements by their string value, ng-repeat
thinks a new element has been added, and the entire array will re-render—a functionality which is obviously undesirable for usability and performance reasons. The track by $index
clause in the ng-repeat
expression solves this problem by identifying array elements by their index rather than their string value, which prevents constant re-rendering.
As the ng-if
directive destroys the DOM content nested inside it every time its expression evaluates as false
, it will re-inherit the parent $scope
object every time the inner content is compiled. If anything inside the ng-if
element directive inherits incorrectly from the parent $scope
object, the child $scope
data will be wiped out every time recompilation occurs.
The following is an incorrect solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.data 123; $scope.show = false; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <input ng-model="data " /> <input type="checkbox" ng-model="show" /> <div ng-if="show"> Scope id: {{ $id }} <input ng-model="data " /> </div> </div> </div>
Every time the checkbox is toggled, the newly created child $scope
object will re-inherit from the parent $scope
object and wipe out the existing data. This is obviously undesirable in many scenarios. Instead, the simple utilization of one level of object indirection solves this problem.
The following is the correct solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.data = { val: 123 }; $scope.show = false; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <input ng-model="data.val" /> <input type="checkbox" ng-model="show" /> <div ng-if="show"> Scope id: {{ $id }} <input ng-model="data.val" /> </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/hq7r5frm/
The ngSwitch
directive acts much in the same way as if you were to combine several ngIf
statements together. If anything inside the active ng-switch $scope
inherits incorrectly from the parent $scope
object, the child $scope
data will be wiped out every time recompilation occurs when the watched switch value is altered.
The following is an incorrect solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.data = 123; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <input ng-model="data " /> <div ng-switch on="data "> <div ng-switch-when="123"> Scope id: {{ $id }} <input ng-model="data " /> </div> <div ng-switch-default> Scope id: {{ $id }} Default </div> </div> </div> </div>
In this example, when the outer <input>
tag is set to the matching value 123
,the inner <input>
tag nested in ng-switch
will inherit that value, as expected. However, when altering the inner input, it doesn't modify the inherited value as the prototypical inheritance chain is broken.
The following is the correct solution:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.data = { val: 123 }; }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> Scope id: {{ $id }} <input ng-model="data.val" /> <div ng-switch on="data.val"> <div ng-switch-when="123"> Scope id: {{ $id }} <input ng-model="data.val" /> </div> <div ng-switch-default> Scope id: {{ $id }} Default </div> </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/8kh41wdm/
AngularJS offers close integration with HTML form elements in the form of directives to afford you the ability to build animated and styled form pages, complete with validation, quickly and easily.
AngularJS forms exist inside the <form>
tag, which corresponds to a native AngularJS directive, as shown in the following code. The novalidate
attribute instructs the browser to ignore its native form validation:
<form novalidate> <!-- form inputs --> </form>
Your HTML input elements will reside inside the <form>
tags. Each instance of the <form>
tag creates a FormController
, which keeps track of all its controls and nested forms. The entire AngularJS form infrastructure is built on top of this.
Suppose you have a controller; a form in your application is as follows:
<div ng-controller="Ctrl"> <form novalidate name="myform"> <input name="myinput" ng-model="formdata.myinput" /> </form> </div>
With this, Ctrl $scope
is provided a constructor for the FormController
as $scope.myform
, which contains a lot of useful attributes and functions. The individual form entries for each input can be accessed as child FormController
objects on the parent FormController
object; for example, $scope.myform.myinput
is the FormController
object for the text input.
Inputs and forms are provided with their own controllers, and AngularJS tracks the state of both the individual inputs and the entire form using a pristine/dirty dichotomy. "Pristine" refers to the state in which inputs are set to their default values, and "dirty" refers to any modifying action taken on the model corresponding to the inputs. The "pristine" state of the entire form is a logical AND result of all the input pristine states or a NOR result of all the dirty states; by its inverted definition, the "dirty" state of the entire form represents an OR result of all the dirty states or a NAND result of all the pristine states.
JSFiddle: http://jsfiddle.net/msfrisbie/trjfzdwc/
These states can be used in several different ways.
Both the <form>
and <input>
elements have the CSS classes, ng-pristine
and ng-dirty
, automatically applied to them based on the state the form is in. These CSS classes can be used to style the inputs based on their state, as follows:
form.ng-pristine { } input.ng-pristine { } form.ng-dirty { } input.ng-dirty { }
All instances of the FormController
and the ngModelController
instances inside it have the $pristine
and $dirty
Boolean properties available. These can be used in the controller business logic or to control the user flow through the form.
The following example shows Enter a value until the input has been modified:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.$watch('myform.myinput.$pristine', function(newval) { $scope.isPristine = newval; }); }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <form novalidate name="myform"> <input name="myinput" ng-model="formdata.myinput" /> </form> <div ng-show="isPristine"> Enter a value </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/unxbyun2/
Alternately, as the form object is attached to the scope, it is possible to directly detect whether the input is pristine in the view:
(index.html)
<div ng-app="myApp">
<div ng-controller="Ctrl">
<form novalidate name="myform">
<input name="myinput" ng-model="formdata.myinput" />
<div ng-show="myform.myinput.$pristine">
Enter a value
</div>
</form>
</div>
</div>
JSFiddle: http://jsfiddle.net/msfrisbie/pr3L1e2b/
It's also possible to force a form or input into a pristine or dirty state using the $setDirty()
or $setPristine()
methods. This has no bearing on what exists inside the inputs at that point in time; it simply overrides the Booleans values, $pristine
and $dirty
, and sets the corresponding CSS class, ng-pristine
or ng-dirty
. Invoking these methods will propagate to any parent forms.
Similar to the pristine/dirty dichotomy, AngularJS forms also have a valid/invalid dichotomy. Input fields in a form can be assigned validation rules that must be satisfied for the form to be valid. AngularJS tracks the validity of both the individual inputs and the entire form using the valid/invalid dichotomy. "Valid" refers to the state in which the inputs satisfy all validation requirements assigned to it, and "invalid" refers to an input that fails one or more validation requirements. The "valid" state of the entire form is a logical AND result of all the input valid states or a NOR result of all the invalid states; by its inverted definition, the "invalid" state of the entire form represents an OR result of all the invalid states or a NAND result of all the valid states.
JSFiddle: http://jsfiddle.net/msfrisbie/ejpsrfgz/
Similar to pristine and dirty, both the <form>
and <input>
elements have the CSS classes, ng-valid
and ng-invalid
, automatically applied to them based on the state the form is in. These CSS classes can be used to style the inputs based on their state, as follows:
form.ng-valid { } input.ng-valid { } form.ng-invalid { } input.ng-invalid { }
All instances of FormController
and the ngModelController
instances inside it have the $valid
and $invalid
Boolean attributes available. These can be used in the controller business logic or to control the user flow through the form.
The following example shows Input field cannot be blank while the input field is empty:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.$watch('myform.myinput.$invalid', function(newval) { $scope.isInvalid = newval; }); }); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <form novalidate name="myform"> <input name="myinput" ng-model="formdata.myinput" required /> </form> <div ng-show="isInvalid"> Input field cannot be blank </div> </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/40bdaey4/
Alternately, as the form object is attached to the scope, it is possible to directly detect whether the input is valid in the view:
(app.js) angular.module('myApp', []) .controller('Ctrl', function() {}); (index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <form novalidate name="myform"> <input name="myinput" ng-model="formdata.myinput" required /> <div ng-show="myform.myinput.$invalid"> Input field cannot be blank </div> </form> </div> </div>
AngularJS comes bundled with the following basic validators:
email
max
maxlength
min
minlength
number
pattern
required
url
While they are useful and largely self-explanatory, you'll likely want to build a custom validator. To do this, you'll need to construct a directive that will watch the model value of that input field, perform some analysis of it, and manually set the validity of that field using the $setValidity()
method.
The following example creates a custom validator that checks whether an input field is a prime number:
(app.js) angular.module('myApp', []) .directive('ensurePrime', function() { return { require: 'ngModel', link: function(scope, element, attrs, ctrl) { function isPrime(n) { if (n<2) { return false; } var m = Math.sqrt(n); for (var i=2; i<=m; i++) { if (n%i === 0) { return false; } } return true; } scope.$watch(attrs.ngModel, function(newval) { if (isPrime(newval)) { ctrl.$setValidity('prime', true); } else { ctrl.$setValidity('prime', false); } }); } }; }); (index.html) <div ng-app="myApp"> <form novalidate name="myform"> <input type="number" ensure-prime name="myinput" ng-model="formdata.myinput" required /> </form> <div ng-show="myform.myinput.$invalid"> Input field must be a prime number </div> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/7mhqvgcp/
AngularJS provides an ngOptions
directive to populate the <select>
elements in your application. Although this is at first glance a trivial matter, ngOptions
utilizes a convoluted comprehension_expression
that can populate the dropdown from a data object in a variety of ways.
Assume that your application is as follows:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.players = [ { number: 17, name: 'Alshon', position: 'WR' }, { number: 15, name: 'Brandon', position: 'WR' }, { number: 22, name: 'Matt', position: 'RB' }, { number: 83, name: 'Martellus', position: 'TE' }, { number: 6, name: 'Jay', position: 'QB' } ]; $scope.team = { '3B': { number: 9, name: 'Brandon' }, '2B': { number: 19, name: 'Marco' }, '3B': { number: 48, name: 'Pablo' }, 'C': { number: 28, name: 'Buster' }, 'SS': { number: 35, name: 'Brandon' } }; });
The ngOptions
directive allows you to populate a <select>
element with both an array and an object's attributes.
The comprehension expression lets you define how you want to map the data array to a set of <option>
tags and its string label and corresponding values. The easier implementation is to only define the label string, in which case the application will default to set the <option>
value to the entire array element, as follows:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <!—- label for value in array -—> <select ng-model="player" ng-options="p.name for p in players"> </select> </div> </div>
This will compile into the following (with the form CSS classes stripped):
<select ng-model="player" ng-options="player.name for player in players"> <option value="?" selected="selected"></option> <option value="0">Alshon</option> <option value="1">Brandon</option> <option value="2">Matt</option> <option value="3">Martellus</option> <option value="4">Jay</option> </select>
JSFiddle: http://jsfiddle.net/msfrisbie/vy62c575/
Here, the values of each option are the array indices of the corresponding element. As the model it is attached to is not initialized to any of the present elements, AngularJS inserts a temporary null value into the list until a selection is made, at which point the empty value will be stripped out. When a selection is made, the player model will be assigned to the entire object at that array index.
If you don't want to have the <option>
HTML value assigned the array index, you can override this with a track by
clause, as follows:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <!—- label for value in array -—> <select ng-model="player" ng-options="p.name for p in players track by p.number"> </select> </div> </div>
This will compile into the following:
<select ng-model="player" ng-options="p.name for p in players track by p.number"> <option value="?" selected="selected"></option> <option value="17">Alshon</option> <option value="15">Brandon</option> <option value="22">Matt</option> <option value="83">Martellus</option> <option value="6">Jay</option> </select>
JSFiddle: http://jsfiddle.net/msfrisbie/umehb407/
Making a selection will still assign the corresponding object in the array to the player
model.
If instead you wanted to explicitly control the value of each <option>
element and force it to be the number attribute of each array element, you can do the following:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <!—- label for value in array -—> <select ng-model="player" ng-options="p.number as p.name for p in players"> </select> </div> </div>
This will compile into the following (with the form CSS classes stripped):
<select ng-model="player" ng-options="p.number as p.name for p in players"> <option value="?" selected="selected"></option> <option value="17">Alshon</option> <option value="15">Brandon</option> <option value="22">Matt</option> <option value="83">Martellus</option> <option value="6">Jay</option> </select>
JSFiddle: http://jsfiddle.net/msfrisbie/jtsz46cp/
However, now when an <option>
element is selected, the player
model will only be assigned the number attribute of the corresponding object.
If you want to take advantage of the grouping abilities for the <select>
elements, you can add a group by
clause, as follows:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <!—- label for value in array -—> <select ng-model="player" ng-options="p.name group by p.position for p in players"> </select> </div> </div>
This will compile to the following:
<select ng-model="player" ng-options="p.name group by p.position for p in players"> <option value="?" selected="selected"></option> <optgroup label="WR"> <option value="0">Alshon</option> <option value="1">Brandon</option> </optgroup> <optgroup label="RB"> <option value="2">Matt</option> </optgroup> <optgroup label="TE"> <option value="3">Martellus</option> </optgroup> <optgroup label="QB"> <option value="4">Jay</option> </optgroup> </select>
JSFiddle: http://jsfiddle.net/msfrisbie/2d6mdt9m/
If you want to allow a null
option, you can explicitly define one inside your <select>
tag, as follows:
(index.html) <select ng-model="player" ng-options="comprehension_expression"> <option value="">Choose a player</option> </select>
The <select>
elements that use ngOptions
can also be populated from an object's attributes. It functions similarly to how you would process a data array; the only difference being that you must define how the key-value pairs in the object will be used to generate the list of <option>
elements. For a simple utilization to map the value object's number property to the entire value object, you can do the following:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <!—- label for value in array -—> <select ng-model="player" ng-options="p.number for (pos, p) in team"> </select> </div> </div>
This will compile into the following:
<select ng-model="player" ng-options="p.number for (pos, p) in team"> <option value="?" selected="selected"></option> <option value="1B">9</option> <option value="2B">19</option> <option value="3B">48</option> <option value="C">28</option> <option value="SS">35</option> </select>
JSFiddle: http://jsfiddle.net/msfrisbie/zofojs7n/
The <option>
values default to the key string, but the player
model assignment will still be assigned the entire object that the key refers to.
If you don't want to have the <option>
HTML value assigned the property key, you can override this with a select as
clause:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <!—- label for value in array -—> <select ng-model="player" ng-options="p.number as p.name for (pos, p) in team"> </select> </div> </div>
This will compile into the following:
<select ng-model="player" ng-options="p.number as p.name for (pos, p) in team"> <option value="?" selected="selected"></option> <option value="1B">Brandon</option> <option value="2B">Marco</option> <option value="3B">Pablo</option> <option value="C">Buster</option> <option value="SS">Brandon</option> </select>
JSFiddle: http://jsfiddle.net/msfrisbie/ssLzvtaf/
Now, when an <option>
element is selected, the player
model will only be assigned the number property of the corresponding object.
The ngOptions
directive simply breaks apart the enumerable entity it is passed, into digestible pieces that can be converted into <option>
tags.
Inside a <select>
tag, ngOptions
is heavily preferred to ngRepeat
for performance reasons. Data binding isn't as necessary in the case of dropdown values, so an ngRepeat
implementation for a dropdown that must watch many values in the collection adds unnecessary data binding overhead to the application.
Depending on the purpose of your application, you might find yourself with the need to utilize a publish-subscribe (pub-sub) architecture to accomplish certain features. AngularJS provides the proper toolkit to accomplish this, but there are considerations that need to be made to prevent performance degradation and keep the application organized.
Formerly, using the $broadcast
service from a scope with a large number of descendant scopes incurred a significant performance hit due to the large number of potential listeners that needed to be handled. In the AngularJS 1.2.7 release, an optimization was introduced to $broadcast
that limits the reach of the event to only the scopes that are listening for it. With this, $broadcast
can be used more freely throughout your application, but there is still a void to be filled to service applications that demand a pub-sub architecture. Simply put, your application should be able to broadcast an event to subscribers throughout the entire application without utilizing $rootScope.$broadcast()
.
Suppose you have an application that has multiple disparate scopes existing throughout it that need to react to a singular event, as shown here:
(app.js) angular.module('pubSubApp',[]) .controller('Ctrl',function($scope) {}) .directive('myDir',function() { return { scope: {}, link: function(scope, el, attrs) {} }; });
In order to avoid using $rootScope.$broadcast()
, the $rootScope
will instead be used as a unification point for application-wide messaging. Utilizing $rootScope.$on()
and $rootScope.$emit()
allows you to compartmentalize the actual message broadcasting to a single scope and have child scopes inject $rootScope
and tap into the event bus within it.
The most basic and naive implementation is to inject $rootScope
into every location where you need to access the event bus and configure the events locally, as shown here:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <button ng-click="generateEvent()">Generate event</button> </div> <div my-dir></div> </div> (app.js) angular.module('myApp',[]) .controller('Ctrl', function($scope, $rootScope, $log) { $scope.generateEvent = function() { $rootScope.$emit('busEvent'); }; $rootScope.$on('busEvent', function() { $log.log('Handler called!'); }); }) .directive('myDir', function($rootScope, $log) { return { scope: {}, link: function(scope, el, attrs) { $rootScope.$on('busEvent', function() { $log.log('Handler called!'); }); } }; });
JSFiddle: http://jsfiddle.net/msfrisbie/5ot5scja/
With this setup, even a directive with an isolate scope can utilize the event bus to communicate with a controller that it otherwise would not be able to.
If you're paying close attention, you might have noticed that using this pattern introduces a small problem. Controllers in AngularJS are not singletons, and therefore they require more careful memory management when using this type of cross-application architecture.
Specifically, when a controller in your application is destroyed, the event listener attached to a foreign scope that was declared inside it will not be garbage collected, which will lead to memory leaks. To prevent this, registering an event listener with $on()
will return a deregistration function that must be called on the $destroy
event. This can be done as follows:
(app.js) angular.module('myApp',[]) .controller('Ctrl', function($scope, $rootScope, $log) { $scope.generateEvent = function() { $rootScope.$emit('busEvent'); }; var unbind = $rootScope.$on('busEvent', function() { $log.log('Handler called!'); }); $scope.$on('$destroy', unbind); }) .directive('myDir', function($rootScope, $log) { return { scope: {}, link: function(scope, el, attrs) { var unbind = $rootScope.$on('busEvent', function() { $log.log('Handler called!'); }); scope.$on('$destroy', unbind); } }; });
JSFiddle: http://jsfiddle.net/msfrisbie/xq05p9dt/
The event bus logic can be delegated to a service factory. This service can then be dependency-injected anywhere to communicate application-wide events to wherever else listeners exist. This can be done as follows:
(app.js) angular.module('myApp',[]) .controller('Ctrl',function($scope, EventBus, $log) { $scope.generateEvent = function() { EventBus.emitMsg('busEvent'); }; EventBus.onMsg( 'busEvent', function() { $log.log('Handler called!'); }, $scope ); }) .directive('myDir',function($log, EventBus) { return { scope: {}, link: function(scope, el, attrs) { EventBus.onMsg( 'busEvent', function() { $log.log('Handler called!'); }, scope ); } }; }) .factory('EventBus', function($rootScope) { var eventBus = {}; eventBus.emitMsg = function(msg, data) { data = data || {}; $rootScope.$emit(msg, data); }; eventBus.onMsg = function(msg, func, scope) { var unbind = $rootScope.$on(msg, func); if (scope) { scope.$on('$destroy', unbind); } return unbind; }; return eventBus; });
JSFiddle: http://jsfiddle.net/msfrisbie/m88ruycx/
The best and cleanest implementation of an event bus is to implicitly add the publish
and subscribe
utility methods to all scopes by decorating the $rootScope
object during the application's initialization, specifically, the config
phase:
(app.js) angular.module('myApp',[]) .config(function($provide){ $provide.decorator('$rootScope', function($delegate){ // adds to the constructor prototype to allow // use in isolate scopes var proto = $delegate.constructor.prototype; proto.subscribe = function(event, listener) { var unsubscribe = $delegate.$on(event, listener); this.$on('$destroy', unsubscribe); }; proto.publish = function(event, data) { $delegate.$emit(event, data); }; return $delegate; }); }) .controller('Ctrl',function($scope, $log) { $scope.generateEvent = function() { $scope.publish('busEvent'); }; $scope.subscribe('busEvent', function() { $log.log('Handler called!'); }); }) .directive('myDir', function($log) { return { scope: {}, link: function(scope, el, attrs) { scope.subscribe('busEvent', function() { $log.log('Handler called!'); }); } }; });
JSFiddle: http://jsfiddle.net/msfrisbie/5madmyzt/
The event bus acts as a single target of indirection between the disparate entities in the application. As the events do not escape the $rootScope
object, and $rootScope
can be dependency-injected, you are creating an application-wide messaging network.
Performance is always a consideration when it comes to events. It is cleaner and more efficient to delegate as much of your application as possible to the data binding/model layer, but when there are global events that require you to propagate events (such as a login/logout), events can be an extremely useful tool.
34.231.180.210