aboutsummaryrefslogtreecommitdiffstats
path: root/docs/content/tutorial/step_03.ngdoc
blob: a26a43a189b9944778aae0308b185b1e76dcfb60 (plain)
1
2
3
4
5
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
@ngdoc overview
@name Tutorial: 3 - Filtering Repeaters
@description

<ul doc-tutorial-nav="3"></ul>


We did a lot of work in laying a foundation for the app in the last step, so now we'll do something
simple; we will add full text search (yes, it will be simple!). We will also write an end-to-end
test, because a good end-to-end test is a good friend. It stays with your app, keeps an eye on it,
and quickly detects regressions.


<div doc-tutorial-reset="3"></div>


The app now has a search box. Notice that the phone list on the page changes depending on what a
user types into the search box.

The most important differences between Steps 2 and 3 are listed below. You can see the full diff on
{@link https://github.com/angular/angular-phonecat/compare/step-2...step-3
 GitHub}:


## Controller

We made no changes to the controller.


## Template

__`app/index.html`:__
<pre>
  <div class="container-fluid">
    <div class="row-fluid">
      <div class="span2">
        <!--Sidebar content-->

        Search: <input ng-model="query">

      </div>
      <div class="span10">
        <!--Body content-->

        <ul class="phones">
          <li ng-repeat="phone in phones | filter:query">
            {{phone.name}}
            <p>{{phone.snippet}}</p>
          </li>
        </ul>

      </div>
    </div>
  </div>
</pre>

We added a standard HTML `<input>` tag and used Angular's
{@link api/ng.filter:filter filter} function to process the input for the
{@link api/ng.directive:ngRepeat ngRepeat} directive.

This lets a user enter search criteria and immediately see the effects of their search on the phone
list. This new code demonstrates the following:

* Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the
name of the input box to a variable of the same name in the data model and keeps the two in sync.

  In this code, the data that a user types into the input box (named __`query`__) is immediately
available as a filter input in the list repeater (`phone in phones | filter:`__`query`__). When
changes to the data model cause the repeater's input to change, the repeater efficiently updates
the DOM to reflect the current state of the model.

<img  class="diagram" src="img/tutorial/tutorial_03.png">

* Use of the `filter` filter: The {@link api/ng.filter:filter filter} function uses the
`query` value to create a new array that contains only those records that match the `query`.

  `ngRepeat` automatically updates the view in response to the changing number of phones returned
by the `filter` filter. The process is completely transparent to the developer.

## Test

In Step 2, we learned how to write and run unit tests. Unit tests are perfect for testing
controllers and other components of our application written in JavaScript, but they can't easily
test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much
better choice.

The search feature was fully implemented via templates and data-binding, so we'll write our first
end-to-end test, to verify that the feature works.

__`test/e2e/scenarios.js`:__
<pre>
describe('PhoneCat App', function() {

  describe('Phone list view', function() {

    beforeEach(function() {
      browser().navigateTo('../../app/index.html');
    });


    it('should filter the phone list as user types into the search box', function() {
      expect(repeater('.phones li').count()).toBe(3);

      input('query').enter('nexus');
      expect(repeater('.phones li').count()).toBe(1);

      input('query').enter('motorola');
      expect(repeater('.phones li').count()).toBe(2);
    });
  });
});
</pre>

Even though the syntax of this test looks very much like our controller unit test written with
Jasmine, the end-to-end test uses APIs of {@link guide/dev_guide.e2e-testing Angular's end-to-end
test runner}.

To run the end-to-end test, open one of the following in a new browser tab:

