entangled 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -3
  3. data/README.md +6 -1
  4. data/{spec/dummy/public/js/bower.json → bower.json} +10 -1
  5. data/entangled.gemspec +1 -1
  6. data/{spec/dummy/public/js/entangled.js → entangled.js} +13 -2
  7. data/lib/entangled.rb +1 -0
  8. data/lib/entangled/controller.rb +4 -2
  9. data/lib/entangled/model.rb +2 -2
  10. data/lib/entangled/version.rb +1 -1
  11. data/spec/dummy/app/controllers/messages_controller.rb +6 -0
  12. data/spec/dummy/config/environments/test.rb +15 -0
  13. data/spec/dummy/db/development.sqlite3 +0 -0
  14. data/spec/dummy/db/test.sqlite3 +0 -0
  15. data/spec/dummy/log/development.log +18370 -0
  16. data/spec/dummy/public/Gruntfile.js +30 -0
  17. data/spec/dummy/public/app/app.js +20 -0
  18. data/spec/dummy/public/app/controllers/message.js +16 -0
  19. data/spec/dummy/public/app/controllers/messages.js +23 -0
  20. data/spec/dummy/public/app/entangled/entangled.js +191 -0
  21. data/spec/dummy/public/app/services/message.js +21 -0
  22. data/spec/dummy/public/bower.json +22 -0
  23. data/spec/dummy/public/bower_components/angular-route/README.md +2 -11
  24. data/spec/dummy/public/bower_components/angular-route/angular-route.js +1 -1
  25. data/spec/dummy/public/bower_components/angular-route/angular-route.min.js +1 -1
  26. data/spec/dummy/public/bower_components/angular-route/bower.json +2 -2
  27. data/spec/dummy/public/bower_components/angular-route/package.json +2 -2
  28. data/spec/dummy/public/bower_components/angular/README.md +2 -5
  29. data/spec/dummy/public/bower_components/angular/angular.js +88 -37
  30. data/spec/dummy/public/bower_components/angular/angular.min.js +223 -223
  31. data/spec/dummy/public/bower_components/angular/angular.min.js.gzip +0 -0
  32. data/spec/dummy/public/bower_components/angular/angular.min.js.map +2 -2
  33. data/spec/dummy/public/bower_components/angular/bower.json +1 -1
  34. data/spec/dummy/public/bower_components/angular/package.json +2 -2
  35. data/spec/dummy/public/index.html +5 -2
  36. data/spec/dummy/public/package.json +25 -0
  37. data/spec/dummy/public/test/controllers/messages_test.js +100 -0
  38. metadata +19196 -12
  39. data/spec/dummy/public/bower_components/angular-route/.bower.json +0 -20
  40. data/spec/dummy/public/bower_components/angular/.bower.json +0 -18
  41. data/spec/dummy/public/js/app.js +0 -70