* node.js users: {@link http://localhost:8000/test/e2e/runner.html}
* users with other http servers:
`http://localhost:[port-number]/[context-path]/test/e2e/runner.html`
* casual reader: {@link http://angular.github.com/angular-phonecat/step-3/test/e2e/runner.html}

Previously we've seen how Karma can be used to execute unit tests. Well, it can also run the
end-to-end tests! Use `./scripts/e2e-test.sh` script for that. End-to-end tests are slow, so unlike
with unit tests, Karma will exit after the test run and will not automatically rerun the test
suite on every file change. To rerun the test suite, execute the `e2e-test.sh` script again.

Note: You must ensure you've installed karma-ng-scenario prior to running the `e2e-test.sh` script.
You can do this by issuing `npm install karma-ng-scenario` into your terminal.

This test verifies that the search box and the repeater are correctly wired together. Notice how
easy it is to write end-to-end tests in Angular. Although this example is for a simple test, it
really is that easy to set up any functional, readable, end-to-end test.

# Experiments

* Display the current value of the `query` model by adding a `{{query}}` binding into the
`index.html` template, and see how it changes when you type in the input box.

* Let's see how we can get the current value of the `query` model to appear in the HTML page title.

  You might think you could just add the `{{query}}` to the title tag element as follows:

          <title>Google Phone Gallery: {{query}}</title>

  However, when you reload the page, you won't see the expected result. This is because the "query"
  model lives in the scope, defined by the `ng-controller="PhoneListCtrl"` directive, on the body element:

          <body ng-controller="PhoneListCtrl">

  If you want to bind to the query model from the `<title>` element, you must __move__ the
`ngController` declaration to the HTML element because it is the common parent of both the body
and title elements:

          <html ng-app ng-controller="PhoneListCtrl">

  Be sure to __remove__ the `ng-controller` declaration from the body element.

  While using double curlies works fine within the title element, you might have noticed that
for a split second they are actually displayed to the user while the page is loading. A better
solution would be to use the {@link api/ng.directive:ngBind
ngBind} or {@link api/ng.directive:ngBindTemplate
ngBindTemplate} directives, which are invisible to the user while the page is loading:

          <title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>

* Add the following end-to-end test into the `describe` block within `test/e2e/scenarios.js`:

  <pre>
    it('should display the current filter value within an element with id "status"',
        function() {
      expect(element('#status').text()).toMatch(/Current filter: \s*$/);

      input('query').enter('nexus');

      expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/);

      //alternative version of the last assertion that tests just the value of the binding
      using('#status').expect(binding('query')).toBe('nexus');
    });
  </pre>

  Refresh the browser tab with the end-to-end test runner to see the test fail. To make the test
pass, edit the `index.html` template to add a `div` or `p` element with `id` `"status"` and content
with the `query` binding, prefixed by "Current filter:". For instance:

          <div id="status">Current filter: {{query}}</div>

* Add a `pause()` statement inside of an end-to-end test and rerun it. You'll see the runner pause;
this gives you the opportunity to explore the state of your application while it is displayed in
the browser. The app is live! You can change the search query to prove it. Notice how useful this
is for troubleshooting end-to-end tests.


# Summary

We have now added full text search and included a test to verify that search works! Now let's go on
to {@link step_04 step 4} to learn how to add sorting capability to the phone app.


<ul doc-tutorial-nav="3"></ul>

number * * @description * Text input with number validation and transformation. Sets the `number` validation * error if not a valid number. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.value = 12; } </script> <form name="myForm" ng-controller="Ctrl"> Number: <input type="number" name="input" ng-model="value" min="0" max="99" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> <span class="error" ng-show="myForm.input.$error.number"> Not valid number!</span> <tt>value = {{value}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> </form> </doc:source> <doc:scenario> it('should initialize to model', function() { expect(binding('value')).toEqual('12'); expect(binding('myForm.input.$valid')).toEqual('true'); }); it('should be invalid if empty', function() { input('value').enter(''); expect(binding('value')).toEqual(''); expect(binding('myForm.input.$valid')).toEqual('false'); }); it('should be invalid if over max', function() { input('value').enter('123'); expect(binding('value')).toEqual(''); expect(binding('myForm.input.$valid')).toEqual('false'); }); </doc:scenario> </doc:example> */ 'number': numberInputType, /** * @ngdoc inputType * @name ng.directive:input.url * * @description * Text input with URL validation. Sets the `url` validation error key if the content is not a * valid URL. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.text = 'http://google.com'; } </script> <form name="myForm" ng-controller="Ctrl"> URL: <input type="url" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> <span class="error" ng-show="myForm.input.$error.url"> Not valid url!</span> <tt>text = {{text}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> <tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/> </form> </doc:source> <doc:scenario> it('should initialize to model', function() { expect(binding('text')).toEqual('http://google.com'); expect(binding('myForm.input.$valid')).toEqual('true'); }); it('should be invalid if empty', function() { input('text').enter(''); expect(binding('text')).toEqual(''); expect(binding('myForm.input.$valid')).toEqual('false'); }); it('should be invalid if not url', function() { input('text').enter('xxx'); expect(binding('myForm.input.$valid')).toEqual('false'); }); </doc:scenario> </doc:example> */ 'url': urlInputType, /** * @ngdoc inputType * @name ng.directive:input.email * * @description * Text input with email validation. Sets the `email` validation error key if not a valid email * address. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.text = 'me@example.com'; } </script> <form name="myForm" ng-controller="Ctrl"> Email: <input type="email" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> <span class="error" ng-show="myForm.input.$error.email"> Not valid email!</span> <tt>text = {{text}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> <tt>myForm.$error.email = {{!!myForm.$error.email}}</tt><br/> </form> </doc:source> <doc:scenario> it('should initialize to model', function() { expect(binding('text')).toEqual('me@example.com'); expect(binding('myForm.input.$valid')).toEqual('true'); }); it('should be invalid if empty', function() { input('text').enter(''); expect(binding('text')).toEqual(''); expect(binding('myForm.input.$valid')).toEqual('false'); }); it('should be invalid if not email', function() { input('text').enter('xxx'); expect(binding('myForm.input.$valid')).toEqual('false'); }); </doc:scenario> </doc:example> */ 'email': emailInputType, /** * @ngdoc inputType * @name ng.directive:input.radio * * @description * HTML radio button. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string} value The value to which the expression should be set when selected. * @param {string=} name Property name of the form under which the control is published. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.color = 'blue'; } </script> <form name="myForm" ng-controller="Ctrl"> <input type="radio" ng-model="color" value="red"> Red <br/> <input type="radio" ng-model="color" value="green"> Green <br/> <input type="radio" ng-model="color" value="blue"> Blue <br/> <tt>color = {{color}}</tt><br/> </form> </doc:source> <doc:scenario> it('should change state', function() { expect(binding('color')).toEqual('blue'); input('color').select('red'); expect(binding('color')).toEqual('red'); }); </doc:scenario> </doc:example> */ 'radio': radioInputType, /** * @ngdoc inputType * @name ng.directive:input.checkbox * * @description * HTML checkbox. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} ngTrueValue The value to which the expression should be set when selected. * @param {string=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.value1 = true; $scope.value2 = 'YES' } </script> <form name="myForm" ng-controller="Ctrl"> Value1: <input type="checkbox" ng-model="value1"> <br/> Value2: <input type="checkbox" ng-model="value2" ng-true-value="YES" ng-false-value="NO"> <br/> <tt>value1 = {{value1}}</tt><br/> <tt>value2 = {{value2}}</tt><br/> </form> </doc:source> <doc:scenario> it('should change state', function() { expect(binding('value1')).toEqual('true'); expect(binding('value2')).toEqual('YES'); input('value1').check(); input('value2').check(); expect(binding('value1')).toEqual('false'); expect(binding('value2')).toEqual('NO'); }); </doc:scenario> </doc:example> */ 'checkbox': checkboxInputType, 'hidden': noop, 'button': noop, 'submit': noop, 'reset': noop }; function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { var listener = function() { var value = element.val(); // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming // e.g. <input ng-model="foo" ng-trim="false"> if (toBoolean(attr.ngTrim || 'T')) { value = trim(value); } if (ctrl.$viewValue !== value) { scope.$apply(function() { ctrl.$setViewValue(value); }); } }; // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the // input event on backspace, delete or cut if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { var timeout; var deferListener = function() { if (!timeout) { timeout = $browser.defer(function() { listener(); timeout = null; }); } }; element.on('keydown', function(event) { var key = event.keyCode; // ignore // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; deferListener(); }); // if user paste into input using mouse, we need "change" event to catch it element.on('change', listener); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it if ($sniffer.hasEvent('paste')) { element.on('paste cut', deferListener); } } ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; // pattern validator var pattern = attr.ngPattern, patternValidator, match; var validate = function(regexp, value) { if (ctrl.$isEmpty(value) || regexp.test(value)) { ctrl.$setValidity('pattern', true); return value; } else { ctrl.$setValidity('pattern', false); return undefined; } }; if (pattern) { match = pattern.match(/^\/(.*)\/([gim]*)$/); if (match) { pattern = new RegExp(match[1], match[2]); patternValidator = function(value) { return validate(pattern, value); }; } else { patternValidator = function(value) { var patternObj = scope.$eval(pattern); if (!patternObj || !patternObj.test) { throw minErr('ngPattern')('noregexp', 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, patternObj, startingTag(element)); } return validate(patternObj, value); }; } ctrl.$formatters.push(patternValidator); ctrl.$parsers.push(patternValidator); } // min length validator if (attr.ngMinlength) { var minlength = int(attr.ngMinlength); var minLengthValidator = function(value) { if (!ctrl.$isEmpty(value) && value.length < minlength) { ctrl.$setValidity('minlength', false); return undefined; } else { ctrl.$setValidity('minlength', true); return value; } }; ctrl.$parsers.push(minLengthValidator); ctrl.$formatters.push(minLengthValidator); } // max length validator if (attr.ngMaxlength) { var maxlength = int(attr.ngMaxlength); var maxLengthValidator = function(value) { if (!ctrl.$isEmpty(value) && value.length > maxlength) { ctrl.$setValidity('maxlength', false); return undefined; } else { ctrl.$setValidity('maxlength', true); return value; } }; ctrl.$parsers.push(maxLengthValidator); ctrl.$formatters.push(maxLengthValidator); } } function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { var empty = ctrl.$isEmpty(value); if (empty || NUMBER_REGEXP.test(value)) { ctrl.$setValidity('number', true); return value === '' ? null : (empty ? value : parseFloat(value)); } else { ctrl.$setValidity('number', false); return undefined; } }); ctrl.$formatters.push(function(value) { return ctrl.$isEmpty(value) ? '' : '' + value; }); if (attr.min) { var minValidator = function(value) { var min = parseFloat(attr.min); if (!ctrl.$isEmpty(value) && value < min) { ctrl.$setValidity('min', false); return undefined; } else { ctrl.$setValidity('min', true); return value; } }; ctrl.$parsers.push(minValidator); ctrl.$formatters.push(minValidator); } if (attr.max) { var maxValidator = function(value) { var max = parseFloat(attr.max); if (!ctrl.$isEmpty(value) && value > max) { ctrl.$setValidity('max', false); return undefined; } else { ctrl.$setValidity('max', true); return value; } }; ctrl.$parsers.push(maxValidator); ctrl.$formatters.push(maxValidator); } ctrl.$formatters.push(function(value) { if (ctrl.$isEmpty(value) || isNumber(value)) { ctrl.$setValidity('number', true); return value; } else { ctrl.$setValidity('number', false); return undefined; } }); } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); var urlValidator = function(value) { if (ctrl.$isEmpty(value) || URL_REGEXP.test(value)) { ctrl.$setValidity('url', true); return value; } else { ctrl.$setValidity('url', false); return undefined; } }; ctrl.$formatters.push(urlValidator); ctrl.$parsers.push(urlValidator); } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); var emailValidator = function(value) { if (ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value)) { ctrl.$setValidity('email', true); return value; } else { ctrl.$setValidity('email', false); return undefined; } }; ctrl.$formatters.push(emailValidator); ctrl.$parsers.push(emailValidator); } function radioInputType(scope, element, attr, ctrl) { // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } element.on('click', function() { if (element[0].checked) { scope.$apply(function() { ctrl.$setViewValue(attr.value); }); } }); ctrl.$render = function() { var value = attr.value; element[0].checked = (value == ctrl.$viewValue); }; attr.$observe('value', ctrl.$render); } function checkboxInputType(scope, element, attr, ctrl) { var trueValue = attr.ngTrueValue, falseValue = attr.ngFalseValue; if (!isString(trueValue)) trueValue = true; if (!isString(falseValue)) falseValue = false; element.on('click', function() { scope.$apply(function() { ctrl.$setViewValue(element[0].checked); }); }); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; }; // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. ctrl.$isEmpty = function(value) { return value !== trueValue; }; ctrl.$formatters.push(function(value) { return value === trueValue; }); ctrl.$parsers.push(function(value) { return value ? trueValue : falseValue; }); } /** * @ngdoc directive * @name ng.directive:textarea * @restrict E * * @description * HTML textarea element control with angular data-binding. The data-binding and validation * properties of this element are exactly the same as those of the * {@link ng.directive:input input element}. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of * `required` when you want to data-bind to the `required` attribute. * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. */ /** * @ngdoc directive * @name ng.directive:input * @restrict E * * @description * HTML input element control with angular data-binding. Input control follows HTML5 input types * and polyfills the HTML5 validation behavior for older browsers. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {boolean=} ngRequired Sets `required` attribute if set to true * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.user = {name: 'guest', last: 'visitor'}; } </script> <div ng-controller="Ctrl"> <form name="myForm"> User name: <input type="text" name="userName" ng-model="user.name" required> <span class="error" ng-show="myForm.userName.$error.required"> Required!</span><br> Last name: <input type="text" name="lastName" ng-model="user.last" ng-minlength="3" ng-maxlength="10"> <span class="error" ng-show="myForm.lastName.$error.minlength"> Too short!</span> <span class="error" ng-show="myForm.lastName.$error.maxlength"> Too long!</span><br> </form> <hr> <tt>user = {{user}}</tt><br/> <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br> <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br> <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br> <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br> <tt>myForm.$valid = {{myForm.$valid}}</tt><br> <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br> <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br> <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br> </div> </doc:source> <doc:scenario> it('should initialize to model', function() { expect(binding('user')).toEqual('{"name":"guest","last":"visitor"}'); expect(binding('myForm.userName.$valid')).toEqual('true'); expect(binding('myForm.$valid')).toEqual('true'); }); it('should be invalid if empty when required', function() { input('user.name').enter(''); expect(binding('user')).toEqual('{"last":"visitor"}'); expect(binding('myForm.userName.$valid')).toEqual('false'); expect(binding('myForm.$valid')).toEqual('false'); }); it('should be valid if empty when min length is set', function() { input('user.last').enter(''); expect(binding('user')).toEqual('{"name":"guest","last":""}'); expect(binding('myForm.lastName.$valid')).toEqual('true'); expect(binding('myForm.$valid')).toEqual('true'); }); it('should be invalid if less than required min length', function() { input('user.last').enter('xx'); expect(binding('user')).toEqual('{"name":"guest"}'); expect(binding('myForm.lastName.$valid')).toEqual('false'); expect(binding('myForm.lastName.$error')).toMatch(/minlength/); expect(binding('myForm.$valid')).toEqual('false'); }); it('should be invalid if longer than max length', function() { input('user.last').enter('some ridiculously long name'); expect(binding('user')) .toEqual('{"name":"guest"}'); expect(binding('myForm.lastName.$valid')).toEqual('false'); expect(binding('myForm.lastName.$error')).toMatch(/maxlength/); expect(binding('myForm.$valid')).toEqual('false'); }); </doc:scenario> </doc:example> */ var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { return { restrict: 'E', require: '?ngModel', link: function(scope, element, attr, ctrl) { if (ctrl) { (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, $browser); } } }; }]; var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', DIRTY_CLASS = 'ng-dirty'; /** * @ngdoc object * @name ng.directive:ngModel.NgModelController * * @property {string} $viewValue Actual string value in the view. * @property {*} $modelValue The value in the model, that the control is bound to. * @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever the control reads value from the DOM. Each function is called, in turn, passing the value through to the next. Used to sanitize / convert the value as well as validation. For validation, the parsers should update the validity state using {@link ng.directive:ngModel.NgModelController#methods_$setValidity $setValidity()}, and return `undefined` for invalid values. * * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever the model value changes. Each function is called, in turn, passing the value through to the next. Used to format / convert values for display in the control and validation. * <pre> * function formatter(value) { * if (value) { * return value.toUpperCase(); * } * } * ngModel.$formatters.push(formatter); * </pre> * @property {Object} $error An object hash with all errors as keys. * * @property {boolean} $pristine True if user has not interacted with the control yet. * @property {boolean} $dirty True if user has already interacted with the control. * @property {boolean} $valid True if there is no error. * @property {boolean} $invalid True if at least one error on the control. * * @description * * `NgModelController` provides API for the `ng-model` directive. The controller contains * services for data-binding, validation, CSS updates, and value formatting and parsing. It * purposefully does not contain any logic which deals with DOM rendering or listening to * DOM events. Such DOM related logic should be provided by other directives which make use of * `NgModelController` for data-binding. * * ## Custom Control Example * This example shows how to use `NgModelController` with a custom control to achieve * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) * collaborate together to achieve the desired result. * * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element * contents be edited in place by the user. This will not work on older browsers. * * <example module="customControl"> <file name="style.css"> [contenteditable] { border: 1px solid black; background-color: white; min-height: 20px; } .ng-invalid { border: 1px solid red; } </file> <file name="script.js"> angular.module('customControl', []). directive('contenteditable', function() { return { restrict: 'A', // only activate on element attribute require: '?ngModel', // get a hold of NgModelController link: function(scope, element, attrs, ngModel) { if(!ngModel) return; // do nothing if no ng-model // Specify how UI should be updated ngModel.$render = function() { element.html(ngModel.$viewValue || ''); }; // Listen for change events to enable binding element.on('blur keyup change', function() { scope.$apply(read); }); read(); // initialize // Write data to the model function read() { var html = element.html(); // When we clear the content editable the browser leaves a <br> behind // If strip-br attribute is provided then we strip this out if( attrs.stripBr && html == '<br>' ) { html = ''; } ngModel.$setViewValue(html); } } }; }); </file> <file name="index.html"> <form name="myForm"> <div contenteditable name="myWidget" ng-model="userContent" strip-br="true" required>Change me!</div> <span ng-show="myForm.myWidget.$error.required">Required!</span> <hr> <textarea ng-model="userContent"></textarea> </form> </file> <file name="scenario.js"> it('should data-bind and become invalid', function() { var contentEditable = element('[contenteditable]'); expect(contentEditable.text()).toEqual('Change me!'); input('userContent').enter(''); expect(contentEditable.text()).toEqual(''); expect(contentEditable.prop('className')).toMatch(/ng-invalid-required/); }); </file> * </example> * * ## Isolated Scope Pitfall * * Note that if you have a directive with an isolated scope, you cannot require `ngModel` * since the model value will be looked up on the isolated scope rather than the outer scope. * When the directive updates the model value, calling `ngModel.$setViewValue()` the property * on the outer scope will not be updated. * * Here is an example of this situation. You'll notice that even though both 'input' and 'div' * seem to be attached to the same model, they are not kept in synch. * * <example module="badIsolatedDirective"> <file name="script.js"> angular.module('badIsolatedDirective', []).directive('bad', function() { return { require: 'ngModel', scope: { }, template: '<input ng-model="innerModel">', link: function(scope, element, attrs, ngModel) { scope.$watch('innerModel', function(value) { console.log(value); ngModel.$setViewValue(value); }); } }; }); </file> <file name="index.html"> <input ng-model="someModel"> <div bad ng-model="someModel"></div> </file> * </example> * * */ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', function($scope, $exceptionHandler, $attr, $element, $parse) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$parsers = []; this.$formatters = []; this.$viewChangeListeners = []; this.$pristine = true; this.$dirty = false; this.$valid = true; this.$invalid = false; this.$name = $attr.name; var ngModelGet = $parse($attr.ngModel), ngModelSet = ngModelGet.assign; if (!ngModelSet) { throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", $attr.ngModel, startingTag($element)); } /** * @ngdoc function * @name ng.directive:ngModel.NgModelController#$render * @methodOf ng.directive:ngModel.NgModelController * * @description * Called when the view needs to be updated. It is expected that the user of the ng-model * directive will implement this method. */ this.$render = noop; /** * @ngdoc function * @name { ng.directive:ngModel.NgModelController#$isEmpty * @methodOf ng.directive:ngModel.NgModelController * * @description * This is called when we need to determine if the value of the input is empty. * * For instance, the required directive does this to work out if the input has data or not. * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. * * You can override this for input directives whose concept of being empty is different to the * default. The `checkboxInputType` directive does this because in its case a value of `false` * implies empty. */ this.$isEmpty = function(value) { return isUndefined(value) || value === '' || value === null || value !== value; }; var parentForm = $element.inheritedData('$formController') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid $error = this.$error = {}; // keep invalid keys here // Setup initial state of the control $element.addClass(PRISTINE_CLASS); toggleValidCss(true); // convenience method for easy toggling of classes function toggleValidCss(isValid, validationErrorKey) { validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; $element. removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); } /** * @ngdoc function * @name ng.directive:ngModel.NgModelController#$setValidity * @methodOf ng.directive:ngModel.NgModelController * * @description * Change the validity state, and notifies the form when the control changes validity. (i.e. it * does not notify form if given validator is already marked as invalid). * * This method should be called by validators - i.e. the parser or formatter functions. * * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. * The `validationErrorKey` should be in camelCase and will get converted into dash-case * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` * class and can be bound to as `{{someForm.someControl.$error.myError}}` . * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). */ this.$setValidity = function(validationErrorKey, isValid) { // Purposeful use of ! here to cast isValid to boolean in case it is undefined // jshint -W018 if ($error[validationErrorKey] === !isValid) return; // jshint +W018 if (isValid) { if ($error[validationErrorKey]) invalidCount--; if (!invalidCount) { toggleValidCss(true); this.$valid = true; this.$invalid = false; } } else { toggleValidCss(false); this.$invalid = true; this.$valid = false; invalidCount++; } $error[validationErrorKey] = !isValid; toggleValidCss(isValid, validationErrorKey); parentForm.$setValidity(validationErrorKey, isValid, this); }; /** * @ngdoc function * @name ng.directive:ngModel.NgModelController#$setPristine * @methodOf ng.directive:ngModel.NgModelController * * @description * Sets the control to its pristine state. * * This method can be called to remove the 'ng-dirty' class and set the control to its pristine * state (ng-pristine class). */ this.$setPristine = function () { this.$dirty = false; this.$pristine = true; $element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); }; /** * @ngdoc function * @name ng.directive:ngModel.NgModelController#$setViewValue * @methodOf ng.directive:ngModel.NgModelController * * @description * Read a value from view. * * This method should be called from within a DOM event handler. * For example {@link ng.directive:input input} or * {@link ng.directive:select select} directives call it. * * It internally calls all `$parsers` (including validators) and updates the `$modelValue` and the actual model path. * Lastly it calls all registered change listeners. * * @param {string} value Value from the view. */ this.$setViewValue = function(value) { this.$viewValue = value; // change to dirty if (this.$pristine) { this.$dirty = true; this.$pristine = false; $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); parentForm.$setDirty(); } forEach(this.$parsers, function(fn) { value = fn(value); }); if (this.$modelValue !== value) { this.$modelValue = value; ngModelSet($scope, value); forEach(this.$viewChangeListeners, function(listener) { try { listener(); } catch(e) { $exceptionHandler(e); } }); } }; // model -> value var ctrl = this; $scope.$watch(function ngModelWatch() { var value = ngModelGet($scope); // if scope model value and ngModel value are out of sync if (ctrl.$modelValue !== value) { var formatters = ctrl.$formatters, idx = formatters.length; ctrl.$modelValue = value; while(idx--) { value = formatters[idx](value); } if (ctrl.$viewValue !== value) { ctrl.$viewValue = value; ctrl.$render(); } } }); }]; /** * @ngdoc directive * @name ng.directive:ngModel * * @element input * * @description * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a * property on the scope using {@link ng.directive:ngModel.NgModelController NgModelController}, * which is created and exposed by this directive. * * `ngModel` is responsible for: * * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` * require. * - Providing validation behavior (i.e. required, number, email, url). * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`). * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the * current scope. If the property doesn't already exist on this scope, it will be created * implicitly and added to the scope. * * For best practices on using `ngModel`, see: * * - {@link https://github.com/angular/angular.js/wiki/Understanding-Scopes} * * For basic examples, how to use `ngModel`, see: * * - {@link ng.directive:input input} * - {@link ng.directive:input.text text} * - {@link ng.directive:input.checkbox checkbox} * - {@link ng.directive:input.radio radio} * - {@link ng.directive:input.number number} * - {@link ng.directive:input.email email} * - {@link ng.directive:input.url url} * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * */ var ngModelDirective = function() { return { require: ['ngModel', '^?form'], controller: NgModelController, link: function(scope, element, attr, ctrls) { // notify others, especially parent forms var modelCtrl = ctrls[0], formCtrl = ctrls[1] || nullFormCtrl; formCtrl.$addControl(modelCtrl); element.on('$destroy', function() { formCtrl.$removeControl(modelCtrl); }); } }; }; /** * @ngdoc directive * @name ng.directive:ngChange * * @description * Evaluate given expression when user changes the input. * The expression is not evaluated when the value change is coming from the model. * * Note, this directive requires `ngModel` to be present. * * @element input * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change * in input value. * * @example * <doc:example> * <doc:source> * <script> * function Controller($scope) { * $scope.counter = 0; * $scope.change = function() { * $scope.counter++; * }; * } * </script> * <div ng-controller="Controller"> * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" /> * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" /> * <label for="ng-change-example2">Confirmed</label><br /> * debug = {{confirmed}}<br /> * counter = {{counter}} * </div> * </doc:source> * <doc:scenario> * it('should evaluate the expression if changing from view', function() { * expect(binding('counter')).toEqual('0'); * element('#ng-change-example1').click(); * expect(binding('counter')).toEqual('1'); * expect(binding('confirmed')).toEqual('true'); * }); * * it('should not evaluate the expression if changing from model', function() { * element('#ng-change-example2').click(); * expect(binding('counter')).toEqual('0'); * expect(binding('confirmed')).toEqual('true'); * }); * </doc:scenario> * </doc:example> */ var ngChangeDirective = valueFn({ require: 'ngModel', link: function(scope, element, attr, ctrl) { ctrl.$viewChangeListeners.push(function() { scope.$eval(attr.ngChange); }); } }); var requiredDirective = function() { return { require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; attr.required = true; // force truthy in case we are on non input element var validator = function(value) { if (attr.required && ctrl.$isEmpty(value)) { ctrl.$setValidity('required', false); return; } else { ctrl.$setValidity('required', true); return value; } }; ctrl.$formatters.push(validator); ctrl.$parsers.unshift(validator); attr.$observe('required', function() { validator(ctrl.$viewValue); }); } }; }; /** * @ngdoc directive * @name ng.directive:ngList * * @description * Text input that converts between a delimited string and an array of strings. The delimiter * can be a fixed string (by default a comma) or a regular expression. * * @element input * @param {string=} ngList optional delimiter that should be used to split the value. If * specified in form `/something/` then the value will be converted into a regular expression. * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.names = ['igor', 'misko', 'vojta']; } </script> <form name="myForm" ng-controller="Ctrl"> List: <input name="namesInput" ng-model="names" ng-list required> <span class="error" ng-show="myForm.namesInput.$error.required"> Required!</span> <br> <tt>names = {{names}}</tt><br/> <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> </form> </doc:source> <doc:scenario> it('should initialize to model', function() { expect(binding('names')).toEqual('["igor","misko","vojta"]'); expect(binding('myForm.namesInput.$valid')).toEqual('true'); expect(element('span.error').css('display')).toBe('none'); }); it('should be invalid if empty', function() { input('names').enter(''); expect(binding('names')).toEqual(''); expect(binding('myForm.namesInput.$valid')).toEqual('false'); expect(element('span.error').css('display')).not().toBe('none'); }); </doc:scenario> </doc:example> */ var ngListDirective = function() { return { require: 'ngModel', link: function(scope, element, attr, ctrl) { var match = /\/(.*)\//.exec(attr.ngList), separator = match && new RegExp(match[1]) || attr.ngList || ','; var parse = function(viewValue) { // If the viewValue is invalid (say required but empty) it will be `undefined` if (isUndefined(viewValue)) return; var list = []; if (viewValue) { forEach(viewValue.split(separator), function(value) { if (value) list.push(trim(value)); }); } return list; }; ctrl.$parsers.push(parse); ctrl.$formatters.push(function(value) { if (isArray(value)) { return value.join(', '); } return undefined; }); // Override the standard $isEmpty because an empty array means the input is empty. ctrl.$isEmpty = function(value) { return !value || !value.length; }; } }; }; var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; /** * @ngdoc directive * @name ng.directive:ngValue * * @description * Binds the given expression to the value of `input[select]` or `input[radio]`, so * that when the element is selected, the `ngModel` of that element is set to the * bound value. * * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as * shown below. * * @element input * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute * of the `input` element * * @example <doc:example> <doc:source> <script> function Ctrl($scope) { $scope.names = ['pizza', 'unicorns', 'robots']; $scope.my = { favorite: 'unicorns' }; } </script> <form ng-controller="Ctrl"> <h2>Which is your favorite?</h2> <label ng-repeat="name in names" for="{{name}}"> {{name}} <input type="radio" ng-model="my.favorite" ng-value="name" id="{{name}}" name="favorite"> </label> </span> <div>You chose {{my.favorite}}</div> </form> </doc:source> <doc:scenario> it('should initialize to model', function() { expect(binding('my.favorite')).toEqual('unicorns'); }); it('should bind the values to the inputs', function() { input('my.favorite').select('pizza'); expect(binding('my.favorite')).toEqual('pizza'); }); </doc:scenario> </doc:example> */ var ngValueDirective = function() { return { priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { return function ngValueConstantLink(scope, elm, attr) { attr.$set('value', scope.$eval(attr.ngValue)); }; } else { return function ngValueLink(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { attr.$set('value', value); }); }; } } }; };