@@ -0,0 +1,30 @@
1
+ // Created with the help of
2
+ // http://paislee.io/testing-angularjs-with-grunt-karma-and-jasmine/
3
+
4
+ module.exports = function(grunt) {
5
+
6
+ grunt.loadNpmTasks('grunt-karma');
7
+
8
+ grunt.initConfig({
9
+ karma: {
10
+ unit: {
11
+ options: {
12
+ frameworks: ['jasmine'],
13
+ singleRun: true,
14
+ browsers: ['Chrome'], // 'PhantomJS'
15
+ files: [
16
+ 'bower_components/angular/angular.js',
17
+ 'bower_components/angular-route/angular-route.js',
18
+ 'bower_components/angular-mocks/angular-mocks.js',
19
+ 'app/**/*.js',
20
+ 'test/**/*.js'
21
+ ]
22
+ }
23
+ }
24
+ }
25
+ });
26
+
27
+ grunt.registerTask('test', [
28
+ 'karma'
29
+ ]);
30
+ };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ angular.module('entangledTest', [
4
+ 'ngRoute',
5
+ 'entangled'
6
+ ])
7
+
8
+ .config(function($routeProvider) {
9
+ $routeProvider
10
+ .when('/', {
11
+ templateUrl: 'views/messages/index.html',
12
+ controller: 'MessagesCtrl'
13
+ }).when('/messages/:id', {
14
+ templateUrl: 'views/messages/show.html',
15
+ controller: 'MessageCtrl'
16
+ })
17
+ .otherwise({
18
+ redirectTo: '/'
19
+ });
20
+ })
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ angular.module('entangledTest')
4
+
5
+ .controller('MessageCtrl', function($scope, $routeParams, Message) {
6
+ $scope.update = function() {
7
+ $scope.message.$save();
8
+ };
9
+
10
+ Message.find($routeParams.id, function(message) {
11
+ $scope.$apply(function() {
12
+ $scope.message = message;
13
+ console.log('Show callback called!');
14
+ });
15
+ });
16
+ });
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ angular.module('entangledTest')
4
+
5
+ .controller('MessagesCtrl', function($scope, Message) {
6
+ $scope.message = Message.new();
7
+
8
+ $scope.create = function() {
9
+ $scope.message.$save(function() {
10
+ $scope.message = Message.new();
11
+ });
12
+ };
13
+
14
+ $scope.destroy = function(message) {
15
+ message.$destroy();
16
+ };
17
+
18
+ Message.all(function(messages) {
19
+ $scope.$apply(function() {
20
+ $scope.messages = messages;
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,191 @@
1
+ // Register Angular module and call it 'entangled'
2
+ angular.module('entangled', [])
3
+
4
+ // Register service and call it 'Entangled'
5
+ .factory('Entangled', function() {
6
+ // Every response coming from the server will be wrapped
7
+ // in a Resource constructor to represent a CRUD-able
8
+ // resource that can be saved and destroyed using the
9
+ // methods $save() and $destroy. A Resource also
10
+ // stores the socket's URL it was retrieved from so it
11
+ // can be reused for other requests.
12
+ var Resource = function(params, webSocketUrl) {
13
+ // Assign proerties
14
+ for (var key in params) {
15
+ // Skip inherent object properties
16
+ if (params.hasOwnProperty(key)) {
17
+ this[key] = params[key];
18
+ }
19
+ }
20
+
21
+ this.webSocketUrl = webSocketUrl;
22
+ };
23
+
24
+ // $save() will send a request to the server
25
+ // to either create a new record or update
26
+ // an existing, depending on whether or not
27
+ // an id is present.
28
+ Resource.prototype.$save = function(callback) {
29
+ var that = this;
30
+
31
+ if (this.id) {
32
+ // Update
33
+ var socket = new WebSocket(that.webSocketUrl + '/' + that.id + '/update');
34
+ socket.onopen = function() {
35
+ socket.send(JSON.stringify(that));
36
+
37
+ if (callback) callback();
38
+ };
39
+ } else {
40
+ // Create
41
+ var socket = new WebSocket(that.webSocketUrl + '/create');
42
+
43
+ socket.onopen = function() {
44
+ socket.send(JSON.stringify(that));
45
+
46
+ if (callback) callback();
47
+ };
48
+ }
49
+ };
50
+
51
+ // $destroy() will send a request to the server to
52
+ // destroy an existing record.
53
+ Resource.prototype.$destroy = function(callback) {
54
+ var socket = new WebSocket(this.webSocketUrl + '/' + this.id + '/destroy');
55
+ socket.onopen = function() {
56
+ // It's fine to send an empty message since the
57
+ // socket's URL contains all the information
58
+ // needed to destroy the record (the id).
59
+ socket.send(null);
60
+
61
+ if (callback) callback();
62
+ };
63
+ };
64
+
65
+ // Resources wraps all individual Resource objects
66
+ // in a collection.
67
+ var Resources = function(resources, webSocketUrl) {
68
+ this.all = [];
69
+
70
+ for (var i = 0; i < resources.length; i++) {
71
+ var resource = new Resource(resources[i], webSocketUrl);
72
+ this.all.push(resource);
73
+ }
74
+ };
75
+
76
+ // Entangled is the heart of this service. It contains
77
+ // several methods to instantiate a new Resource,
78
+ // retrieve an existing Resource from the server,
79
+ // and retrieve a collection of existing Resources
80
+ // from the server.
81
+ //
82
+ // Entangled is a constructor that takes the URL
83
+ // of the index action on the server where the
84
+ // Resources can be retrieved.
85
+ var Entangled = function(webSocketUrl) {
86
+ // Store the root URL that sockets
87
+ // will connect to
88
+ this.webSocketUrl = webSocketUrl;
89
+ };
90
+
91
+ // Function to instantiate a new Resource, optionally
92
+ // with given parameters
93
+ Entangled.prototype.new = function(params) {
94
+ return new Resource(params, this.webSocketUrl);
95
+ };
96
+
97
+ // Retrieve all Resources from the root of the socket's
98
+ // URL
99
+ Entangled.prototype.all = function(callback) {
100
+ var socket = new WebSocket(this.webSocketUrl);
101
+
102
+ socket.onmessage = function(event) {
103
+ // If the message from the server isn't empty
104
+ if (event.data.length) {
105
+ // Convert message to JSON
106
+ var data = JSON.parse(event.data);
107
+
108
+ // If the collection of Resources was sent
109
+ if (data.resources) {
110
+ // Store retrieved Resources in property
111
+ this.resources = new Resources(data.resources, socket.url);
112
+ } else if (data.action) {
113
+ if (data.action === 'create') {
114
+ // If new Resource was created, add it to the
115
+ // collection
116
+ this.resources.all.push(new Resource(data.resource, socket.url));
117
+ } else if (data.action === 'update') {
118
+ // If an existing Resource was updated,
119
+ // update it in the collection as well
120
+ var index;
121
+
122
+ for (var i = 0; i < this.resources.all.length; i++) {
123
+ if (this.resources.all[i].id === data.resource.id) {
124
+ index = i;
125
+ }
126
+ }
127
+
128
+ this.resources.all[index] = new Resource(data.resource, socket.url);
129
+ } else if (data.action === 'destroy') {
130
+ // If a Resource was destroyed, remove it
131
+ // from Resources as well
132
+ var index;
133
+
134
+ for (var i = 0; i < this.resources.all.length; i++) {
135
+ if (this.resources.all[i].id === data.resource.id) {
136
+ index = i;
137
+ }
138
+ }
139
+
140
+ this.resources.all.splice(index, 1);
141
+ } else {
142
+ console.log('Something else other than CRUD happened...');
143
+ console.log(data);
144
+ }
145
+ }
146
+ }
147
+
148
+ // Run the callback and pass in the
149
+ // resulting collection
150
+ callback(this.resources.all);
151
+ };
152
+ };
153
+
154
+ // Find an individual Resource on the server
155
+ Entangled.prototype.find = function(id, callback) {
156
+ var webSocketUrl = this.webSocketUrl;
157
+ var socket = new WebSocket(webSocketUrl + '/' + id);
158
+
159
+ socket.onmessage = function(event) {
160
+ // If the message from the server isn't empty
161
+ if (event.data.length) {
162
+ // Parse message and convert to JSON
163
+ var data = JSON.parse(event.data);
164
+
165
+ if (data.resource && !data.action) {
166
+ // If the Resource was sent from the server,
167
+ // store it
168
+ this.resource = new Resource(data.resource, webSocketUrl);
169
+ } else if (data.action) {
170
+ if (data.action === 'update') {
171
+ // If the stored Resource was updated,
172
+ // update it here as well
173
+ this.resource = new Resource(data.resource, webSocketUrl);
174
+ } else if (data.action === 'destroy') {
175
+ // If the stored Resource was destroyed,
176
+ // remove it from here as well
177
+ this.resource = undefined;
178
+ }
179
+ } else {
180
+ console.log('Something else other than CRUD happened...');
181
+ console.log(data);
182
+ }
183
+ }
184
+
185
+ // Run callback with retrieved Resource
186
+ callback(this.resource);
187
+ };
188
+ };
189
+
190
+ return Entangled;
191
+ });
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ angular.module('entangledTest')
4
+
5
+ .factory('Message', function(Entangled) {
6
+ var entangled = new Entangled('ws://localhost:3000/messages');
7
+
8
+ var Message = {
9
+ new: function(params) {
10
+ return entangled.new(params);
11
+ },
12
+ all: function(callback) {
13
+ return entangled.all(callback);
14
+ },
15
+ find: function(id, callback) {
16
+ return entangled.find(id, callback);
17
+ }
18
+ };
19
+
20
+ return Message;
21
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "dummy",
3
+ "version": "0.0.0",
4
+ "homepage": "https://github.com/dchacke/entangled",
5
+ "authors": [
6
+ "Dennis Charles Hackethal <dennis.hackethal@gmail.com>"
7
+ ],
8
+ "license": "MIT",
9
+ "private": true,
10
+ "ignore": [
11
+ "**/.*",
12
+ "node_modules",
13
+ "bower_components",
14
+ "test",
15
+ "tests"
16
+ ],
17
+ "dependencies": {
18
+ "angular-route": "~1.3.14",
19
+ "angular": "~1.3.14",
20
+ "angular-mocks": "~1.3.14"
21
+ }
22
+ }
@@ -14,21 +14,12 @@ You can install this package either with `npm` or with `bower`.
14
14
  npm install angular-route
15
15
  ```
16
16
 
17
- Add a `<script>` to your `index.html`:
18
-
19
- ```html
20
- <script src="/node_modules/angular-route/angular-route.js"></script>
21
- ```
22
-
23
17
  Then add `ngRoute` as a dependency for your app:
24
18
 
25
19
  ```javascript
26
- angular.module('myApp', ['ngRoute']);
20
+ angular.module('myApp', [require('angular-route')]);
27
21
  ```
28
22
 
29
- Note that this package is not in CommonJS format, so doing `require('angular-route')` will
30
- return `undefined`.
31
-
32
23
  ### bower
33
24
 
34
25
  ```shell
@@ -56,7 +47,7 @@ Documentation is available on the
56
47
 
57
48
  The MIT License
58
49
 
59
- Copyright (c) 2010-2012 Google, Inc. http://angularjs.org
50
+ Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
60
51
 
61
52
  Permission is hereby granted, free of charge, to any person obtaining a copy
62
53
  of this software and associated documentation files (the "Software"), to deal
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license AngularJS v1.3.13
2
+ * @license AngularJS v1.3.14
3
3
  * (c) 2010-2014 Google, Inc. http://angularjs.org
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*
2
- AngularJS v1.3.13
2
+ AngularJS v1.3.14
3
3
  (c) 2010-2014 Google, Inc. http://angularjs.org
4
4
  License: MIT
5
5
  */
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "angular-route",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
4
4
  "main": "./angular-route.js",
5
5
  "ignore": [],
6
6
  "dependencies": {
7
- "angular": "1.3.13"
7
+ "angular": "1.3.14"
8
8
  }
9
9
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "angular-route",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
4
4
  "description": "AngularJS router module",
5
- "main": "angular-route.js",
5
+ "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },
@@ -20,10 +20,7 @@ Then add a `<script>` to your `index.html`:
20
20
  <script src="/node_modules/angular/angular.js"></script>
21
21
  ```
22
22
 
23
- Note that this package is not in CommonJS format, so doing `require('angular')` will return `undefined`.
24
- If you're using [Browserify](https://github.com/substack/node-browserify), you can use
25
- [exposify](https://github.com/thlorenz/exposify) to have `require('angular')` return the `angular`
26
- global.
23
+ Or `require('angular')` from your code.
27
24
 
28
25
  ### bower
29
26
 
@@ -46,7 +43,7 @@ Documentation is available on the
46
43
 
47
44
  The MIT License
48
45
 
49
- Copyright (c) 2010-2012 Google, Inc. http://angularjs.org
46
+ Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
50
47
 
51
48
  Permission is hereby granted, free of charge, to any person obtaining a copy
52
49
  of this software and associated documentation files (the "Software"), to deal
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license AngularJS v1.3.13
2
+ * @license AngularJS v1.3.14
3
3
  * (c) 2010-2014 Google, Inc. http://angularjs.org
4
4
  * License: MIT
5
5
  */
@@ -54,7 +54,7 @@ function minErr(module, ErrorConstructor) {
54
54
  return match;
55
55
  });
56
56
 
57
- message = message + '\nhttp://errors.angularjs.org/1.3.13/' +
57
+ message = message + '\nhttp://errors.angularjs.org/1.3.14/' +
58
58
  (module ? module + '/' : '') + code;
59
59
  for (i = 2; i < arguments.length; i++) {
60
60
  message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' +
@@ -2121,11 +2121,11 @@ function toDebugString(obj) {
2121
2121
  * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
2122
2122
  */
2123
2123
  var version = {
2124
- full: '1.3.13', // all of these placeholder strings will be replaced by grunt's
2124
+ full: '1.3.14', // all of these placeholder strings will be replaced by grunt's
2125
2125
  major: 1, // package task
2126
2126
  minor: 3,
2127
- dot: 13,
2128
- codeName: 'meticulous-riffleshuffle'
2127
+ dot: 14,
2128
+ codeName: 'instantaneous-browserification'
2129
2129
  };
2130
2130
 
2131
2131
 
@@ -17854,20 +17854,23 @@ var htmlAnchorDirective = valueFn({
17854
17854
  *
17855
17855
  * @description
17856
17856
  *
17857
- * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs:
17857
+ * This directive sets the `disabled` attribute on the element if the
17858
+ * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
17859
+ *
17860
+ * A special directive is necessary because we cannot use interpolation inside the `disabled`
17861
+ * attribute. The following example would make the button enabled on Chrome/Firefox
17862
+ * but not on older IEs:
17863
+ *
17858
17864
  * ```html
17859
- * <div ng-init="scope = { isDisabled: false }">
17860
- * <button disabled="{{scope.isDisabled}}">Disabled</button>
17865
+ * <div ng-init="isDisabled = false">
17866
+ * <button disabled="{{isDisabled}}">Disabled</button>
17861
17867
  * </div>
17862
17868
  * ```
17863
17869
  *
17864
- * The HTML specification does not require browsers to preserve the values of boolean attributes
17865
- * such as disabled. (Their presence means true and their absence means false.)
17870
+ * This is because the HTML specification does not require browsers to preserve the values of
17871
+ * boolean attributes such as `disabled` (Their presence means true and their absence means false.)
17866
17872
  * If we put an Angular interpolation expression into such an attribute then the
17867
17873
  * binding information would be lost when the browser removes the attribute.
17868
- * The `ngDisabled` directive solves this problem for the `disabled` attribute.
17869
- * This complementary directive is not removed by the browser and so provides
17870
- * a permanent reliable place to store the binding information.
17871
17874
  *
17872
17875
  * @example
17873
17876
  <example>
@@ -17886,7 +17889,7 @@ var htmlAnchorDirective = valueFn({
17886
17889
  *
17887
17890
  * @element INPUT
17888
17891
  * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
17889
- * then special attribute "disabled" will be set on the element
17892
+ * then the `disabled` attribute will be set on the element
17890
17893
  */
17891
17894
 
17892
17895
 
@@ -19888,7 +19891,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
19888
19891
  return value;
19889
19892
  });
19890
19893
 
19891
- if (attr.min || attr.ngMin) {
19894
+ if (isDefined(attr.min) || attr.ngMin) {
19892
19895
  var minVal;
19893
19896
  ctrl.$validators.min = function(value) {
19894
19897
  return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
@@ -19904,7 +19907,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
19904
19907
  });
19905
19908
  }
19906
19909
 
19907
- if (attr.max || attr.ngMax) {
19910
+ if (isDefined(attr.max) || attr.ngMax) {
19908
19911
  var maxVal;
19909
19912
  ctrl.$validators.max = function(value) {
19910
19913
  return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
@@ -22710,6 +22713,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
22710
22713
  ngModelGet = parsedNgModel,
22711
22714
  ngModelSet = parsedNgModelAssign,
22712
22715
  pendingDebounce = null,
22716
+ parserValid,
22713
22717
  ctrl = this;
22714
22718
 
22715
22719
  this.$$setOptions = function(options) {
@@ -22982,16 +22986,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
22982
22986
  // the model although neither viewValue nor the model on the scope changed
22983
22987
  var modelValue = ctrl.$$rawModelValue;
22984
22988
 
22985
- // Check if the there's a parse error, so we don't unset it accidentially
22986
- var parserName = ctrl.$$parserName || 'parse';
22987
- var parserValid = ctrl.$error[parserName] ? false : undefined;
22988
-
22989
22989
  var prevValid = ctrl.$valid;
22990
22990
  var prevModelValue = ctrl.$modelValue;
22991
22991
 
22992
22992
  var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
22993
22993
 
22994
- ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) {
22994
+ ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
22995
22995
  // If there was no change in validity, don't update the model
22996
22996
  // This prevents changing an invalid modelValue to undefined
22997
22997
  if (!allowInvalid && prevValid !== allValid) {
@@ -23009,12 +23009,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
23009
23009
 
23010
23010
  };
23011
23011
 
23012
- this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) {
23012
+ this.$$runValidators = function(modelValue, viewValue, doneCallback) {
23013
23013
  currentValidationRunId++;
23014
23014
  var localValidationRunId = currentValidationRunId;
23015
23015
 
23016
23016
  // check parser error
23017
- if (!processParseErrors(parseValid)) {
23017
+ if (!processParseErrors()) {
23018
23018
  validationDone(false);
23019
23019
  return;
23020
23020
  }
@@ -23024,21 +23024,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
23024
23024
  }
23025
23025
  processAsyncValidators();
23026
23026
 
23027
- function processParseErrors(parseValid) {
23027
+ function processParseErrors() {
23028
23028
  var errorKey = ctrl.$$parserName || 'parse';
23029
- if (parseValid === undefined) {
23029
+ if (parserValid === undefined) {
23030
23030
  setValidity(errorKey, null);
23031
23031
  } else {
23032
- setValidity(errorKey, parseValid);
23033
- if (!parseValid) {
23032
+ if (!parserValid) {
23034
23033
  forEach(ctrl.$validators, function(v, name) {
23035
23034
  setValidity(name, null);
23036
23035
  });
23037
23036
  forEach(ctrl.$asyncValidators, function(v, name) {
23038
23037
  setValidity(name, null);
23039
23038
  });
23040
- return false;
23041
23039
  }
23040
+ // Set the parse error last, to prevent unsetting it, should a $validators key == parserName
23041
+ setValidity(errorKey, parserValid);
23042
+ return parserValid;
23042
23043
  }
23043
23044
  return true;
23044
23045
  }
@@ -23133,7 +23134,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
23133
23134
  this.$$parseAndValidate = function() {
23134
23135
  var viewValue = ctrl.$$lastCommittedViewValue;
23135
23136
  var modelValue = viewValue;
23136
- var parserValid = isUndefined(modelValue) ? undefined : true;
23137
+ parserValid = isUndefined(modelValue) ? undefined : true;
23137
23138
 
23138
23139
  if (parserValid) {
23139
23140
  for (var i = 0; i < ctrl.$parsers.length; i++) {
@@ -23159,7 +23160,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
23159
23160
 
23160
23161
  // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
23161
23162
  // This can happen if e.g. $setViewValue is called from inside a parser
23162
- ctrl.$$runValidators(parserValid, modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
23163
+ ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
23163
23164
  if (!allowInvalid) {
23164
23165
  // Note: Don't check ctrl.$valid here, as we could have
23165
23166
  // external validators (e.g. calculated on the server),
@@ -23280,6 +23281,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
23280
23281
  // TODO(perf): why not move this to the action fn?
23281
23282
  if (modelValue !== ctrl.$modelValue) {
23282
23283
  ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
23284
+ parserValid = undefined;
23283
23285
 
23284
23286
  var formatters = ctrl.$formatters,
23285
23287
  idx = formatters.length;
@@ -23292,7 +23294,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
23292
23294
  ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
23293
23295
  ctrl.$render();
23294
23296
 
23295
- ctrl.$$runValidators(undefined, modelValue, viewValue, noop);
23297
+ ctrl.$$runValidators(modelValue, viewValue, noop);
23296
23298
  }
23297
23299
  }
23298
23300
 
@@ -24108,6 +24110,55 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
24108
24110
  * when keys are deleted and reinstated.
24109
24111
  *
24110
24112
  *
24113
+ * # Tracking and Duplicates
24114
+ *
24115
+ * When the contents of the collection change, `ngRepeat` makes the corresponding changes to the DOM:
24116
+ *
24117
+ * * When an item is added, a new instance of the template is added to the DOM.
24118
+ * * When an item is removed, its template instance is removed from the DOM.
24119
+ * * When items are reordered, their respective templates are reordered in the DOM.
24120
+ *
24121
+ * By default, `ngRepeat` does not allow duplicate items in arrays. This is because when
24122
+ * there are duplicates, it is not possible to maintain a one-to-one mapping between collection
24123
+ * items and DOM elements.
24124
+ *
24125
+ * If you do need to repeat duplicate items, you can substitute the default tracking behavior
24126
+ * with your own using the `track by` expression.
24127
+ *
24128
+ * For example, you may track items by the index of each item in the collection, using the
24129
+ * special scope property `$index`:
24130
+ * ```html
24131
+ * <div ng-repeat="n in [42, 42, 43, 43] track by $index">
24132
+ * {{n}}
24133
+ * </div>
24134
+ * ```
24135
+ *
24136
+ * You may use arbitrary expressions in `track by`, including references to custom functions
24137
+ * on the scope:
24138
+ * ```html
24139
+ * <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
24140
+ * {{n}}
24141
+ * </div>
24142
+ * ```
24143
+ *
24144
+ * If you are working with objects that have an identifier property, you can track
24145
+ * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
24146
+ * will not have to rebuild the DOM elements for items it has already rendered, even if the
24147
+ * JavaScript objects in the collection have been substituted for new ones:
24148
+ * ```html
24149
+ * <div ng-repeat="model in collection track by model.id">
24150
+ * {{model.name}}
24151
+ * </div>
24152
+ * ```
24153
+ *
24154
+ * When no `track by` expression is provided, it is equivalent to tracking by the built-in
24155
+ * `$id` function, which tracks items by their identity:
24156
+ * ```html
24157
+ * <div ng-repeat="obj in collection track by $id(obj)">
24158
+ * {{obj.prop}}
24159
+ * </div>
24160
+ * ```
24161
+ *
24111
24162
  * # Special repeat start and end points
24112
24163
  * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
24113
24164
  * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
@@ -24175,12 +24226,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
24175
24226
  *
24176
24227
  * For example: `(name, age) in {'adam':10, 'amalie':12}`.
24177
24228
  *
24178
- * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
24179
- * which can be used to associate the objects in the collection with the DOM elements. If no tracking function
24180
- * is specified the ng-repeat associates elements by identity in the collection. It is an error to have
24181
- * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are
24182
- * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression,
24183
- * before specifying a tracking expression.
24229
+ * * `variable in expression track by tracking_expression` – You can also provide an optional tracking expression
24230
+ * which can be used to associate the objects in the collection with the DOM elements. If no tracking expression
24231
+ * is specified, ng-repeat associates elements by identity. It is an error to have
24232
+ * more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
24233
+ * mapped to the same DOM element, which is not possible.) If filters are used in the expression, they should be
24234
+ * applied before the tracking expression.
24184
24235
  *
24185
24236
  * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements
24186
24237
  * will be associated by item identity in the array.