Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60689afe1c | |||
| 881fddaaba | |||
| fda338e8e2 | |||
| 05c8d88126 | |||
| aa57f57ccb | |||
| 51b78f61f9 | |||
| 739100e8a8 | |||
| 20e0c7f966 | |||
| f34e3413c7 | |||
| 4d28598b3d | |||
| 16d6e89f3b | |||
| 6e220ab612 | |||
| ce14e4a6b7 | |||
| b5dbc9834a | |||
| 57f10251db | |||
| 48cd05a1ad | |||
| 6a71ba17f0 | |||
| cf1b175508 | |||
| 5bba68c264 | |||
| e5a4345aac | |||
| 49ef92041f | |||
| 081ae8bfca | |||
| 4f8aa6f5bb | |||
| d7e888b8c6 | |||
| 491b4af92e | |||
| 5930b8ee27 | |||
| 69d9072d5b | |||
| 9b5de0ac97 | |||
| 21b76aed06 | |||
| 363fb16c10 | |||
| e00f9f62e3 | |||
| 507c666d16 | |||
| 743b33ab9d | |||
| 9f8fa79f25 | |||
| fe3c1d44a5 | |||
| 5ae72dc7af | |||
| 47bc98a1ea | |||
| 10920a2857 | |||
| 90a9270dfa | |||
| d4b05dd13a | |||
| 59802bc790 | |||
| b0a35a794a | |||
| a9545e3963 | |||
| 269c15c74b | |||
| 9f48025733 | |||
| 1d0ccd0d73 | |||
| 03e5164528 | |||
| 3047964712 | |||
| 225afb9eb4 | |||
| b377d6b043 | |||
| ac650cb2e6 | |||
| 926088ba77 | |||
| f94cf634d8 | |||
| 85b3786f55 | |||
| 1116a96df6 | |||
| 1c1c9b27cb | |||
| c5e7a5bc9b | |||
| 7550b7bda8 | |||
| 43b97b211b | |||
| 76405897fc | |||
| bb730704f2 | |||
| 8965d57aa0 | |||
| 951a2743d5 | |||
| a2a907e439 | |||
| 308f22ba9a | |||
| 32e2643d90 | |||
| 3edaf0922f | |||
| 1095596962 | |||
| ee0dac4d1d | |||
| 6f1fae8a91 | |||
| f96485085a | |||
| 147650b3be | |||
| 10c426ffa3 | |||
| 785647eb9d | |||
| a2a6279157 | |||
| b50d2a4a1f | |||
| f539fb1f7c | |||
| 2dcb5acb7b | |||
| 905a8fd4a4 | |||
| df9b57a481 | |||
| fee2077d0b | |||
| 691aafc981 | |||
| da7af937d6 | |||
| 1c9b9e24ed | |||
| 46a5b2c298 | |||
| 28e4707dc4 | |||
| 420b5211da | |||
| 04bccee099 | |||
| a404839795 | |||
| 25d4e5cca4 | |||
| c67112563f | |||
| 2b2ec26a75 | |||
| 41f90e5fb0 | |||
| a85c591d36 |
@@ -19,6 +19,7 @@ env:
|
||||
- JOB=unit BROWSER_PROVIDER=saucelabs
|
||||
- JOB=docs-e2e BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jquery-2.1 BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs
|
||||
global:
|
||||
- CXX=g++-4.8 # node 4 likes the G++ v4.8 compiler
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
<a name="1.5.4"></a>
|
||||
# 1.5.4 graduated-sophistry (2016-04-14)
|
||||
<a name="1.5.5"></a>
|
||||
# 1.5.5 material-conspiration (2016-04-18)
|
||||
|
||||
|
||||
## Reverts
|
||||
|
||||
- **$compile:** move setting of controller data to single location
|
||||
Reverted commit [21d148ae](https://github.com/angular/angular.js/commit/21d148aedc29c7efba4131ff2ef6383b4700868c)
|
||||
since it caused the Angular Material tabs directive to fail.
|
||||
|
||||
- **ngRoute:** allow `ngView` to be included in an asynchronously loaded template
|
||||
Eagerly loading `$route`, could break tests, because it might request the root or default route
|
||||
template (something `$httpBackend` would know nothing about).
|
||||
|
||||
It will be re-applied for `v1.6.x`, with a breaking change notice and possibly a way to disable
|
||||
the feature in tests.
|
||||
|
||||
([8237482d](https://github.com/angular/angular.js/commit/8237482d49e76e2c4994fe6207e3c9799ef04163),
|
||||
[#1213](https://github.com/angular/angular.js/issues/1213), [#6812](https://github.com/angular/angular.js/issues/6812),
|
||||
[#14088](https://github.com/angular/angular.js/issues/14088))
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
@@ -40,6 +58,9 @@
|
||||
- **ngOptions:** set select value when model matches disabled option
|
||||
([832eba5f](https://github.com/angular/angular.js/commit/832eba5fc952312e6b99127123e6e75bdf729006),
|
||||
[#12756](https://github.com/angular/angular.js/issues/12756))
|
||||
- **$http:** pass event object to `eventHandlers`/`uploadEventHandlers`
|
||||
([25d4e5cc](https://github.com/angular/angular.js/commit/25d4e5cca4fa615e49d65976223c6deb5b485b4c),
|
||||
[#14436](https://github.com/angular/angular.js/issues/14436))
|
||||
|
||||
|
||||
## Features
|
||||
@@ -80,6 +101,10 @@
|
||||
[#13607](https://github.com/angular/angular.js/issues/13607), [#13239](https://github.com/angular/angular.js/issues/13239), [#12076](https://github.com/angular/angular.js/issues/12076))
|
||||
|
||||
|
||||
<a name="1.5.4"></a>
|
||||
# 1.5.4 graduated-sophistry (2016-04-14)
|
||||
|
||||
This was a partially published release that you should ignore.
|
||||
|
||||
<a name="1.5.3"></a>
|
||||
# 1.5.3 diplohaplontic-meiosis (2016-03-25)
|
||||
@@ -983,9 +1008,6 @@ is `$locals`.
|
||||
[#13236](https://github.com/angular/angular.js/issues/13236))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.5.0-beta.2"></a>
|
||||
# 1.5.0-beta.2 effective-delegation (2015-11-17)
|
||||
|
||||
@@ -1305,9 +1327,6 @@ requirement more strict and alerts the developer explicitly.
|
||||
[#2318](https://github.com/angular/angular.js/issues/2318), [#9319](https://github.com/angular/angular.js/issues/9319), [#12159](https://github.com/angular/angular.js/issues/12159))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.3.20"></a>
|
||||
# 1.3.20 shallow-translucence (2015-09-29)
|
||||
|
||||
@@ -1318,9 +1337,6 @@ requirement more strict and alerts the developer explicitly.
|
||||
([d434f3db](https://github.com/angular/angular.js/commit/d434f3db53d6209eb140b904e83bbde401686c16))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.2.29"></a>
|
||||
# 1.2.29 ultimate-deprecation (2015-09-29)
|
||||
|
||||
@@ -1347,9 +1363,6 @@ requirement more strict and alerts the developer explicitly.
|
||||
[#9936](https://github.com/angular/angular.js/issues/9936))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.4.6"></a>
|
||||
# 1.4.6 multiplicative-elevation (2015-09-17)
|
||||
|
||||
@@ -2178,9 +2191,7 @@ describe('$q.when', function() {
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### ngAnimate
|
||||
|
||||
- **$animateCss:** due to [d5683d21](https://github.com/angular/angular.js/commit/d5683d21165e725bc5a850e795f681b0a8a008f5),
|
||||
- **ngAnimate** - $animateCss: due to [d5683d21](https://github.com/angular/angular.js/commit/d5683d21165e725bc5a850e795f681b0a8a008f5),
|
||||
The $animateCss service will now always return an
|
||||
object even if the animation is not set to run. If your code is using
|
||||
$animateCss then please consider the following code change:
|
||||
@@ -2264,9 +2275,7 @@ $animateProvider.classNameFilter(/ng-animate-special/);
|
||||
```
|
||||
|
||||
|
||||
### ngOptions
|
||||
|
||||
- ** due to [dfa722a8](https://github.com/angular/angular.js/commit/dfa722a8a6864793fd9580d8ae704a06d10b5509),
|
||||
- **ngOptions**: due to [dfa722a8](https://github.com/angular/angular.js/commit/dfa722a8a6864793fd9580d8ae704a06d10b5509),
|
||||
|
||||
|
||||
Although it is unlikely that anyone is using it in this way, this change does change the
|
||||
|
||||
@@ -280,7 +280,7 @@ You can find out more detailed information about contributing in the
|
||||
[groups]: https://groups.google.com/forum/?fromgroups#!forum/angular
|
||||
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
|
||||
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
|
||||
[js-style-guide]: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
|
||||
[js-style-guide]: https://google.github.io/styleguide/javascriptguide.xml
|
||||
[jsfiddle]: http://jsfiddle.net/
|
||||
[list]: https://groups.google.com/forum/?fromgroups#!forum/angular
|
||||
[ngDocs]: https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation
|
||||
|
||||
@@ -79,6 +79,7 @@ module.exports = function(grunt) {
|
||||
tests: {
|
||||
jqlite: 'karma-jqlite.conf.js',
|
||||
jquery: 'karma-jquery.conf.js',
|
||||
'jquery-2.1': 'karma-jquery-2.1.conf.js',
|
||||
docs: 'karma-docs.conf.js',
|
||||
modules: 'karma-modules.conf.js'
|
||||
},
|
||||
@@ -87,6 +88,7 @@ module.exports = function(grunt) {
|
||||
autotest: {
|
||||
jqlite: 'karma-jqlite.conf.js',
|
||||
jquery: 'karma-jquery.conf.js',
|
||||
'jquery-2.1': 'karma-jquery-2.1.conf.js',
|
||||
modules: 'karma-modules.conf.js',
|
||||
docs: 'karma-docs.conf.js'
|
||||
},
|
||||
@@ -357,10 +359,11 @@ module.exports = function(grunt) {
|
||||
//alias tasks
|
||||
grunt.registerTask('test', 'Run unit, docs and e2e tests with Karma', ['jshint', 'jscs', 'package', 'test:unit', 'test:promises-aplus', 'tests:docs', 'test:protractor']);
|
||||
grunt.registerTask('test:jqlite', 'Run the unit tests with Karma' , ['tests:jqlite']);
|
||||
grunt.registerTask('test:jquery', 'Run the jQuery unit tests with Karma', ['tests:jquery']);
|
||||
grunt.registerTask('test:jquery', 'Run the jQuery (latest) unit tests with Karma', ['tests:jquery']);
|
||||
grunt.registerTask('test:jquery-2.1', 'Run the jQuery 2.1 unit tests with Karma', ['tests:jquery-2.1']);
|
||||
grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', ['build', 'tests:modules']);
|
||||
grunt.registerTask('test:docs', 'Run the doc-page tests with Karma', ['package', 'tests:docs']);
|
||||
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', ['test:jqlite', 'test:jquery', 'test:modules']);
|
||||
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', ['test:jqlite', 'test:jquery', 'test:jquery-2.1', 'test:modules']);
|
||||
grunt.registerTask('test:protractor', 'Run the end to end tests with Protractor and keep a test server running in the background', ['webdriver', 'connect:testserver', 'protractor:normal']);
|
||||
grunt.registerTask('test:travis-protractor', 'Run the end to end tests with Protractor for Travis CI builds', ['connect:testserver', 'protractor:travis']);
|
||||
grunt.registerTask('test:ci-protractor', 'Run the end to end tests with Protractor for Jenkins CI builds', ['webdriver', 'connect:testserver', 'protractor:jenkins']);
|
||||
|
||||
@@ -19,6 +19,7 @@ piece of cake. Best of all? It makes development fun!
|
||||
* Contribution guidelines: [CONTRIBUTING.md](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md)
|
||||
* Dashboard: https://dashboard.angularjs.org
|
||||
|
||||
##### Looking for Angular 2? Go here: https://github.com/angular/angular
|
||||
|
||||
Building AngularJS
|
||||
---------
|
||||
|
||||
@@ -231,6 +231,15 @@ var angularFiles = {
|
||||
'@angularTest'
|
||||
],
|
||||
|
||||
'karmaJqueryOld': [
|
||||
'bower_components/jquery-2.1/dist/jquery.js',
|
||||
'test/jquery_alias.js',
|
||||
'@angularSrc',
|
||||
'@angularSrcModules',
|
||||
'@angularScenario',
|
||||
'@angularTest'
|
||||
],
|
||||
|
||||
'karmaJqueryExclude': [
|
||||
'src/angular-bootstrap.js',
|
||||
'src/ngScenario/angular-bootstrap.js',
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"name": "AngularJS",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"jquery": "2.1.1",
|
||||
"jquery": "2.2.3",
|
||||
"jquery-2.1": "jquery#2.1.4",
|
||||
"closure-compiler": "https://dl.google.com/closure-compiler/compiler-20140814.zip",
|
||||
"ng-closure-runner": "https://raw.github.com/angular/ng-closure-runner/v0.2.3/assets/ng-closure-runner.zip"
|
||||
}
|
||||
|
||||
@@ -647,6 +647,11 @@ ul.events > li {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.diagram {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) and (max-width: 991px) {
|
||||
.main-body-grid {
|
||||
margin-top: 160px;
|
||||
|
||||
@@ -153,7 +153,7 @@ angular.module('examples', [])
|
||||
|
||||
postData.description = ctrl.example.name;
|
||||
|
||||
formPostData('http://plnkr.co/edit/?p=preview', newWindow, postData);
|
||||
formPostData('https://plnkr.co/edit/?p=preview', newWindow, postData);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ angular.module('tutorials', [])
|
||||
'',
|
||||
'step_00', 'step_01', 'step_02', 'step_03', 'step_04',
|
||||
'step_05', 'step_06', 'step_07', 'step_08', 'step_09',
|
||||
'step_10', 'step_11', 'step_12', 'the_end'
|
||||
'step_10', 'step_11', 'step_12', 'step_13', 'step_14',
|
||||
'the_end'
|
||||
];
|
||||
return {
|
||||
scope: {},
|
||||
@@ -43,7 +44,7 @@ angular.module('tutorials', [])
|
||||
'<a href="http://angular.github.io/angular-phonecat/step-{{step}}/app">Step {{step}} Live Demo</a>.</p>\n' +
|
||||
'</div>\n' +
|
||||
'<p>The most important changes are listed below. You can see the full diff on ' +
|
||||
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>\n' +
|
||||
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>.\n' +
|
||||
'</p>'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "AngularJS-docs-app",
|
||||
"dependencies": {
|
||||
"jquery": "2.1.1",
|
||||
"jquery": "2.2.3",
|
||||
"lunr.js": "0.5.12",
|
||||
"open-sans-fontface": "1.0.4",
|
||||
"google-code-prettify": "1.0.1",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "base.template.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<a href='https://github.com/{$ git.info.owner $}/{$ git.info.repo $}/tree/{$ git.version.isSnapshot and 'master' or git.version.raw $}/{$ doc.fileInfo.projectRelativePath $}#L{$ doc.startingLine $}' class='view-source pull-right btn btn-primary'>
|
||||
<i class="glyphicon glyphicon-zoom-in"> </i>View Source
|
||||
</a>
|
||||
|
||||
{% block header %}
|
||||
<header class="api-profile-header">
|
||||
<h1 class="api-profile-header-heading">{$ doc.name $}</h1>
|
||||
<ol class="api-profile-header-structure naked-list step-list">
|
||||
{% block related_components %}{% endblock %}
|
||||
<li>
|
||||
- {$ doc.docType $} in module <a href="{$ doc.moduleDoc.path $}">{$ doc.moduleDoc.name $}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
<div class="api-profile-description">
|
||||
{$ doc.description | marked $}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% if doc.knownIssues %}
|
||||
<h2 id="known-issues">Known Issues</h2>
|
||||
{% for issue in doc.knownIssues -%}
|
||||
<div class="known-issue">
|
||||
{$ issue | marked $} {% if not loop.last %}<hr>{% endif %}
|
||||
</div>
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
|
||||
{% if doc.deprecated %}
|
||||
<fieldset class="deprecated">
|
||||
<legend>Deprecated API</legend>
|
||||
{$ doc.deprecated| marked $}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{% block dependencies %}
|
||||
{%- if doc.requires %}
|
||||
<h2 id="dependencies">Dependencies</h2>
|
||||
<ul>
|
||||
{% for require in doc.requires %}<li>{$ require | link $}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional %}
|
||||
{% endblock %}
|
||||
|
||||
{% block examples %}
|
||||
{%- if doc.examples %}
|
||||
<h2 id="example">Example</h2>
|
||||
{%- for example in doc.examples -%}
|
||||
{$ example | marked $}
|
||||
{%- endfor -%}
|
||||
{% endif -%}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -5,35 +5,41 @@
|
||||
{% if doc.title %}{$ doc.title | marked $}{% else %}{$ doc.name | code $}{% endif %}
|
||||
</h1>
|
||||
|
||||
<h2>Installation</h2>
|
||||
{% if doc.installation or doc.installation == '' %}
|
||||
{$ doc.installation | marked $}
|
||||
{% else %}
|
||||
<h2>Installation</h2>
|
||||
|
||||
<p>First include {$ doc.packageFile | code $} in your HTML:</p>
|
||||
|
||||
{% code %}
|
||||
<script src="angular.js">
|
||||
<script src="{$ doc.packageFile $}">
|
||||
{% endcode %}
|
||||
|
||||
<p>You can download this file from the following places:</p>
|
||||
<p>First, get the file:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://developers.google.com/speed/libraries/devguide#angularjs">Google CDN</a><br>
|
||||
e.g. {$ ("//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/" + doc.packageFile) | code $}
|
||||
<a href="https://developers.google.com/speed/libraries/devguide#angularjs">Google CDN</a> e.g.
|
||||
{% code %}"//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://bower.io">Bower</a><br>
|
||||
e.g. {% code %}bower install {$ doc.packageName $}@X.Y.Z{% endcode %}
|
||||
<a href="https://www.npmjs.com/">NPM</a> e.g.
|
||||
{% code %}npm install {$ doc.packageName $}@X.Y.Z{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://code.angularjs.org/">code.angularjs.org</a><br>
|
||||
e.g. {% code %}"//code.angularjs.org/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
|
||||
<a href="http://bower.io">Bower</a> e.g.
|
||||
{% code %}bower install {$ doc.packageName $}@X.Y.Z{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://code.angularjs.org/">code.angularjs.org</a>
|
||||
(discouraged for production use) e.g.
|
||||
{% code %}"//code.angularjs.org/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
|
||||
</li>
|
||||
</ul>
|
||||
<p>where X.Y.Z is the AngularJS version you are running.</p>
|
||||
<p>Then load the module in your application by adding it as a dependent module:</p>
|
||||
|
||||
<p>Then, include {$ doc.packageFile | code $} in your HTML:</p>
|
||||
|
||||
{% code %}
|
||||
<script src="path/to/angular.js"></script>
|
||||
<script src="path/to/{$ doc.packageFile $}"></script>
|
||||
{% endcode %}
|
||||
|
||||
<p>Finally, load the module in your application by adding it as a dependent module:</p>
|
||||
{% code %}
|
||||
angular.module('app', ['{$ doc.name $}']);
|
||||
{% endcode %}
|
||||
@@ -53,7 +59,7 @@
|
||||
<td>{$ issueDoc.id | link(issueDoc.name, issueDoc) $}</td>
|
||||
<td>
|
||||
{% for issue in issueDoc.knownIssues -%}
|
||||
{$ issue | marked $}
|
||||
{$ issue | marked $} {% if not loop.last %}<hr>{% endif %}
|
||||
{% endfor -%}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -228,10 +228,10 @@
|
||||
)
|
||||
</p>
|
||||
<p>
|
||||
Code licensed under the
|
||||
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The
|
||||
MIT License</a>. Documentation licensed under <a
|
||||
href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.
|
||||
Code licensed under
|
||||
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The MIT License</a>.
|
||||
Documentation licensed under
|
||||
<a href="http://creativecommons.org/licenses/by/3.0/" target="_blank">CC BY 3.0</a>.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
@ngdoc error
|
||||
@name $compile:reqslot
|
||||
@fullName Required transclusion slot
|
||||
@description
|
||||
|
||||
This error occurs when a directive or component try to transclude a slot that is not provided.
|
||||
|
||||
Transcluded elements must contain something. This error could happen when you try to transclude a self closing tag element.
|
||||
Also you can make a transclusion slot optional with a `?` prefix.
|
||||
|
||||
```js
|
||||
// In this example the <my-component> must have an <important-component> inside to transclude it.
|
||||
// If not, a reqslot error will be generated.
|
||||
|
||||
var componentConfig = {
|
||||
template: 'path/to/template.html',
|
||||
tranclude: {
|
||||
importantSlot: 'importantComponent', // mandatory transclusion
|
||||
optionalSlot: '?optionalComponent', // optional transclusion
|
||||
}
|
||||
};
|
||||
|
||||
angular
|
||||
.module('doc')
|
||||
.component('myComponent', componentConfig)
|
||||
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Will not work because <important-component> is missing -->
|
||||
<my-component>
|
||||
</my-component>
|
||||
|
||||
<my-component>
|
||||
<optional-component></optional-component>
|
||||
</my-component>
|
||||
|
||||
<!-- Will work -->
|
||||
<my-component>
|
||||
<important-component></important-component>
|
||||
</my-component>
|
||||
|
||||
<my-component>
|
||||
<optional-component></optional-component>
|
||||
<important-component></important-component>
|
||||
</my-component>
|
||||
```
|
||||
@@ -350,7 +350,7 @@ base and the path that should be handled by the application.
|
||||
### Base href constraints
|
||||
|
||||
The `$location` service is not able to function properly if the current URL is outside the URL given
|
||||
as the base href. This can have subtle confusing consequencies...
|
||||
as the base href. This can have subtle confusing consequences...
|
||||
|
||||
Consider a base href set as follows: `<base href="/base/">` (i.e. the application exists in the "folder"
|
||||
called `/base`). The URL `/base` is actually outside the application (it refers to the `base` file found
|
||||
|
||||
@@ -12,6 +12,13 @@ triggered, will attempt to perform a CSS Transition, CSS Keyframe Animation or a
|
||||
placed on the given directive). Animations can be placed using vanilla CSS by following the naming conventions set in place by AngularJS
|
||||
or with JavaScript code when it's defined as a factory.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note that we have used non-prefixed CSS transition properties in our examples as the major browsers now support non-prefixed
|
||||
properties. If you intend to support older browsers or certain mobile browsers then you will need to include prefixed
|
||||
versions of the transition properties. Take a look at http://caniuse.com/#feat=css-transitions for what browsers require prefixes,
|
||||
and https://github.com/postcss/autoprefixer for a tool that can automatically generate the prefixes for you.
|
||||
</div>
|
||||
|
||||
Animations are not available unless you include the {@link ngAnimate `ngAnimate` module} as a dependency within your application.
|
||||
|
||||
Below is a quick example of animations being enabled for `ngShow` and `ngHide`:
|
||||
@@ -29,18 +36,17 @@ Below is a quick example of animations being enabled for `ngShow` and `ngHide`:
|
||||
</file>
|
||||
<file name="animations.css">
|
||||
.sample-show-hide {
|
||||
padding:10px;
|
||||
border:1px solid black;
|
||||
background:white;
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.sample-show-hide {
|
||||
-webkit-transition:all linear 0.5s;
|
||||
transition:all linear 0.5s;
|
||||
transition: all linear 0.5s;
|
||||
}
|
||||
|
||||
.sample-show-hide.ng-hide {
|
||||
opacity:0;
|
||||
opacity: 0;
|
||||
}
|
||||
</file>
|
||||
</example>
|
||||
@@ -80,11 +86,8 @@ occur when ngRepeat triggers them:
|
||||
class
|
||||
*/
|
||||
.repeated-item.ng-enter, .repeated-item.ng-move {
|
||||
-webkit-transition:0.5s linear all;
|
||||
-moz-transition:0.5s linear all;
|
||||
-o-transition:0.5s linear all;
|
||||
transition:0.5s linear all;
|
||||
opacity:0;
|
||||
transition: all 0.5s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -95,7 +98,7 @@ occur when ngRepeat triggers them:
|
||||
*/
|
||||
.repeated-item.ng-enter.ng-enter-active,
|
||||
.repeated-item.ng-move.ng-move-active {
|
||||
opacity:1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -104,10 +107,7 @@ occur when ngRepeat triggers them:
|
||||
that has the .repeated-item class
|
||||
*/
|
||||
.repeated-item.ng-leave {
|
||||
-webkit-animation:0.5s my_animation;
|
||||
-moz-animation:0.5s my_animation;
|
||||
-o-animation:0.5s my_animation;
|
||||
animation:0.5s my_animation;
|
||||
animation: 0.5s my_animation;
|
||||
}
|
||||
|
||||
@keyframes my_animation {
|
||||
@@ -115,24 +115,6 @@ occur when ngRepeat triggers them:
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
/*
|
||||
Unfortunately each browser vendor requires
|
||||
its own definition of keyframe animation code...
|
||||
*/
|
||||
@-webkit-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
@-moz-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
@-o-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
```
|
||||
|
||||
The same approach to animation can be used using JavaScript code (**jQuery is used within to perform animations**):
|
||||
@@ -217,10 +199,7 @@ The example below shows how to perform animations during class changes:
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.css-class-add, .css-class-remove {
|
||||
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
}
|
||||
|
||||
.css-class,
|
||||
@@ -231,7 +210,7 @@ The example below shows how to perform animations during class changes:
|
||||
|
||||
.css-class-remove.css-class-remove-active {
|
||||
font-size:1.0em;
|
||||
color:black;
|
||||
color: black;
|
||||
}
|
||||
</file>
|
||||
</example>
|
||||
@@ -317,8 +296,8 @@ app.config(function($animateProvider) {
|
||||
```css
|
||||
/* prefixed with animate- */
|
||||
.animate-fade-add.animate-fade-add-active {
|
||||
transition:1s linear all;
|
||||
opacity:0;
|
||||
transition: all 1s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -467,8 +467,8 @@ AngularJS itself via npm:
|
||||
|
||||
```bash
|
||||
npm init
|
||||
npm install@1.5.x angular --save
|
||||
npm install @angular/router --save
|
||||
npm install angular@1.5.x --save
|
||||
npm install @angular/router@0.2.0 --save
|
||||
```
|
||||
|
||||
|
||||
@@ -658,7 +658,7 @@ because the `HeroList` and `HeroDetail` will not contain any child routes.
|
||||
|
||||
The `component` property in a **Route Definition** defines the **Component** directive that will be rendered
|
||||
into the DOM via the **Outlet**. For example the `heroDetail` **Component** will be rendered into the page
|
||||
where the `<ng-outlet></ng-outlet>` lives as `<hero-detail></hero-detail>.
|
||||
where the `<ng-outlet></ng-outlet>` lives as `<hero-detail></hero-detail>`.
|
||||
|
||||
The `name` property is used to reference the **Route Definition** when generating URLs or navigating to
|
||||
**Routes**. For example this link will `<a ng-link="['Heroes']">Heroes</a>` navigate the **Route Definition**
|
||||
@@ -854,7 +854,6 @@ Router itself, which was made available by the binding in the **Component Defini
|
||||
function HeroDetailComponent(heroService) {
|
||||
...
|
||||
this.gotoHeroes = function() {
|
||||
var heroId = this.hero && this.hero.id;
|
||||
this.$router.navigate(['HeroList']);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -133,7 +133,7 @@ components should follow a few simple conventions:
|
||||
For a deletion, that means the component doesn't delete the `hero` itself, but sends it back to
|
||||
the owner component via the correct event.
|
||||
```html
|
||||
<!-- note that we use snake case for bindings in the template as usual -->
|
||||
<!-- note that we use kebab-case for bindings in the template as usual -->
|
||||
<editable-field on-update="$ctrl.update('location', value)"></editable-field><br>
|
||||
<button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
|
||||
```
|
||||
@@ -233,7 +233,7 @@ it upwards to the heroList component, which updates the original data.
|
||||
</file>
|
||||
|
||||
<file name="heroDetail.js">
|
||||
function HeroDetailController($scope, $element, $attrs) {
|
||||
function HeroDetailController() {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.update = function(prop, value) {
|
||||
@@ -455,7 +455,7 @@ The examples use the [Jasmine](http://jasmine.github.io/) testing framework.
|
||||
describe('component: heroDetail', function() {
|
||||
var component, scope, hero, $componentController;
|
||||
|
||||
beforeEach(module('simpleComponent'));
|
||||
beforeEach(module('heroApp'));
|
||||
|
||||
beforeEach(inject(function($rootScope, _$componentController_) {
|
||||
scope = $rootScope.$new();
|
||||
@@ -463,34 +463,24 @@ describe('component: heroDetail', function() {
|
||||
hero = {name: 'Wolverine'};
|
||||
}));
|
||||
|
||||
it('should set the default values of the hero', function() {
|
||||
// It's necessary to always pass the scope in the locals, so that the controller instance can be bound to it
|
||||
component = $componentController('heroDetail', {$scope: scope});
|
||||
|
||||
expect(component.hero).toEqual({
|
||||
name: undefined,
|
||||
location: 'unknown'
|
||||
});
|
||||
});
|
||||
|
||||
it('should assign the name bindings to the hero object', function() {
|
||||
// Here we are passing actual bindings to the component
|
||||
|
||||
component = $componentController('heroDetail',
|
||||
{$scope: scope},
|
||||
null,
|
||||
{hero: hero}
|
||||
);
|
||||
expect(component.hero.name).toBe('Wolverine');
|
||||
});
|
||||
|
||||
it('should call the onDelete binding when a hero is deleted', function() {
|
||||
var deleteSpy = jasmine.createSpy('deleteSpy');
|
||||
component = $componentController('heroDetail',
|
||||
{$scope: scope},
|
||||
{hero: hero, onDelete: jasmine.createSpy('deleteSpy')}
|
||||
null,
|
||||
{hero: hero, onDelete: deleteSpy}
|
||||
);
|
||||
|
||||
component.onDelete({hero: component.hero});
|
||||
expect(spy('deleteSpy')).toHaveBeenCalledWith(component.hero);
|
||||
expect(deleteSpy).toHaveBeenCalledWith({hero: component.hero});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -277,8 +277,8 @@ Now that Angular knows of all the parts of the application, it needs to create t
|
||||
In the previous section we saw that controllers are created using a factory function.
|
||||
For services there are multiple ways to define their factory
|
||||
(see the {@link services service guide}).
|
||||
In the example above, we are using a function that returns the `currencyConverter` function as the factory
|
||||
for the service.
|
||||
In the example above, we are using an anonymous function as the factory function for `currencyConverter` service.
|
||||
This function should return the `currencyConverter` service instance.
|
||||
|
||||
Back to the initial question: How does the `InvoiceController` get a reference to the `currencyConverter` function?
|
||||
In Angular, this is done by simply defining arguments on the constructor function. With this, the injector
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
@ngdoc overview
|
||||
@name Decorators
|
||||
@sortOrder 345
|
||||
@description
|
||||
|
||||
# Decorators in AngularJS
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** This guide is targeted towards developers who are already familiar with AngularJS basics.
|
||||
If you're just getting started, we recommend the {@link tutorial/ tutorial} first.
|
||||
</div>
|
||||
|
||||
## What are decorators?
|
||||
|
||||
Decorators are a design pattern that is used to separate modification or *decoration* of a class without modifying the
|
||||
original source code. In Angular, decorators are functions that allow a service, directive or filter to be modified
|
||||
prior to its usage.
|
||||
|
||||
## How to use decorators
|
||||
|
||||
There are two ways to register decorators
|
||||
|
||||
- `$provide.decorator`, and
|
||||
- `module.decorator`
|
||||
|
||||
Each provide access to a `$delegate`, which is the instantiated service/directive/filter, prior to being passed to the
|
||||
service that required it.
|
||||
|
||||
### $provide.decorator
|
||||
|
||||
The {@link api/auto/service/$provide#decorator decorator function} allows access to a $delegate of the service once it
|
||||
has been instantiated. For example:
|
||||
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function $logDecorator($delegate) {
|
||||
|
||||
var originalWarn = $delegate.warn;
|
||||
$delegate.warn = function decoratedWarn(msg) {
|
||||
msg = 'Decorated Warn: ' + msg;
|
||||
originalWarn.apply($delegate, arguments);
|
||||
};
|
||||
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
After the `$log` service has been instantiated the decorator is fired. The decorator function has a `$delegate` object
|
||||
injected to provide access to the service that matches the selector in the decorator. This `$delegate` will be the
|
||||
service you are decorating. The return value of the function *provided to the decorator* will take place of the service,
|
||||
directive, or filter being decorated.
|
||||
|
||||
<hr>
|
||||
|
||||
The `$delegate` may be either modified or completely replaced. Given a service `myService` with a method `someFn`, the
|
||||
following could all be viable solutions:
|
||||
|
||||
|
||||
#### Completely Replace the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var myDecoratedService = {
|
||||
// new service object to replace myService
|
||||
};
|
||||
return myDecoratedService;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Patch the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var someFn = $delegate.someFn;
|
||||
|
||||
function aNewFn() {
|
||||
// new service function
|
||||
someFn.apply($delegate, arguments);
|
||||
}
|
||||
|
||||
$delegate.someFn = aNewFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Augment the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
function helperFn() {
|
||||
// an additional fn to add to the service
|
||||
}
|
||||
|
||||
$delegate.aHelpfulAddition = helperFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note that whatever is returned by the decorator function will replace that which is being decorated. For example, a
|
||||
missing return statement will wipe out the entire object being decorated.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
Decorators have different rules for different services. This is because services are registered in different ways.
|
||||
Services are selected by name, however filters and directives are selected by appending `"Filter"` or `"Directive"` to
|
||||
the end of the name. The `$delegate` provided is dictated by the type of service.
|
||||
|
||||
| Service Type | Selector | $delegate |
|
||||
|--------------|-------------------------------|-----------------------------------------------------------------------|
|
||||
| Service | `serviceName` | The `object` or `function` returned by the service |
|
||||
| Directive | `directiveName + 'Directive'` | An `Array.<DirectiveObject>`<sub>{@link guide/decorators#drtvArray 1}</sub> |
|
||||
| Filter | `filterName + 'Filter'` | The `function` returned by the filter |
|
||||
|
||||
<small id="drtvArray">1. Multiple directives may be registered to the same selector/name</small>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** Developers should take care in how and why they are modifying the `$delegate` for the service. Not only
|
||||
should expectations for the consumer be kept, but some functionality (such as directive registration) does not take
|
||||
place after decoration, but during creation/registration of the original service. This means, for example, that
|
||||
an action such as pushing a directive object to a directive `$delegate` will likely result in unexpected behavior.
|
||||
|
||||
Furthermore, great care should be taken when decorating core services, directives, or filters as this may unexpectedly
|
||||
or adversely affect the functionality of the framework.
|
||||
</div>
|
||||
|
||||
### module.decorator
|
||||
|
||||
This {@link api/ng/type/angular.Module#decorator function} is the same as the `$provide.decorator` function except it is
|
||||
exposed through the module API. This allows you to separate your decorator patterns from your module config blocks. The
|
||||
main caveat here is that you will need to take note the order in which you create your decorators.
|
||||
|
||||
Unlike in the module config block (which allows configuration of services prior to their creation), the service must be
|
||||
registered prior to the decorator (see {@link guide/providers#provider-recipe Provider Recipe}). For example, the
|
||||
following would not work because you are attempting to decorate outside of the configuration phase and the service
|
||||
hasn't been created yet:
|
||||
|
||||
```js
|
||||
// will cause an error since 'someService' hasn't been registered
|
||||
angular.module('myApp').decorator('someService', ...);
|
||||
|
||||
angular.module('myApp').factory('someService', ...);
|
||||
```
|
||||
|
||||
## Example Applications
|
||||
|
||||
The following sections provide examples each of a service decorator, a directive decorator, and a filter decorator.
|
||||
|
||||
### Service Decorator Example
|
||||
|
||||
This example shows how we can replace the $log service with our own to display log messages.
|
||||
|
||||
<example module="myServiceDecorator" name="service-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('myServiceDecorator', []).
|
||||
|
||||
controller('Ctrl', [
|
||||
'$scope',
|
||||
'$log',
|
||||
'$timeout',
|
||||
function($scope, $log, $timeout) {
|
||||
var types = ['error', 'warn', 'log', 'info' ,'debug'], i;
|
||||
|
||||
for (i = 0; i < types.length; i++) {
|
||||
$log[types[i]](types[i] + ': message ' + (i + 1));
|
||||
}
|
||||
|
||||
$timeout(function() {
|
||||
$log.info('info: message logged in timeout');
|
||||
});
|
||||
}
|
||||
]).
|
||||
|
||||
directive('myLog', [
|
||||
'$log',
|
||||
function($log) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<ul id="myLog"><li ng-repeat="l in myLog" class="{{l.type}}">{{l.message}}</li></ul>',
|
||||
scope: {},
|
||||
compile: function() {
|
||||
return function(scope) {
|
||||
scope.myLog = $log.stack;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
]).
|
||||
|
||||
config([
|
||||
'$provide',
|
||||
function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function logDecorator($delegate) {
|
||||
|
||||
var myLog = {
|
||||
warn: function(msg) {
|
||||
log(msg, 'warn');
|
||||
},
|
||||
error: function(msg) {
|
||||
log(msg, 'error');
|
||||
},
|
||||
info: function(msg) {
|
||||
log(msg, 'info');
|
||||
},
|
||||
debug: function(msg) {
|
||||
log(msg, 'debug');
|
||||
},
|
||||
log: function(msg) {
|
||||
log(msg, 'log');
|
||||
},
|
||||
stack: []
|
||||
};
|
||||
|
||||
function log(msg, type) {
|
||||
myLog.stack.push({ type: type, message: msg.toString() });
|
||||
if (console && console[type]) console[type](msg);
|
||||
}
|
||||
|
||||
return myLog;
|
||||
|
||||
}
|
||||
]);
|
||||
|
||||
}
|
||||
]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<h1>Logs</h1>
|
||||
<my-log></my-log>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="style.css">
|
||||
li.warn { color: yellow; }
|
||||
li.error { color: red; }
|
||||
li.info { color: blue }
|
||||
li.log { color: black }
|
||||
li.debug { color: green }
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should display log messages in dom', function() {
|
||||
element.all(by.repeater('l in myLog')).count().then(function(count) {
|
||||
expect(count).toEqual(6);
|
||||
});
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Directive Decorator Example
|
||||
|
||||
Failed interpolated expressions in `ng-href` attributes can easily go unnoticed. We can decorate `ngHref` to warn us of
|
||||
those conditions.
|
||||
|
||||
<example module="urlDecorator" name="directive-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('urlDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.id = 3;
|
||||
$scope.warnCount = 0; // for testing
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
|
||||
// from that markup (if they exist) and returns an array of those expressions
|
||||
function matchExpressions(str) {
|
||||
var exps = str.match(/{{([^}]+)}}/g);
|
||||
|
||||
// if there isn't any, get out of here
|
||||
if (exps === null) return;
|
||||
|
||||
exps = exps.map(function(exp) {
|
||||
var prop = exp.match(/[^{}]+/);
|
||||
return prop === null ? null : prop[0];
|
||||
});
|
||||
|
||||
return exps;
|
||||
}
|
||||
|
||||
// remember: directives must be selected by appending 'Directive' to the directive selector
|
||||
$provide.decorator('ngHrefDirective', [
|
||||
'$delegate',
|
||||
'$log',
|
||||
'$parse',
|
||||
function($delegate, $log, $parse) {
|
||||
|
||||
// store the original link fn
|
||||
var originalLinkFn = $delegate[0].link;
|
||||
|
||||
// replace the compile fn
|
||||
$delegate[0].compile = function(tElem, tAttr) {
|
||||
|
||||
// store the original exp in the directive attribute for our warning message
|
||||
var originalExp = tAttr.ngHref;
|
||||
|
||||
// get the interpolated expressions
|
||||
var exps = matchExpressions(originalExp);
|
||||
|
||||
// create and store the getters using $parse
|
||||
var getters = exps.map(function(el) {
|
||||
if (el) return $parse(el);
|
||||
});
|
||||
|
||||
return function newLinkFn(scope, elem, attr) {
|
||||
// fire the originalLinkFn
|
||||
originalLinkFn.apply($delegate[0], arguments);
|
||||
|
||||
// observe the directive attr and check the expressions
|
||||
attr.$observe('ngHref', function(val) {
|
||||
|
||||
// if we have getters and getters is an array...
|
||||
if (getters && angular.isArray(getters)) {
|
||||
|
||||
// loop through the getters and process them
|
||||
angular.forEach(getters, function(g, idx) {
|
||||
|
||||
// if val is truthy, then the warning won't log
|
||||
var val = angular.isFunction(g) ? g(scope) : true;
|
||||
if (!val) {
|
||||
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
|
||||
'" is falsy!');
|
||||
|
||||
scope.warnCount++; // for testing
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// get rid of the old link function since we return a link function in compile
|
||||
delete $delegate[0].link;
|
||||
|
||||
// return the $delegate
|
||||
return $delegate;
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<a ng-href="/products/{{ id }}/view" id="id3">View Product {{ id }}</a>
|
||||
- <strong>id == 3</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ id + 5 }}/view" id="id8">View Product {{ id + 5 }}</a>
|
||||
- <strong>id + 5 == 8</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ someOtherId }}/view" id="someOtherId">View Product {{ someOtherId }}</a>
|
||||
- <strong style="background-color: #ffff00;">someOtherId == undefined</strong>, so warn<br>
|
||||
<a ng-href="/products/{{ someOtherId + 5 }}/view" id="someOtherId5">View Product {{ someOtherId + 5 }}</a>
|
||||
- <strong>someOtherId + 5 == 5</strong>, so no warning<br>
|
||||
<div>Warn Count: {{ warnCount }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should warn when an expression in the interpolated value is falsy', function() {
|
||||
var id3 = element(by.id('id3'));
|
||||
var id8 = element(by.id('id8'));
|
||||
var someOther = element(by.id('someOtherId'));
|
||||
var someOther5 = element(by.id('someOtherId5'));
|
||||
|
||||
expect(id3.getText()).toEqual('View Product 3');
|
||||
expect(id3.getAttribute('href')).toContain('/products/3/view');
|
||||
|
||||
expect(id8.getText()).toEqual('View Product 8');
|
||||
expect(id8.getAttribute('href')).toContain('/products/8/view');
|
||||
|
||||
expect(someOther.getText()).toEqual('View Product');
|
||||
expect(someOther.getAttribute('href')).toContain('/products//view');
|
||||
|
||||
expect(someOther5.getText()).toEqual('View Product 5');
|
||||
expect(someOther5.getAttribute('href')).toContain('/products/5/view');
|
||||
|
||||
expect(element(by.binding('warnCount')).getText()).toEqual('Warn Count: 1');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Filter Decorator Example
|
||||
|
||||
Let's say we have created an app that uses the default format for many of our `Date` filters. Suddenly requirements have
|
||||
changed (that never happens) and we need all of our default dates to be `'shortDate'` instead of `'mediumDate'`.
|
||||
|
||||
<example module="filterDecorator" name="filter-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('filterDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.genesis = new Date(2010, 0, 5);
|
||||
$scope.ngConf = new Date(2016, 4, 4);
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
$provide.decorator('dateFilter', [
|
||||
'$delegate',
|
||||
function dateDecorator($delegate) {
|
||||
|
||||
// store the original filter
|
||||
var originalFilter = $delegate;
|
||||
|
||||
// return our filter
|
||||
return shortDateDefault;
|
||||
|
||||
// shortDateDefault sets the format to shortDate if it is falsy
|
||||
function shortDateDefault(date, format, timezone) {
|
||||
if (!format) format = 'shortDate';
|
||||
|
||||
// return the result of the original filter
|
||||
return originalFilter(date, format, timezone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<div id="genesis">Initial Commit default to short date: {{ genesis | date }}</div>
|
||||
<div>ng-conf 2016 default short date: {{ ngConf | date }}</div>
|
||||
<div id="ngConf">ng-conf 2016 with full date format: {{ ngConf | date:'fullDate' }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should default date filter to short date format', function() {
|
||||
expect(element(by.id('genesis')).getText())
|
||||
.toMatch(/Initial Commit default to short date: \d{1,2}\/\d{1,2}\/\d{2}/);
|
||||
});
|
||||
|
||||
it('should still allow dates to be formatted', function() {
|
||||
expect(element(by.id('ngConf')).getText())
|
||||
.toMatch(/ng-conf 2016 with full date format\: [A-Za-z]+, [A-Za-z]+ \d{1,2}, \d{4}/);
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
@@ -27,6 +27,9 @@ Angular expressions are like JavaScript expressions with the following differenc
|
||||
* **Forgiving:** In JavaScript, trying to evaluate undefined properties generates `ReferenceError`
|
||||
or `TypeError`. In Angular, expression evaluation is forgiving to `undefined` and `null`.
|
||||
|
||||
* **Filters:** You can use {@link guide/filter filters} within expressions to format data before
|
||||
displaying it.
|
||||
|
||||
* **No Control Flow Statements:** You cannot use the following in an Angular expression:
|
||||
conditionals, loops, or exceptions.
|
||||
|
||||
@@ -38,10 +41,10 @@ Angular expressions are like JavaScript expressions with the following differenc
|
||||
|
||||
* **No Object Creation With New Operator:** You cannot use `new` operator in an Angular expression.
|
||||
|
||||
* **No Comma And Void Operators:** You cannot use `,` or `void` operators in an Angular expression.
|
||||
* **No Bitwise, Comma, And Void Operators:** You cannot use
|
||||
[Bitwise](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators),
|
||||
`,` or `void` operators in an Angular expression.
|
||||
|
||||
* **Filters:** You can use {@link guide/filter filters} within expressions to format data before
|
||||
displaying it.
|
||||
|
||||
If you want to run more complex JavaScript code, you should make it a controller method and call
|
||||
the method from your view. If you want to `eval()` an Angular expression yourself, use the
|
||||
|
||||
@@ -101,7 +101,7 @@ This is a short list of libraries with specific support and documentation for wo
|
||||
|
||||
## Learning Resources
|
||||
|
||||
###Books
|
||||
### Books
|
||||
* [AngularJS: Up and Running](http://www.amazon.com/AngularJS-Running-Enhanced-Productivity-Structured/dp/1491901942) by Brad Green and Shyam Seshadri
|
||||
* [Mastering Web App Development](http://www.amazon.com/Mastering-Web-Application-Development-AngularJS/dp/1782161821) by Pawel Kozlowski and Pete Bacon Darwin
|
||||
* [AngularJS Directives](http://www.amazon.com/AngularJS-Directives-Alex-Vanston/dp/1783280336) by Alex Vanston
|
||||
@@ -113,7 +113,7 @@ This is a short list of libraries with specific support and documentation for wo
|
||||
* [Responsive Web Design with AngularJS](http://www.amazon.com/Responsive-Design-AngularJS-Sandeep-Kumar/dp/178439842X) by Sandeep Kumar Patel
|
||||
* [Professional AngularJS](http://www.amazon.com/Professional-AngularJS-Valeri-Karpov/dp/1118832078/)
|
||||
|
||||
###Videos:
|
||||
### Videos:
|
||||
* [egghead.io](http://egghead.io/)
|
||||
* [Angular on YouTube](http://youtube.com/angularjs)
|
||||
* [Firebase Foundations for AngularJS](http://blog.watchandcode.com/firebase-foundations/)
|
||||
|
||||
@@ -26,9 +26,9 @@ machine:
|
||||
* [Git](http://git-scm.com/): The [Github Guide to
|
||||
Installing Git](https://help.github.com/articles/set-up-git) is a good source of information.
|
||||
|
||||
* [Node.js](http://nodejs.org): We use Node to generate the documentation, run a
|
||||
* [Node.js v4.x](http://nodejs.org): We use Node to generate the documentation, run a
|
||||
development web server, run tests, and generate distributable files. Depending on your system, you can install Node either from source or as a
|
||||
pre-packaged bundle.
|
||||
pre-packaged bundle. (Currently our build does not work properly on Node v5 or greater - please use v4.x.)
|
||||
|
||||
* [Java](http://www.java.com): We minify JavaScript using our
|
||||
[Closure Tools](https://developers.google.com/closure/) jar. Make sure you have Java (version 7 or higher) installed
|
||||
@@ -40,15 +40,6 @@ and included in your [PATH](http://docs.oracle.com/javase/tutorial/essential/env
|
||||
npm install -g grunt-cli
|
||||
```
|
||||
|
||||
* [Bower](http://bower.io/): We use Bower to manage client-side packages for the docs. Install the `bower` command-line tool globally with:
|
||||
|
||||
```shell
|
||||
npm install -g bower
|
||||
```
|
||||
|
||||
**Note:** You may need to use sudo (for OSX, *nix, BSD etc) or run your command shell as Administrator (for Windows) to install Grunt &
|
||||
Bower globally.
|
||||
|
||||
## Forking Angular on Github
|
||||
|
||||
To create a Github account, follow the instructions [here](https://github.com/signup/free).
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
# PhoneCat Tutorial App
|
||||
|
||||
A great way to get introduced to AngularJS is to work through this tutorial, which walks you through
|
||||
the construction of an AngularJS web app. The app you will build is a catalog that displays a list
|
||||
the construction of an Angular web app. The app you will build is a catalog that displays a list
|
||||
of Android devices, lets you filter the list to see only devices that interest you, and then view
|
||||
details for any device.
|
||||
|
||||
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413" alt="demo
|
||||
application running in the browser">
|
||||
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413"
|
||||
alt="demo application running in the browser">
|
||||
|
||||
Follow the tutorial to see how Angular makes browsers smarter — without the use of native
|
||||
extensions or plug-ins:
|
||||
@@ -28,10 +28,11 @@ When you finish the tutorial you will be able to:
|
||||
* Create a dynamic application that works in all modern browsers.
|
||||
* Use data binding to wire up your data model to your views.
|
||||
* Create and run unit tests, with Karma.
|
||||
* Create and run end to end tests, with Protractor.
|
||||
* Move application logic out of the template and into Controllers.
|
||||
* Create and run end-to-end tests, with Protractor.
|
||||
* Move application logic out of the template and into components and controllers.
|
||||
* Get data from a server using Angular services.
|
||||
* Apply animations to your application, using ngAnimate.
|
||||
* Apply animations to your application, using the `ngAnimate` module.
|
||||
* Structure your Angular applications in a modular way that scales well for larger projects.
|
||||
* Identify resources for learning more about AngularJS.
|
||||
|
||||
The tutorial guides you through the entire process of building a simple application, including
|
||||
@@ -42,16 +43,18 @@ You can go through the whole tutorial in a couple of hours or you may want to sp
|
||||
really digging into it. If you're looking for a shorter introduction to AngularJS, check out the
|
||||
{@link misc/started Getting Started} document.
|
||||
|
||||
# Get Started
|
||||
|
||||
# Environment Setup
|
||||
|
||||
The rest of this page explains how you can set up your local machine for development.
|
||||
If you just want to read the tutorial then you can just go straight to the first step:
|
||||
If you just want to _read_ the tutorial, you can go straight to the first step:
|
||||
[Step 0 - Bootstrapping](tutorial/step_00).
|
||||
|
||||
# Working with the code
|
||||
|
||||
## Working with the Code
|
||||
|
||||
You can follow along with this tutorial and hack on the code in the comfort of your own computer.
|
||||
In this way you can get hands-on practice of really writing AngularJS code and also on using the
|
||||
This way, you can get hands-on practice of really writing Angular code and also on using the
|
||||
recommended testing tools.
|
||||
|
||||
The tutorial relies on the use of the [Git][git] versioning system for source code management.
|
||||
@@ -62,10 +65,11 @@ a few git commands.
|
||||
### Install Git
|
||||
|
||||
You can download and install Git from http://git-scm.com/download. Once installed, you should have
|
||||
access to the `git` command line tool. The main commands that you will need to use are:
|
||||
access to the `git` command line tool. The main commands that you will need to use are:
|
||||
|
||||
* `git clone ...`: Clone a remote repository onto your local machine.
|
||||
* `git checkout ...`: Check out a particular branch or a tagged version of the code to hack on.
|
||||
|
||||
- `git clone ...` : clone a remote repository onto your local machine
|
||||
- `git checkout ...` : check out a particular branch or a tagged version of the code to hack on
|
||||
|
||||
### Download angular-phonecat
|
||||
|
||||
@@ -73,13 +77,14 @@ Clone the [angular-phonecat repository][angular-phonecat] located at GitHub by r
|
||||
command:
|
||||
|
||||
```
|
||||
git clone --depth=14 https://github.com/angular/angular-phonecat.git
|
||||
git clone --depth=16 https://github.com/angular/angular-phonecat.git
|
||||
```
|
||||
|
||||
This command creates the `angular-phonecat` directory in your current directory.
|
||||
This command creates an `angular-phonecat` sub-directory in your current directory.
|
||||
|
||||
<div class="alert alert-info">The `--depth=14` option just tells Git to pull down only the last 14 commits. This makes the
|
||||
download much smaller and faster.
|
||||
<div class="alert alert-info">
|
||||
The `--depth=16` option tells Git to pull down only the last 16 commits.
|
||||
This makes the download much smaller and faster.
|
||||
</div>
|
||||
|
||||
Change your current directory to `angular-phonecat`.
|
||||
@@ -88,16 +93,16 @@ Change your current directory to `angular-phonecat`.
|
||||
cd angular-phonecat
|
||||
```
|
||||
|
||||
The tutorial instructions, from now on, assume you are running all commands from the
|
||||
The tutorial instructions, from now on, assume you are running all commands from within the
|
||||
`angular-phonecat` directory.
|
||||
|
||||
|
||||
### Install Node.js
|
||||
|
||||
If you want to run the preconfigured local web-server and the test tools then you will also need
|
||||
[Node.js v0.10.27+][node].
|
||||
If you want to run the preconfigured local web server and the test tools then you will also need
|
||||
[Node.js v4+][node].
|
||||
|
||||
You can download a Node.js installer for your operating system from http://nodejs.org/download/.
|
||||
You can download a Node.js installer for your operating system from https://nodejs.org/en/download/.
|
||||
|
||||
Check the version of Node.js that you have installed by running the following command:
|
||||
|
||||
@@ -105,7 +110,7 @@ Check the version of Node.js that you have installed by running the following co
|
||||
node --version
|
||||
```
|
||||
|
||||
In Debian based distributions, there is a name clash with another utility called `node`. The
|
||||
In Debian based distributions, there might be a name clash with another utility called `node`. The
|
||||
suggested solution is to also install the `nodejs-legacy` apt package, which renames `node` to
|
||||
`nodejs`.
|
||||
|
||||
@@ -115,12 +120,9 @@ nodejs --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
|
||||
<div class="alert alert-info">If you need to run different versions of node.js
|
||||
in your local environment, consider installing
|
||||
<a href="https://github.com/creationix/nvm" title="Node Version Manager Github Repo link">
|
||||
Node Version Manager (nvm)
|
||||
</a>.
|
||||
<div class="alert alert-info">
|
||||
If you need to run different versions of Node.js in your local environment, consider installing
|
||||
[Node Version Manager (nvm)][nvm] or [Node Version Manager (nvm) for Windows][nvm-windows].
|
||||
</div>
|
||||
|
||||
Once you have Node.js installed on your machine, you can download the tool dependencies by running:
|
||||
@@ -129,30 +131,32 @@ Once you have Node.js installed on your machine, you can download the tool depen
|
||||
npm install
|
||||
```
|
||||
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following tools
|
||||
into the `node_modules` directory:
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following tools into the
|
||||
`node_modules` directory:
|
||||
|
||||
- [Bower][bower] - client-side code package manager
|
||||
- [Http-Server][http-server] - simple local static web server
|
||||
- [Karma][karma] - unit test runner
|
||||
- [Protractor][protractor] - end to end (E2E) test runner
|
||||
* [Bower][bower] - client-side code package manager
|
||||
* [Http-Server][http-server] - simple local static web server
|
||||
* [Karma][karma] - unit test runner
|
||||
* [Protractor][protractor] - end-to-end (E2E) test runner
|
||||
|
||||
Running `npm install` will also automatically use bower to download the Angular framework into the
|
||||
Running `npm install` will also automatically use bower to download the AngularJS framework into the
|
||||
`app/bower_components` directory.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note the angular-phonecat project is setup to install and run these utilities via npm scripts.
|
||||
This means that you do not have to have any of these utilities installed globally on your system
|
||||
to follow the tutorial. See **Installing Helper Tools** below for more information.
|
||||
to follow the tutorial. See [Installing Helper Tools](tutorial/#install-helper-tools-optional-)
|
||||
below for more information.
|
||||
</div>
|
||||
|
||||
The project is preconfigured with a number of npm helper scripts to make it easy to run the common
|
||||
tasks that you will need while developing:
|
||||
|
||||
- `npm start` : start a local development web-server
|
||||
- `npm test` : start the Karma unit test runner
|
||||
- `npm run protractor` : run the Protractor end to end (E2E) tests
|
||||
- `npm run update-webdriver` : install the drivers needed by Protractor
|
||||
* `npm start`: Start a local development web server.
|
||||
* `npm test`: Start the Karma unit test runner.
|
||||
* `npm run protractor`: Run the Protractor end-to-end (E2E) tests.
|
||||
* `npm run update-webdriver`: Install the drivers needed by Protractor.
|
||||
|
||||
|
||||
### Install Helper Tools (optional)
|
||||
|
||||
@@ -167,7 +171,7 @@ For instance, to install the Bower command line executable you would do:
|
||||
sudo npm install -g bower
|
||||
```
|
||||
|
||||
*(Omit the sudo if running on Windows)*
|
||||
_(Omit the sudo if running on Windows)_
|
||||
|
||||
Then you can run the bower tool directly, such as:
|
||||
|
||||
@@ -176,10 +180,10 @@ bower install
|
||||
```
|
||||
|
||||
|
||||
### Running Development Web Server
|
||||
### Running the Development Web Server
|
||||
|
||||
While Angular applications are purely client-side code, and it is possible to open them in a web
|
||||
browser directly from the file system, it is better to serve them from a HTTP web server. In
|
||||
browser directly from the file system, it is better to serve them from an HTTP web server. In
|
||||
particular, for security reasons, most modern browsers will not allow JavaScript to make server
|
||||
requests if the page is loaded directly from the file system.
|
||||
|
||||
@@ -190,70 +194,64 @@ application during development. Start the web server by running:
|
||||
npm start
|
||||
```
|
||||
|
||||
This will create a local webserver that is listening to port 8000 on your local machine.
|
||||
You can now browse to the application at:
|
||||
|
||||
```
|
||||
http://localhost:8000/app/index.html
|
||||
```
|
||||
This will create a local web server that is listening to port 8000 on your local machine.
|
||||
You can now browse to the application at http://localhost:8000/index.html.
|
||||
|
||||
<div class="alert alert-info">
|
||||
To serve the web app on a different IP address or port, edit the "start" script within package.json.
|
||||
You can use `-a` to set the address and `-p` to set the port.
|
||||
To serve the web app on a different IP address or port, edit the "start" script within
|
||||
`package.json`. You can use `-a` to set the address and `-p` to set the port. You also need to
|
||||
update the `baseUrl` configuration property in `e2e-test/protractor.conf.js`.
|
||||
</div>
|
||||
|
||||
|
||||
### Running Unit Tests
|
||||
|
||||
We use unit tests to ensure that the JavaScript code in our application is operating correctly.
|
||||
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in the
|
||||
`test/unit` directory.
|
||||
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in test
|
||||
files (specs) side-by-side with the application code. This way it's easier to find them and keep
|
||||
them up-to-date with the code under test. It also makes refactoring our app structure easier, since
|
||||
tests are moved together with the source code.
|
||||
|
||||
The angular-phonecat project is configured to use [Karma][karma] to run the unit tests for the
|
||||
application. Start Karma by running:
|
||||
application. Start Karma by running:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
This will start the Karma unit test runner. Karma will read the configuration file at
|
||||
`test/karma.conf.js`. This configuration file tells Karma to:
|
||||
This will start the Karma unit test runner. Karma will read the configuration file `karma.conf.js`,
|
||||
located at the root of the project directory. This configuration file tells Karma to:
|
||||
|
||||
- open up a Chrome browser and connect it to Karma
|
||||
- execute all the unit tests in this browser
|
||||
- report the results of these tests in the terminal/command line window
|
||||
- watch all the project's JavaScript files and re-run the tests whenever any of these change
|
||||
* Open up instances of the Chrome and Firefox browsers and connect them to Karma.
|
||||
* Execute all the unit tests in these browsers.
|
||||
* Report the results of these tests in the terminal/command line window.
|
||||
* Watch all the project's JavaScript files and re-run the tests whenever any of these change.
|
||||
|
||||
It is good to leave this running all the time, in the background, as it will give you immediate
|
||||
feedback about whether your changes pass the unit tests while you are working on the code.
|
||||
|
||||
|
||||
### Running End to End Tests
|
||||
### Running E2E Tests
|
||||
|
||||
We use End to End tests to ensure that the application as a whole operates as expected.
|
||||
End to End tests are designed to test the whole client side application, in particular that the
|
||||
views are displaying and behaving correctly. It does this by simulating real user interaction with
|
||||
the real application running in the browser.
|
||||
We use E2E (end-to-end) tests to ensure that the application as a whole operates as expected.
|
||||
E2E tests are designed to test the whole client-side application, in particular that the views are
|
||||
displaying and behaving correctly. It does this by simulating real user interaction with the real
|
||||
application running in the browser.
|
||||
|
||||
The End to End tests are kept in the `test/e2e` directory.
|
||||
The E2E tests are kept in the `e2e-tests` directory.
|
||||
|
||||
The angular-phonecat project is configured to use [Protractor][protractor] to run the End to End
|
||||
tests for the application. Protractor relies upon a set of drivers to allow it to interact with
|
||||
the browser. You can install these drivers by running:
|
||||
The angular-phonecat project is configured to use [Protractor][protractor] to run the E2E tests for
|
||||
the application. Protractor relies upon a set of drivers to allow it to interact with the browser.
|
||||
You can install these drivers by running:
|
||||
|
||||
```
|
||||
npm run update-webdriver
|
||||
```
|
||||
|
||||
*(You should only need to do this once.)*
|
||||
|
||||
You will need to have Java present on your dev machine to allow the Selenium standalone to be started.
|
||||
Check if you already have java installed by opening a terminal/command line window and typing
|
||||
'''
|
||||
java -version
|
||||
'''
|
||||
If java is already installed and exists in the PATH then you will be shown the version installed,
|
||||
if, however you receive a message that "java is not recognized as an internal command or external
|
||||
command" you will need to install [java].
|
||||
<div class="alert alert-info">
|
||||
You don't have to manually run this command. Our npm scripts are configured so that it will be
|
||||
automatically executed as part of the command that runs the E2E tests.
|
||||
</div>
|
||||
|
||||
Since Protractor works by interacting with a running application, we need to start our web server:
|
||||
|
||||
@@ -261,32 +259,79 @@ Since Protractor works by interacting with a running application, we need to sta
|
||||
npm start
|
||||
```
|
||||
|
||||
Then in a separate terminal/command line window, we can run the Protractor test scripts against the
|
||||
application by running:
|
||||
Then, in a _separate_ terminal/command line window, we can run the Protractor test scripts against
|
||||
the application by running:
|
||||
|
||||
```
|
||||
npm run protractor
|
||||
```
|
||||
|
||||
Protractor will read the configuration file at `test/protractor-conf.js`. This configuration tells
|
||||
Protractor to:
|
||||
Protractor will read the configuration file at `e2e-tests/protractor.conf.js`. This configuration
|
||||
file tells Protractor to:
|
||||
|
||||
- open up a Chrome browser and connect it to the application
|
||||
- execute all the End to End tests in this browser
|
||||
- report the results of these tests in the terminal/command line window
|
||||
- close down the browser and exit
|
||||
* Open up a Chrome browser and connect it to the application.
|
||||
* Execute all the E2E tests in this browser.
|
||||
* Report the results of these tests in the terminal/command line window.
|
||||
* Close the browser and exit.
|
||||
|
||||
It is good to run the end to end tests whenever you make changes to the HTML views or want to check
|
||||
that the application as a whole is executing correctly. It is very common to run End to End tests
|
||||
before pushing a new commit of changes to a remote repository.
|
||||
It is good to run the E2E tests whenever you make changes to the HTML views or want to check that
|
||||
the application as a whole is executing correctly. It is very common to run E2E tests before pushing
|
||||
a new commit of changes to a remote repository.
|
||||
|
||||
|
||||
### Common Issues
|
||||
|
||||
<br />
|
||||
**Firewall / Proxy issues**
|
||||
|
||||
Git and other tools, often use the `git:` protocol for accessing files in remote repositories.
|
||||
Some firewall configurations are blocking `git://` URLs, which leads to errors when trying to clone
|
||||
repositories or download dependencies. (For example corporate firewalls are "notorious" for blocking
|
||||
`git:`.)
|
||||
|
||||
If you run into this issue, you can force the use of `https:` instead, by running the following
|
||||
command: `git config --global url."https://".insteadOf git://`
|
||||
|
||||
<br />
|
||||
**Updating WebDriver takes too long**
|
||||
|
||||
Running `update-webdriver` for the first time may take from several seconds up to a few minutes
|
||||
(depending on your hardware and network connection). If you cancel the operation (e.g. using
|
||||
`Ctrl+C`), you might get errors, when trying to run Protractor later.
|
||||
|
||||
In that case, you can delete the `node_modules/` directory and run `npm install` again.
|
||||
|
||||
<br />
|
||||
**Protractor dependencies**
|
||||
|
||||
Under the hood, Protractor uses the [Selenium Stadalone Server][selenium], which in turn requires
|
||||
the [Java Development Kit (JDK)][jdk] to be installed on your local machine. Check this by running
|
||||
`java -version` from the command line.
|
||||
|
||||
If JDK is not already installed, you can download it [here][jdk-download].
|
||||
|
||||
<br />
|
||||
**Error running the web server**
|
||||
|
||||
The web server is configured to use port 8000. If the port is already in use (for example by another
|
||||
instance of a running web server) you will get an `EADDRINUSE` error. Make sure the port is
|
||||
available, before running `npm start`.
|
||||
|
||||
<hr />
|
||||
|
||||
Now that you have set up your local machine, let's get started with the tutorial:
|
||||
{@link step_00 Step 0 - Bootstrapping}
|
||||
|
||||
Now that you have set up your local machine, let's get started with the tutorial: {@link step_00 Step 0 - Bootstrapping}
|
||||
|
||||
[git]: http://git-scm.com/
|
||||
[node]: http://nodejs.org/
|
||||
[angular-phonecat]: https://github.com/angular/angular-phonecat
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[bower]: http://bower.io/
|
||||
[git]: http://git-scm.com/
|
||||
[http-server]: https://github.com/nodeapps/http-server
|
||||
[karma]: https://github.com/karma-runner/karma
|
||||
[java]: https://www.java.com/en/download/help/download_options.xml
|
||||
[jdk]: https://en.wikipedia.org/wiki/Java_Development_Kit
|
||||
[jdk-download]: http://www.oracle.com/technetwork/java/javase/downloads/index.html
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[node]: http://nodejs.org/
|
||||
[nvm]: https://github.com/creationix/nvm
|
||||
[nvm-windows]: https://github.com/coreybutler/nvm-windows
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[selenium]: http://docs.seleniumhq.org/
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
|
||||
|
||||
In this step of the tutorial, you will become familiar with the most important source code files of
|
||||
the AngularJS phonecat app. You will also learn how to start the development servers bundled with
|
||||
angular-seed, and run the application in the browser.
|
||||
the AngularJS Phonecat App. You will also learn how to start the development servers bundled with
|
||||
[angular-seed][angular-seed], and run the application in the browser.
|
||||
|
||||
Before you continue, make sure you have set up your development environment and installed all necessary
|
||||
dependencies, as described in {@link index#get-started Get Started}.
|
||||
Before you continue, make sure you have set up your development environment and installed all
|
||||
necessary dependencies, as described in the {@link tutorial/#environment-setup Environment Setup}
|
||||
section.
|
||||
|
||||
In the `angular-phonecat` directory, run this command:
|
||||
|
||||
@@ -19,118 +20,130 @@ In the `angular-phonecat` directory, run this command:
|
||||
git checkout -f step-0
|
||||
```
|
||||
|
||||
|
||||
This resets your workspace to step 0 of the tutorial app.
|
||||
|
||||
You must repeat this for every future step in the tutorial and change the number to the number of
|
||||
the step you are on. This will cause any changes you made within your working directory to be lost.
|
||||
|
||||
If you haven't already done so you need to install the dependencies by running:
|
||||
If you haven't already done so, you need to install the dependencies by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To see the app running in a browser, open a *separate* terminal/command line tab or window, then
|
||||
run `npm start` to start the web server. Now, open a browser window for the app and navigate to
|
||||
<a href="http://localhost:8000/app/" target="_blank" title="Open app on localhost">`http://localhost:8000/app/`</a>
|
||||
To see the app running in a browser, open a _separate_ terminal/command line tab or window, then run
|
||||
`npm start` to start the web server. Now, open a browser window for the app and navigate to
|
||||
http://localhost:8000/index.html.
|
||||
|
||||
Note that if you already ran the master branch app prior to checking out step-0, you may see the cached
|
||||
master version of the app in your browser window at this point. Just hit refresh to re-load the page.
|
||||
Note that if you already ran the master branch app prior to checking out step-0, you may see the
|
||||
cached master version of the app in your browser window at this point. Just hit refresh to re-load
|
||||
the page.
|
||||
|
||||
You can now see the page in your browser. It's not very exciting, but that's OK.
|
||||
|
||||
The HTML page that displays "Nothing here yet!" was constructed with the HTML code shown below.
|
||||
The code contains some key Angular elements that we will need as we progress.
|
||||
|
||||
__`app/index.html`:__
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en" ng-app>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Nothing here {{'yet' + '!'}}</p>
|
||||
<p>Nothing here {{'yet' + '!'}}</p>
|
||||
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## What is the code doing?
|
||||
|
||||
**`ng-app` directive:**
|
||||
<br />
|
||||
**`ng-app` attribute:**
|
||||
|
||||
<html ng-app>
|
||||
```html
|
||||
<html ng-app>
|
||||
```
|
||||
|
||||
The `ng-app` attribute represents an Angular directive named `ngApp` (Angular uses
|
||||
`spinal-case` for its custom attributes and `camelCase` for the corresponding directives
|
||||
which implement them).
|
||||
This directive is used to flag the html element that Angular should consider to be the root element
|
||||
of our application.
|
||||
This gives application developers the freedom to tell Angular if the entire html page or only a
|
||||
portion of it should be treated as the Angular application.
|
||||
The `ng-app` attribute represents an Angular directive, named `ngApp` (Angular uses `kebab-case` for
|
||||
its custom attributes and `camelCase` for the corresponding directives which implement them). This
|
||||
directive is used to flag the HTML element that Angular should consider to be the root element of
|
||||
our application. This gives application developers the freedom to tell Angular if the entire HTML
|
||||
page or only a portion of it should be treated as the AngularJS application.
|
||||
|
||||
**AngularJS script tag:**
|
||||
For more info on `ngApp`, check out the {@link ngApp API Reference}.
|
||||
|
||||
<script src="bower_components/angular/angular.js">
|
||||
<br />
|
||||
**`angular.js` script tag:**
|
||||
|
||||
This code downloads the `angular.js` script which registers a callback that will be executed by the
|
||||
```html
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
```
|
||||
|
||||
This code downloads the `angular.js` script which registers a callback that will be executed by the
|
||||
browser when the containing HTML page is fully downloaded. When the callback is executed, Angular
|
||||
looks for the {@link ng.directive:ngApp ngApp} directive. If
|
||||
Angular finds the directive, it will bootstrap the application with the root of the application DOM
|
||||
being the element on which the `ngApp` directive was defined.
|
||||
looks for the {@link ngApp ngApp} directive. If Angular finds the directive, it will bootstrap the
|
||||
application with the root of the application DOM being the element on which the `ngApp` directive
|
||||
was defined.
|
||||
|
||||
For more info on bootstrapping your app, checkout the [Bootstrap](guide/bootstrap) section of the
|
||||
Developer Guide.
|
||||
|
||||
<br />
|
||||
**Double-curly binding with an expression:**
|
||||
|
||||
Nothing here {{'yet' + '!'}}
|
||||
```html
|
||||
Nothing here {{'yet' + '!'}}
|
||||
```
|
||||
|
||||
This line demonstrates two core features of Angular's templating capabilities:
|
||||
|
||||
* a binding, denoted by double-curlies `{{ }}`
|
||||
* a simple expression `'yet' + '!'` used in this binding.
|
||||
* A binding, denoted by double-curlies: `{{ }}`
|
||||
* A simple expression used in this binding: `'yet' + '!'`
|
||||
|
||||
The binding tells Angular that it should evaluate an expression and insert the result into the
|
||||
DOM in place of the binding. Rather than a one-time insert, as we'll see in the next steps, a
|
||||
binding will result in efficient continuous updates whenever the result of the expression
|
||||
evaluation changes.
|
||||
The binding tells Angular that it should evaluate an expression and insert the result into the DOM
|
||||
in place of the binding. As we will see in the next steps, rather than a one-time insert, a binding
|
||||
will result in efficient continuous updates whenever the result of the expression evaluation
|
||||
changes.
|
||||
|
||||
{@link guide/expression Angular expression} is a JavaScript-like code snippet that is
|
||||
evaluated by Angular in the context of the current model scope, rather than within the scope of
|
||||
the global context (`window`).
|
||||
{@link guide/expression Angular expressions} are JavaScript-like code snippets that are evaluated by
|
||||
Angular in the context of the current model scope, rather than within the scope of the global
|
||||
context (`window`).
|
||||
|
||||
As expected, once this template is processed by Angular, the html page contains the text:
|
||||
"Nothing here yet!".
|
||||
As expected, once this template is processed by Angular, the HTML page contains the text:
|
||||
|
||||
## Bootstrapping AngularJS apps
|
||||
```
|
||||
Nothing here yet!
|
||||
```
|
||||
|
||||
Bootstrapping AngularJS apps automatically using the `ngApp` directive is very easy and suitable
|
||||
for most cases. In advanced cases, such as when using script loaders, you can use the
|
||||
{@link guide/bootstrap imperative / manual way} to bootstrap the app.
|
||||
## Bootstrapping Angular Applications
|
||||
|
||||
There are 3 important things that happen during the app bootstrap:
|
||||
Bootstrapping Angular applications automatically using the `ngApp` directive is very easy and
|
||||
suitable for most cases. In advanced cases, such as when using script loaders, you can use the
|
||||
{@link guide/bootstrap#manual-initialization imperative/manual way} to bootstrap the application.
|
||||
|
||||
There are 3 important things that happen during the bootstrap phase:
|
||||
|
||||
1. The {@link auto.$injector injector} that will be used for dependency injection is created.
|
||||
|
||||
2. The injector will then create the {@link ng.$rootScope root scope} that will
|
||||
become the context for the model of our application.
|
||||
2. The injector will then create the {@link ng.$rootScope root scope} that will become the context
|
||||
for the model of our application.
|
||||
|
||||
3. Angular will then "compile" the DOM starting at the `ngApp` root element, processing any
|
||||
directives and bindings found along the way.
|
||||
|
||||
|
||||
Once an application is bootstrapped, it will then wait for incoming browser events (such as mouse
|
||||
click, key press or incoming HTTP response) that might change the model. Once such an event occurs,
|
||||
Angular detects if it caused any model changes and if changes are found, Angular will reflect them
|
||||
in the view by updating all of the affected bindings.
|
||||
clicks, key presses or incoming HTTP responses) that might change the model. Once such an event
|
||||
occurs, Angular detects if it caused any model changes and if changes are found, Angular will
|
||||
reflect them in the view by updating all of the affected bindings.
|
||||
|
||||
The structure of our application is currently very simple. The template contains just one directive
|
||||
and one static binding, and our model is empty. That will soon change!
|
||||
@@ -140,27 +153,29 @@ and one static binding, and our model is empty. That will soon change!
|
||||
|
||||
## What are all these files in my working directory?
|
||||
|
||||
|
||||
Most of the files in your working directory come from the [angular-seed project][angular-seed] which
|
||||
is typically used to bootstrap new Angular projects. The seed project is pre-configured to install
|
||||
the angular framework (via `bower` into the `app/bower_components/` folder) and tools for developing
|
||||
a typical web app (via `npm`).
|
||||
Most of the files in your working directory come from the [angular-seed project][angular-seed],
|
||||
which is typically used to bootstrap new AngularJS projects. The seed project is pre-configured to
|
||||
install the AngularJS framework (via `bower` into the `app/bower_components/` directory) and tools
|
||||
for developing and testing a typical web application (via `npm`).
|
||||
|
||||
For the purposes of this tutorial, we modified the angular-seed with the following changes:
|
||||
|
||||
* Removed the example app
|
||||
* Added phone images to `app/img/phones/`
|
||||
* Added phone data files (JSON) to `app/phones/`
|
||||
* Removed the example app.
|
||||
* Removed unused dependencies.
|
||||
* Added phone images to `app/img/phones/`.
|
||||
* Added phone data files (JSON) to `app/phones/`.
|
||||
* Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file.
|
||||
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Try adding a new expression to the `index.html` that will do some math:
|
||||
<div></div>
|
||||
|
||||
<p>1 + 2 = {{ 1 + 2 }}</p>
|
||||
* Try adding a new expression to `index.html` that will do some math:
|
||||
|
||||
```html
|
||||
<p>1 + 2 = {{1 + 2}}</p>
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
@@ -12,11 +12,11 @@ dynamically display the same result with any set of data.
|
||||
|
||||
In this step you will add some basic information about two cell phones to an HTML page.
|
||||
|
||||
- The page now contains a list with information about two phones.
|
||||
* The page now contains a list with information about two phones.
|
||||
|
||||
<div doc-tutorial-reset="1"></div>
|
||||
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
@@ -39,15 +39,19 @@ In this step you will add some basic information about two cell phones to an HTM
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Try adding more static HTML to `index.html`. For example:
|
||||
|
||||
<p>Total number of phones: 2</p>
|
||||
```html
|
||||
<p>Total number of phones: 2</p>
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
This addition to your app uses static HTML to display the list. Now, let's go to {@link step_02
|
||||
step 2} to learn how to use AngularJS to dynamically generate the same list.
|
||||
This addition to your app uses static HTML to display the list. Now, let's go to
|
||||
{@link step_02 step 2} to learn how to use Angular to dynamically generate the same list.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="1"></ul>
|
||||
|
||||
@@ -6,37 +6,38 @@
|
||||
<ul doc-tutorial-nav="2"></ul>
|
||||
|
||||
|
||||
Now it's time to make the web page dynamic — with AngularJS. We'll also add a test that verifies the
|
||||
code for the controller we are going to add.
|
||||
Now, it's time to make the web page dynamic — with AngularJS. We will also add a test that verifies
|
||||
the code for the controller we are going to add.
|
||||
|
||||
There are many ways to structure the code for an application. For Angular apps, we encourage the use of
|
||||
[the Model-View-Controller (MVC) design pattern](http://en.wikipedia.org/wiki/Model–View–Controller)
|
||||
to decouple the code and to separate concerns. With that in mind, let's use a little Angular and
|
||||
JavaScript to add model, view, and controller components to our app.
|
||||
There are many ways to structure the code for an application. For Angular applications, we encourage
|
||||
the use of the [Model-View-Controller (MVC) design pattern][mvc-pattern] to decouple the code and
|
||||
separate concerns. With that in mind, let's use a little Angular and JavaScript to add models,
|
||||
views, and controllers to our app.
|
||||
|
||||
- The list of three phones is now generated dynamically from data
|
||||
* The list of three phones is now generated dynamically from data
|
||||
|
||||
<div doc-tutorial-reset="2"></div>
|
||||
|
||||
|
||||
## View and Template
|
||||
|
||||
In Angular, the __view__ is a projection of the model through the HTML __template__. This means that
|
||||
In Angular, the **view** is a projection of the model through the HTML **template**. This means that
|
||||
whenever the model changes, Angular refreshes the appropriate binding points, which updates the
|
||||
view.
|
||||
|
||||
The view component is constructed by Angular from this template:
|
||||
The view is constructed by Angular from this template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body ng-controller="PhoneListCtrl">
|
||||
<body ng-controller="PhoneListController">
|
||||
|
||||
<ul>
|
||||
<li ng-repeat="phone in phones">
|
||||
@@ -49,97 +50,117 @@ __`app/index.html`:__
|
||||
</html>
|
||||
```
|
||||
|
||||
We replaced the hard-coded phone list with the {@link ng.directive:ngRepeat ngRepeat directive}
|
||||
and two {@link guide/expression Angular expressions}:
|
||||
We replaced the hard-coded phone list with the {@link ngRepeat ngRepeat} directive and two
|
||||
{@link guide/expression Angular expressions}:
|
||||
|
||||
* The `ng-repeat="phone in phones"` attribute in the `<li>` tag is an Angular repeater directive.
|
||||
The repeater tells Angular to create a `<li>` element for each phone in the list using the `<li>`
|
||||
tag as the template.
|
||||
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be replaced
|
||||
by the value of the expressions.
|
||||
* The `ng-repeat="phone in phones"` attribute on the `<li>` tag is an Angular repeater directive.
|
||||
The repeater tells Angular to create a `<li>` element for each phone in the list, using the `<li>`
|
||||
tag as the template.
|
||||
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be
|
||||
replaced by the values of the expressions.
|
||||
|
||||
We have added a new directive, called `ng-controller`, which attaches a `PhoneListCtrl`
|
||||
__controller__ to the <body> tag. At this point:
|
||||
We have also added a new directive, called {@link ngController ngController}, which attaches a
|
||||
`PhoneListController` **controller** to the `<body>` tag. At this point:
|
||||
|
||||
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote
|
||||
bindings, which are referring to our application model, which is set up in our `PhoneListCtrl`
|
||||
controller.
|
||||
* `PhoneListController` is in charge of the DOM sub-tree under (and including) the `<body>` element.
|
||||
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote bindings, which
|
||||
are referring to our application model, which is set up in our `PhoneListController` controller.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note: We have specified an {@link angular.Module Angular Module} to load using `ng-app="phonecatApp"`,
|
||||
where `phonecatApp` is the name of our module. This module will contain the `PhoneListCtrl`.
|
||||
Note: We have specified an {@link angular.Module Angular Module} to load using
|
||||
`ng-app="phonecatApp"`, where `phonecatApp` is the name of our module. This module will contain
|
||||
the `PhoneListController`.
|
||||
</div>
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_02.png">
|
||||
|
||||
## Model and Controller
|
||||
|
||||
The data __model__ (a simple array of phones in object literal notation) is now instantiated within
|
||||
the `PhoneListCtrl` __controller__. The __controller__ is simply a constructor function that takes a
|
||||
`$scope` parameter:
|
||||
The data **model** (a simple array of phones in object literal notation) is now instantiated within
|
||||
the `PhoneListController` **controller**. The **controller** is simply a constructor function that
|
||||
takes a `$scope` parameter:
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<br />
|
||||
**`app/app.js`:**
|
||||
|
||||
```js
|
||||
|
||||
// Define the `phonecatApp` module
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope) {
|
||||
// Define the `PhoneListController` controller on the `phonecatApp` module
|
||||
phonecatApp.controller('PhoneListController', function PhoneListController($scope) {
|
||||
$scope.phones = [
|
||||
{'name': 'Nexus S',
|
||||
'snippet': 'Fast just got faster with Nexus S.'},
|
||||
{'name': 'Motorola XOOM™ with Wi-Fi',
|
||||
'snippet': 'The Next, Next Generation tablet.'},
|
||||
{'name': 'MOTOROLA XOOM™',
|
||||
'snippet': 'The Next, Next Generation tablet.'}
|
||||
{
|
||||
name: 'Nexus S',
|
||||
snippet: 'Fast just got faster with Nexus S.'
|
||||
}, {
|
||||
name: 'Motorola XOOM™ with Wi-Fi',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}, {
|
||||
name: 'MOTOROLA XOOM™',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Here we declared a controller called `PhoneListCtrl` and registered it in an AngularJS
|
||||
module, `phonecatApp`. Notice that our `ng-app` directive (on the `<html>` tag) now specifies the `phonecatApp`
|
||||
module name as the module to load when bootstrapping the Angular application.
|
||||
Here we declared a controller called `PhoneListController` and registered it in an Angular module,
|
||||
`phonecatApp`. Notice that our `ngApp` directive (on the `<html>` tag) now specifies the
|
||||
`phonecatApp` module name as the module to load when bootstrapping the application.
|
||||
|
||||
Although the controller is not yet doing very much, it plays a crucial role. By providing context
|
||||
for our data model, the controller allows us to establish data-binding between
|
||||
the model and the view. We connected the dots between the presentation, data, and logic components
|
||||
as follows:
|
||||
for our data model, the controller allows us to establish data-binding between the model and the
|
||||
view. We connected the dots between the presentation, data, and logic components as follows:
|
||||
|
||||
* The {@link ng.directive:ngController ngController} directive, located on the `<body>` tag,
|
||||
references the name of our controller, `PhoneListCtrl` (located in the JavaScript file
|
||||
`controllers.js`).
|
||||
* The {@link ngController ngController} directive, located on the `<body>` tag, references the name
|
||||
of our controller, `PhoneListController` (located in the JavaScript file `app.js`).
|
||||
|
||||
* The `PhoneListController` controller attaches the phone data to the `$scope` that was injected
|
||||
into our controller function. This _scope_ is a prototypal descendant of the _root scope_ that was
|
||||
created when the application was defined. This controller scope is available to all bindings
|
||||
located within the `<body ng-controller="PhoneListController">` tag.
|
||||
|
||||
* The `PhoneListCtrl` controller attaches the phone data to the `$scope` that was injected into our
|
||||
controller function. This *scope* is a prototypical descendant of the *root scope* that was created
|
||||
when the application was defined. This controller scope is available to all bindings located within
|
||||
the `<body ng-controller="PhoneListCtrl">` tag.
|
||||
|
||||
### Scope
|
||||
|
||||
The concept of a scope in Angular is crucial. A scope can be seen as the glue which allows the
|
||||
template, model and controller to work together. Angular uses scopes, along with the information
|
||||
template, model, and controller to work together. Angular uses scopes, along with the information
|
||||
contained in the template, data model, and controller, to keep models and views separate, but in
|
||||
sync. Any changes made to the model are reflected in the view; any changes that occur in the view
|
||||
are reflected in the model.
|
||||
|
||||
To learn more about Angular scopes, see the {@link ng.$rootScope.Scope angular scope documentation}.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_02.png">
|
||||
|
||||
## Tests
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Angular scopes prototypally inherit from their parent scope, all the way up to the *root scope*
|
||||
of the application. As a result, assigning values directly on the scope makes it easy to share
|
||||
data across different parts of the page and create interactive applications.
|
||||
While this approach works for prototypes and smaller applications, it quickly leads to tight
|
||||
coupling and difficulty to reason about changes in our data model.
|
||||
</p>
|
||||
<p>
|
||||
In the next step, we will learn how to better organize our code, by "packaging" related pieces
|
||||
of application and presentation logic into isolated, reusable entities, called components.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
The "Angular way" of separating controller from the view, makes it easy to test code as it is being
|
||||
developed. If our controller is available on the global namespace then we could simply instantiate it
|
||||
with a mock `scope` object:
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
developed. If our controller were available on the global namespace, we could simply instantiate it
|
||||
with a mock scope object:
|
||||
|
||||
<br />
|
||||
```js
|
||||
describe('PhoneListCtrl', function(){
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
var scope = {},
|
||||
ctrl = new PhoneListCtrl(scope);
|
||||
it('should create a `phones` model with 3 phones', function() {
|
||||
var scope = {};
|
||||
var ctrl = new PhoneListController(scope);
|
||||
|
||||
expect(scope.phones.length).toBe(3);
|
||||
});
|
||||
@@ -147,30 +168,31 @@ describe('PhoneListCtrl', function(){
|
||||
});
|
||||
```
|
||||
|
||||
The test instantiates `PhoneListCtrl` and verifies that the phones array property on the scope
|
||||
contains three records. This example demonstrates how easy it is to create a unit test for code in
|
||||
Angular. Since testing is such a critical part of software development, we make it easy to create
|
||||
tests in Angular so that developers are encouraged to write them.
|
||||
The test instantiates `PhoneListController` and verifies that the phones array property on the
|
||||
scope contains three records. This example demonstrates how easy it is to create a unit test for
|
||||
code in Angular. Since testing is such a critical part of software development, we make it easy to
|
||||
create tests in Angular so that developers are encouraged to write them.
|
||||
|
||||
### Testing non-Global Controllers
|
||||
|
||||
In practice, you will not want to have your controller functions in the global namespace. Instead,
|
||||
you can see that we have registered it via an anonymous constructor function on the `phonecatApp`
|
||||
module.
|
||||
## Testing non-global Controllers
|
||||
|
||||
In practice, you will not want to have your controller functions in the global namespace. Instead,
|
||||
you can see that we have registered it via a constructor function on the `phonecatApp` module.
|
||||
|
||||
In this case Angular provides a service, `$controller`, which will retrieve your controller by name.
|
||||
Here is the same test using `$controller`:
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<br />
|
||||
**`app/app.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneListCtrl', function(){
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
it('should create "phones" model with 3 phones', inject(function($controller) {
|
||||
var scope = {},
|
||||
ctrl = $controller('PhoneListCtrl', {$scope:scope});
|
||||
it('should create a `phones` model with 3 phones', inject(function($controller) {
|
||||
var scope = {};
|
||||
var ctrl = $controller('PhoneListController', {$scope: scope});
|
||||
|
||||
expect(scope.phones.length).toBe(3);
|
||||
}));
|
||||
@@ -179,29 +201,46 @@ describe('PhoneListCtrl', function(){
|
||||
```
|
||||
|
||||
* Before each test we tell Angular to load the `phonecatApp` module.
|
||||
* We ask Angular to `inject` the `$controller` service into our test function
|
||||
* We use `$controller` to create an instance of the `PhoneListCtrl`
|
||||
* We ask Angular to `inject` the `$controller` service into our test function.
|
||||
* We use `$controller` to create an instance of the `PhoneListController`.
|
||||
* With this instance, we verify that the phones array property on the scope contains three records.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>**A note on file naming:**</p>
|
||||
<p>
|
||||
As already mentioned in the [introduction](tutorial/#running-unit-tests), the unit test files
|
||||
(specs) are kept side-by-side with the application code. We name our specs after the file
|
||||
containing the code to be tested plus a specific suffix to distinguish them from files
|
||||
containing application code. Note that test files are still plain JavaScript files, so they have
|
||||
a `.js` file extension.
|
||||
</p>
|
||||
<p>
|
||||
In this tutorial, we are using the `.spec` suffix. So the test file corresponding to
|
||||
`something.js` would be called `something.spec.js`.
|
||||
(Another common convention is to use a `_spec` or `_test` suffix.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
### Writing and Running Tests
|
||||
|
||||
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework when
|
||||
writing tests. Although Angular does not require you to use Jasmine, we wrote all of the tests in
|
||||
this tutorial in Jasmine v1.3. You can learn about Jasmine on the [Jasmine home page][jasmine] and
|
||||
at the [Jasmine docs][jasmine-docs].
|
||||
## Writing and Running Tests
|
||||
|
||||
The angular-seed project is pre-configured to run unit tests using [Karma][karma] but you will need
|
||||
Many Angular developers prefer the syntax of
|
||||
[Jasmine's Behavior-Driven Development (BDD) framework][jasmine-home], when writing tests. Although
|
||||
Angular does not require you to use Jasmine, we wrote all of the tests in this tutorial in Jasmine
|
||||
v2.4. You can learn about Jasmine on the [Jasmine home page][jasmine-home] and at the
|
||||
[Jasmine docs][jasmine-docs].
|
||||
|
||||
The angular-seed project is pre-configured to run unit tests using [Karma][karma], but you will need
|
||||
to ensure that Karma and its necessary plugins are installed. You can do this by running
|
||||
`npm install`.
|
||||
|
||||
To run the tests, and then watch the files for changes: `npm test`.
|
||||
To run the tests, and then watch the files for changes execute: `npm test`
|
||||
|
||||
* Karma will start new instances of Chrome and Firefox browsers automatically. Just ignore them and
|
||||
let them run in the background. Karma will use these browsers for test execution.
|
||||
* If you only have one of the browsers installed on your machine (either Chrome or Firefox), make
|
||||
sure to update the karma configuration file before running the test. Locate the configuration file
|
||||
in `test/karma.conf.js`, then update the `browsers` property.
|
||||
sure to update the karma configuration file (`karma.conf.js`), before running the test. Locate the
|
||||
configuration file in the root directory and update the `browsers` property.
|
||||
|
||||
E.g. if you only have Chrome installed:
|
||||
<pre>
|
||||
@@ -213,23 +252,27 @@ To run the tests, and then watch the files for changes: `npm test`.
|
||||
* You should see the following or similar output in the terminal:
|
||||
|
||||
<pre>
|
||||
info: Karma server started at http://localhost:9876/
|
||||
info (launcher): Starting browser "Chrome"
|
||||
info (Chrome 22.0): Connected on socket id tPUm9DXcLHtZTKbAEO-n
|
||||
Chrome 22.0: Executed 1 of 1 SUCCESS (0.093 secs / 0.004 secs)
|
||||
INFO [karma]: Karma server started at http://localhost:9876/
|
||||
INFO [launcher]: Starting browser Chrome
|
||||
INFO [Chrome 49.0]: Connected on socket ... with id ...
|
||||
Chrome 49.0: Executed 1 of 1 SUCCESS (0.05 secs / 0.04 secs)
|
||||
</pre>
|
||||
|
||||
Yay! The test passed! Or not...
|
||||
* To rerun the tests, just change any of the source or test .js files. Karma will notice the change
|
||||
|
||||
* To rerun the tests, just change any of the source or test `.js` files. Karma will notice the change
|
||||
and will rerun the tests for you. Now isn't that sweet?
|
||||
|
||||
<div class="alert alert-info">
|
||||
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a minimized
|
||||
browser is limited, which results in your karma tests running extremely slow.
|
||||
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a
|
||||
minimized browser is limited, which results in your karma tests running extremely slow.
|
||||
</div>
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Add another binding to `index.html`. For example:
|
||||
|
||||
```html
|
||||
@@ -238,46 +281,60 @@ browser is limited, which results in your karma tests running extremely slow.
|
||||
|
||||
* Create a new model property in the controller and bind to it from the template. For example:
|
||||
|
||||
$scope.name = "World";
|
||||
```js
|
||||
$scope.name = 'world';
|
||||
```
|
||||
|
||||
Then add a new binding to `index.html`:
|
||||
|
||||
<p>Hello, {{name}}!</p>
|
||||
```html
|
||||
<p>Hello, {{name}}!</p>
|
||||
```
|
||||
|
||||
Refresh your browser and verify that it says "Hello, World!".
|
||||
Refresh your browser and verify that it says 'Hello, world!'.
|
||||
|
||||
* Update the unit test for the controller in `./test/unit/controllersSpec.js` to reflect the previous change. For example by adding:
|
||||
* Update the unit test for the controller in `app/app.spec.js` to reflect the previous change.
|
||||
For example by adding:
|
||||
|
||||
expect(scope.name).toBe('World');
|
||||
```js
|
||||
expect(scope.name).toBe('world');
|
||||
```
|
||||
|
||||
* Create a repeater in `index.html` that constructs a simple table:
|
||||
|
||||
<table>
|
||||
<tr><th>row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
|
||||
</table>
|
||||
```html
|
||||
<table>
|
||||
<tr><th>Row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
Now, make the list 1-based by incrementing `i` by one in the binding:
|
||||
|
||||
<table>
|
||||
<tr><th>row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
|
||||
</table>
|
||||
```html
|
||||
<table>
|
||||
<tr><th>Row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
Extra points: try and make an 8x8 table using an additional `ng-repeat`.
|
||||
Extra points: Try and make an 8x8 table using an additional `ng-repeat`.
|
||||
|
||||
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`.
|
||||
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use
|
||||
`toBe(4)`.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
You now have a dynamic app that features separate model, view, and controller components, and you
|
||||
are testing as you go. Now, let's go to {@link step_03 step 3} to learn how to add full text search
|
||||
to the app.
|
||||
We now have a dynamic application which separates models, views, and controllers, and we are testing
|
||||
as we go. Let's go to {@link step_03 step 3} to learn how to improve our application's architecture,
|
||||
by utilizing components.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="2"></ul>
|
||||
|
||||
[jasmine]: http://jasmine.github.io/
|
||||
[jasmine-docs]: http://jasmine.github.io/1.3/introduction.html
|
||||
[karma]: http://karma-runner.github.io/
|
||||
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
@@ -1,223 +1,282 @@
|
||||
@ngdoc tutorial
|
||||
@name 3 - Filtering Repeaters
|
||||
@name 3 - Components
|
||||
@step 3
|
||||
@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.
|
||||
In the previous step, we saw how a controller and a template worked together to convert a static
|
||||
HTML page into a dynamic view. This is a very common pattern in Single-Page Applications in general
|
||||
(and Angular applications in particular):
|
||||
|
||||
* Instead of creating a static HTML page on the server, the client-side code "takes over" and
|
||||
interacts dynamically with the view, updating it instantly to reflect changes in model data or
|
||||
state, usually as a result of user interaction (we'll see an example shortly in
|
||||
{@link step_05 step 5}).
|
||||
|
||||
The **template** (the part of the view containing the bindings and presentation logic) acts as a
|
||||
blueprint for how our data should be organized and presented to the user.
|
||||
The **controller** provides the context in which the bindings are evaluated and applies behavior
|
||||
and logic to our template.
|
||||
|
||||
There are still a couple of areas we can do better:
|
||||
|
||||
1. What if we want to reuse the same functionality in a different part of our application ?<br />
|
||||
We would need to duplicate the whole template (including the controller). This is error-prone and
|
||||
hurts maintainability.
|
||||
2. The scope, that glues our controller and template together into a dynamic view, is not isolated
|
||||
from other parts of the page. What this means is that a random, unrelated change in a different
|
||||
part of the page (e.g. a property-name conflict) could have unexpected and hard-to-debug side
|
||||
effects on our view.
|
||||
|
||||
(OK, this might not be a real concern in our minimal example, but it **is** a valid concern for
|
||||
bigger, real-world applications.)
|
||||
|
||||
* 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.
|
||||
|
||||
<div doc-tutorial-reset="3"></div>
|
||||
|
||||
|
||||
## Controller
|
||||
## Components to the rescue!
|
||||
|
||||
We made no changes to the controller.
|
||||
Since this combination (template + controller) is such a common and recurring pattern, Angular
|
||||
provides an easy and concise way to combine them together into reusable and isolated entities,
|
||||
known as _components_.
|
||||
Additionally, Angular will create a so called _isolate scope_ for each instance of our component,
|
||||
which means no prototypal inheritance and no risk of our component affecting other parts of the
|
||||
application or vice versa.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Since this is an introductory tutorial, we are not going to dive deep into all features provided
|
||||
by Angular **components**. You can read more about components and their usage patterns in the
|
||||
[Components](guide/component) section of the Developer Guide.
|
||||
</p>
|
||||
<p>
|
||||
In fact, one could think of components as an opinionated and stripped-down version of their more
|
||||
complex and verbose (but powerful) siblings, **directives**, which are Angular's way of teaching
|
||||
HTML new tricks. You can read all about them in the [Directives](guide/directive) section of the
|
||||
Developer Guide.
|
||||
</p>
|
||||
<p>
|
||||
(**Note:** Directives are an advanced topic, so you might want to postpone studying them, until
|
||||
you have mastered the basics.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Template
|
||||
To create a component, we use the {@link angular.Module#component .component()} method of an
|
||||
{@link module Angular module}. We must provide the name of the component and the Component
|
||||
Definition Object (CDO for short).
|
||||
|
||||
__`app/index.html`:__
|
||||
Remember that (since components are also directives) the name of the component is in `camelCase`,
|
||||
but we will use `kebab-case`, when referring to it in our HTML.
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
In its simplest form, the CDO will just contain a template and a controller. (We can actually omit
|
||||
the controller and Angular will create a dummy controller for us. This is useful for simple
|
||||
"presentational" components, that don't attach any behavior to the template.)
|
||||
|
||||
Search: <input ng-model="query">
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We added a standard HTML `<input>` tag and used Angular's
|
||||
{@link ng.filter:filter filter} function to process the input for the
|
||||
{@link 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 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`:__
|
||||
Let's see an example:
|
||||
|
||||
```js
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html');
|
||||
angular.
|
||||
module('myApp').
|
||||
component('greetUser', {
|
||||
template: 'Hello, {{$ctrl.user}}!',
|
||||
controller: function GreetUserController() {
|
||||
this.user = 'world';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Now, every time we include `<greet-user></greet-user>` in our view, Angular will expand it into a
|
||||
DOM sub-tree constructed using the provided `template` and managed by an instance of the specified
|
||||
controller.
|
||||
|
||||
But wait, where did that `$ctrl` come from and what does it refer to ?
|
||||
|
||||
For reasons already mentioned (and for other reasons that are out of the scope of this tutorial), it
|
||||
is considered a good practice to avoid using the scope directly. We can (and should) use our
|
||||
controller instance; i.e. assign our data and methods on properties of our controller (the "`this`"
|
||||
inside the controller constructor), instead of directly to the scope.
|
||||
|
||||
From the template, we can refer to our controller instance using an alias. This way, the context of
|
||||
evaluation for our expressions is even more clear. By default, components use `$ctrl` as the
|
||||
controller alias, but we can override it, should the need arise.
|
||||
|
||||
There are more options available, so make sure you check out the
|
||||
{@link ng.$compileProvider#component API Reference}, before using `.component()` in your own
|
||||
applications.
|
||||
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
## Using Components
|
||||
|
||||
var phoneList = element.all(by.repeater('phone in phones'));
|
||||
var query = element(by.model('query'));
|
||||
Now that we know how to create components, let's refactor the HTML page to make use of our newly
|
||||
acquired skill.
|
||||
|
||||
expect(phoneList.count()).toBe(3);
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
```html
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="phone-list.component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
<!-- Use a custom component to render a list of phones -->
|
||||
<phone-list></phone-list>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/app.js`:**
|
||||
|
||||
```js
|
||||
// Define the `phonecatApp` module
|
||||
angular.module('phonecatApp', []);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
// Register `phoneList` component, along with its associated controller and template
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
component('phoneList', {
|
||||
template:
|
||||
'<ul>' +
|
||||
'<li ng-repeat="phone in $ctrl.phones">' +
|
||||
'<span>{{phone.name}}</span>' +
|
||||
'<p>{{phone.snippet}}</p>' +
|
||||
'</li>' +
|
||||
'</ul>',
|
||||
controller: function PhoneListController() {
|
||||
this.phones = [
|
||||
{
|
||||
name: 'Nexus S',
|
||||
snippet: 'Fast just got faster with Nexus S.'
|
||||
}, {
|
||||
name: 'Motorola XOOM™ with Wi-Fi',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}, {
|
||||
name: 'MOTOROLA XOOM™',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Voilà! The resulting output should look the same, but let's see what we have gained:
|
||||
|
||||
* Our phone list is reusable. Just drop `<phone-list></phone-list>` anywhere in the page to get a
|
||||
list of phones.
|
||||
* Our main view (`index.html`) is cleaner and more declarative. Just by looking at it, we know there
|
||||
is a list of phones. We are not bothered with implementation details.
|
||||
* Our component is isolated and safe from "external influences". Likewise, we don't have to worry,
|
||||
that we might accidentally break something in some other part of the application. What happens
|
||||
inside our component, stays inside our component.
|
||||
* It's easier to test our component in isolation.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_03.png">
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>**A note on file naming:**</p>
|
||||
<p>
|
||||
It is a good practice to distinguish different types of entities by suffix. In this tutorial, we
|
||||
are using the `.component` suffix for components, so the definition of a `someComponent`
|
||||
component would be in a file named `some-component.component.js`.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Although we have combined our controller with a template into a component, we still can (and should)
|
||||
unit test the controller separately, since this is where are application logic and data reside.
|
||||
|
||||
In order to retrieve and instantiate a component's controller, Angular provides the
|
||||
{@link ngMock.$componentController $componentController} service.
|
||||
|
||||
<div class="alert alert-info">
|
||||
The `$controller` service that we used in the previous step, can only instantiate controllers that
|
||||
where registered by name, using the `.controller()` method. We could have registered our component
|
||||
controller this way too, if we wanted to. Instead, we chose to define it inline — inside the
|
||||
CDO — to keep things localized, but either way works equally well.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
**`app/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
// Test the controller
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
it('should create a `phones` model with 3 phones', inject(function($componentController) {
|
||||
var ctrl = $componentController('phoneList');
|
||||
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
The test retrieves the controller associated with the `phoneList` component, instantiates it and
|
||||
verifies that the phones array property on it contains three records. Note that the data is now on
|
||||
the controller instance itself, not on a `scope`.
|
||||
|
||||
### Running End to End Tests with Protractor
|
||||
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 [Protractor](https://github.com/angular/protractor). Read
|
||||
about the Protractor APIs at http://angular.github.io/protractor/#/api.
|
||||
|
||||
Much like Karma is the test runner for unit tests, we use Protractor to run end-to-end tests.
|
||||
Try it with `npm run protractor`. End-to-end tests are slow, so unlike with unit tests, Protractor
|
||||
will exit after the test run and will not automatically rerun the test suite on every file change.
|
||||
To rerun the test suite, execute `npm run protractor` again.
|
||||
## Running Tests
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note: You must ensure your application is being served via a web-server to test with protractor.
|
||||
You can do this using `npm start`.
|
||||
You also need to ensure you've installed the protractor and updated webdriver prior to running the
|
||||
`npm run protractor`. You can do this by issuing `npm install` and `npm run update-webdriver` into
|
||||
your terminal.
|
||||
</div>
|
||||
Same as before, execute `npm test` to run the tests and then watch the files for changes.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
### Display Current Query
|
||||
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.
|
||||
<div></div>
|
||||
|
||||
### Display Query in Title
|
||||
Let's see how we can get the current value of the `query` model to appear in the HTML page title.
|
||||
* Try the experiments from the previous step, this time on the `phoneList` component.
|
||||
|
||||
* Add an end-to-end test into the `describe` block, `test/e2e/scenarios.js` should look like this:
|
||||
* Add a couple more phone lists on the page, by just adding more `<phone-list></phone-list>`
|
||||
elements in `index.html`. Now add another binding to the `phoneList` component's template:
|
||||
|
||||
```js
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html');
|
||||
});
|
||||
|
||||
var phoneList = element.all(by.repeater('phone in phones'));
|
||||
var query = element(by.model('query'));
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
expect(phoneList.count()).toBe(3);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
|
||||
it('should display the current filter value in the title bar', function() {
|
||||
query.clear();
|
||||
expect(browser.getTitle()).toMatch(/Google Phone Gallery:\s*$/);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(browser.getTitle()).toMatch(/Google Phone Gallery: nexus$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
template:
|
||||
'<p>Total number of phones: {{$ctrl.phones.length}}</p>' +
|
||||
'<ul>' +
|
||||
...
|
||||
```
|
||||
|
||||
Run protractor (`npm run protractor`) to see this test fail.
|
||||
|
||||
|
||||
* 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="phonecatApp" ng-controller="PhoneListCtrl">
|
||||
|
||||
Be sure to __remove__ the `ng-controller` declaration from the body element.
|
||||
|
||||
* Re-run `npm run protractor` to see the test now pass.
|
||||
|
||||
* 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 ng.directive:ngBind ngBind} or
|
||||
{@link 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>
|
||||
Reload the page and watch the new "feature" propagate to all phone lists. In real-world
|
||||
applications, where the phone lists could appear on several different pages, being able to change
|
||||
or add something in one place (e.g. a component's template) and have that change propagate
|
||||
throughout the application, is a big win.
|
||||
|
||||
|
||||
# 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.
|
||||
You have learned how to organize your application and presentation logic into isolated reusable
|
||||
components. Let's go to {@link step_04 step 4} to learn how to organize our code in directories and
|
||||
files, so it remains easy to locate as our application grows.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="3"></ul>
|
||||
|
||||
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
@@ -1,199 +1,315 @@
|
||||
@ngdoc tutorial
|
||||
@name 4 - Two-way Data Binding
|
||||
@name 4 - Directory and File Organization
|
||||
@step 4
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="4"></ul>
|
||||
|
||||
|
||||
In this step, you will add a feature to let your users control the order of the items in the phone
|
||||
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
|
||||
the repeater, and letting the data binding magic do the rest of the work.
|
||||
In this step, we will not be adding any new functionality to our application. Instead, we are going
|
||||
to take a step back, refactor our codebase and move files and code around, in order to make our
|
||||
application more easily expandable and maintainable.
|
||||
|
||||
* In addition to the search box, the app displays a drop down menu that allows users to control the
|
||||
order in which the phones are listed.
|
||||
In the previous step, we saw how to architect our application to be modular and testable. What's
|
||||
equally important though, is organizing our codebase in a way that makes it easy (both for us and
|
||||
other developers on our team) to navigate through the code and quickly locate the pieces that are
|
||||
relevant to a specific feature or section of the application.
|
||||
|
||||
To that end, we will explain why and how we:
|
||||
|
||||
* Put each entity in its **own file**.
|
||||
* Organize our code by **feature area**, instead of by function.
|
||||
* Split our code into **modules** that other modules can depend on.
|
||||
|
||||
<div class="alert alert-info">
|
||||
We will keep it short, not going into great detail on every good practice and convention. These
|
||||
principles are explained in great detail in the [Angular Style Guide][styleguide], which also
|
||||
contains many more techniques for effectively organizing Angular codebases.
|
||||
</div>
|
||||
|
||||
|
||||
<div doc-tutorial-reset="4"></div>
|
||||
|
||||
|
||||
## Template
|
||||
## One Feature per File
|
||||
|
||||
__`app/index.html`:__
|
||||
It might be tempting, for the sake of simplicity, to put everything in one file, or have one file
|
||||
per type; e.g. all controllers is one file, all components in another file, all services in a third
|
||||
file, and so on.
|
||||
This might seem to work well in the beginning, but as our application grows it becomes a burden to
|
||||
maintain. As we add more and more features, our files will get bigger and bigger and it will be
|
||||
difficult to navigate and find the code we are looking for.
|
||||
|
||||
```html
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
Instead we should put each feature/entity in its own file. Each stand-alone controller will be
|
||||
defined in its own file, each component will be defined in each own file, etc.
|
||||
|
||||
Luckily, we don't need to change anything with respect to that guideline in our code, since we have
|
||||
already defined our `phoneList` component in its own `phone-list.component.js` file. Good job!
|
||||
|
||||
We will keep this in mind though, as we add more features.
|
||||
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
## Organizing by Feature
|
||||
|
||||
So, now that we learned we should put everything in its own file, our `app/` directory will soon be
|
||||
full with dozens of files and specs (remember we keep our unit test files next to the corresponding
|
||||
source code files). What's more important, logically related files will not be grouped together; it
|
||||
will be really difficult of locate all files related to a specific section of the application and
|
||||
make a change or fix a bug.
|
||||
|
||||
So, what shall we do?
|
||||
|
||||
Well, we are going to group our files into directories _by feature_. For example, since we have a
|
||||
section in our application that lists phones, we will put all related files into a `phone-list/`
|
||||
directory under `app/`. We are soon to find out that certain features are used across different
|
||||
parts of the application. We will put those inside `app/core/`.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Other typical names for our `core` directory are `shared`, `common` and `components`. The last
|
||||
one is kind of misleading though, as it will contain other things than components as well.
|
||||
</p>
|
||||
<p>
|
||||
(This is mostly a relic of the past, when "components" just meant the generic building blocks of
|
||||
an application.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Based on what we have discussed so far, here is our directory/file layout for the `phoneList`
|
||||
"feature":
|
||||
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
app.js
|
||||
```
|
||||
|
||||
We made the following changes to the `index.html` template:
|
||||
|
||||
* First, we added a `<select>` html element named `orderProp`, so that our users can pick from the
|
||||
two provided sorting options.
|
||||
## Using Modules
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_04.png">
|
||||
As previously mentioned, one of the benefits of having a modular architecture is code reuse —
|
||||
not only inside the same application, but across applications too. There is one final step in making
|
||||
this code reuse frictionless:
|
||||
|
||||
* We then chained the `filter` filter with {@link ng.filter:orderBy `orderBy`}
|
||||
filter to further process the input into the repeater. `orderBy` is a filter that takes an input
|
||||
array, copies it and reorders the copy which is then returned.
|
||||
* Each feature/section should declare its own module and all related entities should register
|
||||
themselves on that module.
|
||||
|
||||
Angular creates a two way data-binding between the select element and the `orderProp` model.
|
||||
`orderProp` is then used as the input for the `orderBy` filter.
|
||||
|
||||
As we discussed in the section about data-binding and the repeater in step 3, whenever the model
|
||||
changes (for example because a user changes the order with the select drop down menu), Angular's
|
||||
data-binding will cause the view to automatically update. No bloated DOM manipulation code is
|
||||
necessary!
|
||||
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
Let's take the `phoneList` feature as an example. Previously, the `phoneList` component would
|
||||
register itself on the `phonecatApp` module:
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope) {
|
||||
$scope.phones = [
|
||||
{'name': 'Nexus S',
|
||||
'snippet': 'Fast just got faster with Nexus S.',
|
||||
'age': 1},
|
||||
{'name': 'Motorola XOOM™ with Wi-Fi',
|
||||
'snippet': 'The Next, Next Generation tablet.',
|
||||
'age': 2},
|
||||
{'name': 'MOTOROLA XOOM™',
|
||||
'snippet': 'The Next, Next Generation tablet.',
|
||||
'age': 3}
|
||||
];
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
});
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
component('phoneList', ...);
|
||||
```
|
||||
|
||||
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
|
||||
record. This property is used to order phones by age.
|
||||
Similarly, the accompanying spec file loads the `phonecatApp` module before each test (because
|
||||
that's where our component is registered). Now, imagine that we need a list of phones on another
|
||||
project that we are working on. Thanks to our modular architecture, we don't have to reinvent the
|
||||
wheel; we simply copy the `phone-list/` directory on our other project and add the necessary script
|
||||
tags in our `index.html` file and we are done, right?
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set a default value here, the `orderBy` filter would remain uninitialized until our
|
||||
user picked an option from the drop down menu.
|
||||
Well, not so fast. The new project doesn't know anything about a `phonecatApp` module. So, we would
|
||||
have to replace all references to `phonecatApp` with the name of this project's main module. As you
|
||||
can imagine this is both laborious and error-prone.
|
||||
|
||||
This is a good time to talk about two-way data-binding. Notice that when the app is loaded in the
|
||||
browser, "Newest" is selected in the drop down menu. This is because we set `orderProp` to `'age'`
|
||||
in the controller. So the binding works in the direction from our model to the UI. Now if you
|
||||
select "Alphabetically" in the drop down menu, the model will be updated as well and the phones
|
||||
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI
|
||||
to the model.
|
||||
Yeah, you guessed it: There is a better way!
|
||||
|
||||
Each feature/section, will declare its own module and have all related entities registered there.
|
||||
The main module (`phonecatApp`) will declare a dependency on each feature/section module. Now,
|
||||
all it takes to reuse the same code on new project is copying the feature directory over and adding
|
||||
the feature module as a dependency in the new project's main module.
|
||||
|
||||
Here is what our `phoneList` feature will look like after this change:
|
||||
|
||||
## Test
|
||||
<br />
|
||||
**`/`:**
|
||||
|
||||
The changes we made should be verified with both a unit test and an end-to-end test. Let's look at
|
||||
the unit test first.
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.module.js
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
app.module.js
|
||||
```
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.module.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
// Define the `phoneList` module
|
||||
angular.module('phoneList', []);
|
||||
```
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl;
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
```js
|
||||
// Register the `phoneList` component on the `phoneList` module,
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {...});
|
||||
```
|
||||
|
||||
beforeEach(inject(function($controller) {
|
||||
scope = {};
|
||||
ctrl = $controller('PhoneListCtrl', {$scope:scope});
|
||||
}));
|
||||
<br />
|
||||
**`app/app.module.js`:**<br />
|
||||
_(since `app/app.js` now only contains the main module declaration, we gave it a `.module` suffix)_
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
expect(scope.phones.length).toBe(3);
|
||||
});
|
||||
```js
|
||||
// Define the `phonecatApp` module
|
||||
angular.module('phonecatApp', [
|
||||
// ...which depends on the `phoneList` module
|
||||
'phoneList'
|
||||
]);
|
||||
```
|
||||
|
||||
By passing `phoneList` inside the dependencies array when defining the `phonecatApp` module, Angular
|
||||
will make all entities registered on `phoneList` available on `phonecatApp` as well.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Don't forget to also update your `index.html` adding a `<script>` tag for each JacaScript file
|
||||
we have created. This might seem tedious, but is totally worth it.
|
||||
</p>
|
||||
<p>
|
||||
In a production-ready application, you would concatenate and minify all your JavaScript files
|
||||
anyway (for performance reasons), so this won't be an issue any more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Note that files defining a module (i.e. `.module.js`) need to be included before other files that
|
||||
add features (e.g. components, controllers, services, filters) to that module.
|
||||
</div>
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
## External Templates
|
||||
|
||||
Since we are at refactoring, let's do one more thing. As we learned, components have templates,
|
||||
which are basically fragments of HTML code that dictate how our data is laid out and presented to
|
||||
the user. In {@link step_03 step 3}, we saw how we can specify the template for a component as a
|
||||
string using the `template` property of the CDO (Component Definition Object).
|
||||
Having HTML code in a string isn't ideal, especially for bigger templates. It would be much better,
|
||||
if we could have our HTML code in `.html` files. This way, we would get all the support our
|
||||
IDE/editor has to offer (e.g. HTML-specific color-highlighting and auto-completion) and also keep
|
||||
our component definitions cleaner.
|
||||
|
||||
So, while it's perfectly fine to keep our component templates inline (using the `template` property
|
||||
of the CDO), we are going to use an external template for our `phoneList` component. In order to
|
||||
denote that we are using an external template, we use the `templateUrl` property and specify the URL
|
||||
that our template will be loaded from. Since we want to keep our template close to where the
|
||||
component is defined, we place it inside `app/phone-list/`.
|
||||
|
||||
We copied the contents of the `template` property (the HTML code) into
|
||||
`app/phone-list/phone-list.template.html` and modified our CDO like this:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
// Note: The URL is relative to our `index.html` file
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ...
|
||||
});
|
||||
```
|
||||
|
||||
At runtime, when Angular needs to create an instance of the `phoneList` component, it will make an
|
||||
HTTP request to get the template from `app/phone-list/phone-list.template.html`.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Keeping inline with our convention, we will be using the `.template` suffix for external
|
||||
templates. Another common convention is to just have the `.html` extension
|
||||
(e.g. `phone-list.html`).
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Using an external template like this, will result in more HTTP requests to the server (one for
|
||||
each external template). Although Angular takes care not to make extraneous requests (e.g.
|
||||
fetching the templates lazily, caching the results, etc), additional requests do have a cost
|
||||
(especially on mobile devices and data-plan connections).
|
||||
</p>
|
||||
<p>
|
||||
Luckily, there are ways to avoid the extra costs (while still keeping your templates external).
|
||||
A detailed discussion of the subject is outside the scope of this tutorial, but you can take a
|
||||
look at the {@link ng.$templateRequest $templateRequest} and
|
||||
{@link ng.$templateCache $templateCache} services for more info on how Angular manages external
|
||||
templates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
## Final Directory/File Layout
|
||||
|
||||
After all the refactorings that took place, this is how our application looks from the outside:
|
||||
|
||||
<br />
|
||||
**`/`:**
|
||||
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
phone-list.module.js
|
||||
phone-list.template.html
|
||||
app.css
|
||||
app.module.js
|
||||
index.html
|
||||
```
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Since this was just a refactoring step (no actual code addition/deletions), we shouldn't need to
|
||||
change much (if anything) as far as our specs are concerned.
|
||||
|
||||
One thing that we can (and should) change is the name of the module to be loaded before each test in
|
||||
`app/phone-list/phone-list.component.spec.js`. We don't need to pull in the whole `phonecatApp`
|
||||
module (which will soon grow to depend on more stuff). All we want to test is already included in
|
||||
the much smaller `phoneList` module, so it suffices to just load that.
|
||||
This is one extra benefit that we get out of our modular architecture for free.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
If not already done so, run the tests (using the `npm test` command) and verify that they still
|
||||
pass.
|
||||
|
||||
The unit test now verifies that the default ordering property is set.
|
||||
<div class="alert alert-success">
|
||||
One of the great things about tests is the confidence they provide, when refactoring your
|
||||
application. It's easy to break something as you start moving files around and re-arranging
|
||||
modules. Having good test coverage is the quickest, easiest and most reliable way of knowing that
|
||||
your application will continue to work as expected.
|
||||
</div>
|
||||
|
||||
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
|
||||
shared by all tests in the parent `describe` block.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs)</pre>
|
||||
|
||||
|
||||
Let's turn our attention to the end-to-end test.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
it('should be possible to control phone order via the drop down select box', function() {
|
||||
|
||||
var phoneNameColumn = element.all(by.repeater('phone in phones').column('phone.name'));
|
||||
var query = element(by.model('query'));
|
||||
|
||||
function getNames() {
|
||||
return phoneNameColumn.map(function(elm) {
|
||||
return elm.getText();
|
||||
});
|
||||
}
|
||||
|
||||
query.sendKeys('tablet'); //let's narrow the dataset to make the test assertions shorter
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
"Motorola XOOM\u2122 with Wi-Fi",
|
||||
"MOTOROLA XOOM\u2122"
|
||||
]);
|
||||
|
||||
element(by.model('orderProp')).element(by.css('option[value="name"]')).click();
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
"MOTOROLA XOOM\u2122",
|
||||
"Motorola XOOM\u2122 with Wi-Fi"
|
||||
]);
|
||||
});...
|
||||
```
|
||||
|
||||
The end-to-end test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
# Experiments
|
||||
|
||||
* In the `PhoneListCtrl` controller, remove the statement that sets the `orderProp` value and
|
||||
you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down list and the
|
||||
ordering will default to unordered/natural order.
|
||||
|
||||
* Add an `{{orderProp}}` binding into the `index.html` template to display its current value as
|
||||
text.
|
||||
|
||||
* Reverse the sort order by adding a `-` symbol before the sorting value: `<option value="-age">Oldest</option>`
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have added list sorting and tested the app, go to {@link step_05 step 5} to learn
|
||||
about Angular services and how Angular uses dependency injection.
|
||||
Even if we didn't add any new and exciting functionality to our application, we have made a great
|
||||
step towards a well-architected and maintainable application. Time to spice things up. Let's go to
|
||||
{@link step_05 step 5} to learn how to add full-text search to the application.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="4"></ul>
|
||||
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
[styleguide]: https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md
|
||||
|
||||
@@ -1,277 +1,168 @@
|
||||
@ngdoc tutorial
|
||||
@name 5 - XHRs & Dependency Injection
|
||||
@name 5 - Filtering Repeaters
|
||||
@step 5
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="5"></ul>
|
||||
|
||||
|
||||
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
|
||||
from our server using one of Angular's built-in {@link guide/services services} called {@link
|
||||
ng.$http $http}. We will use Angular's {@link guide/di dependency
|
||||
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
|
||||
We did a lot of work in laying a foundation for the app in the previous steps, 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 (E2E) test, because a good E2E test is a good friend. It stays with your app, keeps an
|
||||
eye on it, and quickly detects regressions.
|
||||
|
||||
* 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.
|
||||
|
||||
* There is now a list of 20 phones, loaded from the server.
|
||||
|
||||
<div doc-tutorial-reset="5"></div>
|
||||
|
||||
|
||||
## Data
|
||||
The `app/phones/phones.json` file in your project is a dataset that contains a larger list of phones
|
||||
stored in the JSON format.
|
||||
## Component Controller
|
||||
|
||||
Following is a sample of the file:
|
||||
We made no changes to the component's controller.
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
## Component Template
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
Search: <input ng-model="$ctrl.query" />
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We added a standard HTML `<input>` tag and used Angular's {@link ng.filter:filter filter} function
|
||||
to process the input for the {@link ngRepeat ngRepeat} directive.
|
||||
|
||||
## Controller
|
||||
By virtue of the {@link ngModel ngModel} 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:
|
||||
|
||||
We'll use Angular's {@link ng.$http $http} service in our controller to make an HTTP
|
||||
request to your web server to fetch the data in the `app/phones/phones.json` file. `$http` is just
|
||||
one of several built-in {@link guide/services Angular services} that handle common operations
|
||||
in web apps. Angular injects these services for you where you need them.
|
||||
* Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the
|
||||
value of the input box to the data model variable specified with `ngModel` and keeps the two in
|
||||
sync.
|
||||
|
||||
Services are managed by Angular's {@link guide/di DI subsystem}. Dependency injection
|
||||
helps to make your web apps both well-structured (e.g., separate components for presentation, data,
|
||||
and control) and loosely coupled (dependencies between components are not resolved by the
|
||||
components themselves, but by the DI subsystem).
|
||||
In this code, the data that a user types into the input box (bound to **`$ctrl.query`**) is
|
||||
immediately available as a filter input in the list repeater
|
||||
(`phone in $ctrl.phones | filter:`**`$ctrl.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.
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
|
||||
* Use of the `filter` filter: The {@link ng.filter:filter filter} function uses the `$ctrl.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.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
In previous steps, we learned how to write and run unit tests. Unit tests are perfect for testing
|
||||
controllers and other parts of our application written in JavaScript, but they can't easily
|
||||
test templates, DOM manipulation or interoperability of components and services. For these, an
|
||||
end-to-end (E2E) test is a much better choice.
|
||||
|
||||
The search feature was fully implemented via templates and data-binding, so we'll write our first
|
||||
E2E test, to verify that the feature works.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
describe('PhoneCat Application', function() {
|
||||
|
||||
describe('phoneList', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html');
|
||||
});
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
var phoneList = element.all(by.repeater('phone in $ctrl.phones'));
|
||||
var query = element(by.model('$ctrl.query'));
|
||||
|
||||
expect(phoneList.count()).toBe(3);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
});
|
||||
```
|
||||
|
||||
`$http` makes an HTTP GET request to our web server, asking for `phones/phones.json` (the url is
|
||||
relative to our `index.html` file). The server responds by providing the data in the json file.
|
||||
(The response might just as well have been dynamically generated by a backend server. To the
|
||||
browser and our app, they both look the same. For the sake of simplicity, we used a json file in this
|
||||
tutorial.)
|
||||
|
||||
The `$http` service returns a {@link ng.$q promise object} with a `success`
|
||||
method. We call this method to handle the asynchronous response and assign the phone data to the
|
||||
scope controlled by this controller, as a model called `phones`. Notice that Angular detected the
|
||||
json response and parsed it for us!
|
||||
|
||||
To use a service in Angular, you simply declare the names of the dependencies you need as arguments
|
||||
to the controller's constructor function, as follows:
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}
|
||||
|
||||
Angular's dependency injector provides services to your controller when the controller is being
|
||||
constructed. The dependency injector also takes care of creating any transitive dependencies the
|
||||
service may have (services often depend upon other services).
|
||||
|
||||
Note that the names of arguments are significant, because the injector uses these to look up the
|
||||
dependencies.
|
||||
This test verifies that the search box and the repeater are correctly wired together. Notice how
|
||||
easy it is to write E2E tests in Angular. Although this example is for a simple test, it really is
|
||||
that easy to set up any functional, readable, E2E test.
|
||||
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
## Running E2E Tests with Protractor
|
||||
|
||||
Even though the syntax of this test looks very much like our controller unit test written with
|
||||
Jasmine, the E2E test uses APIs of [Protractor][protractor]. Read about the Protractor APIs in the
|
||||
[Protractor API Docs][protractor-docs].
|
||||
|
||||
### `$` Prefix Naming Convention
|
||||
|
||||
You can create your own services, and in fact we will do exactly that in step 11. As a naming
|
||||
convention, Angular's built-in services, Scope methods and a few other Angular APIs have a `$`
|
||||
prefix in front of the name.
|
||||
|
||||
The `$` prefix is there to namespace Angular-provided services.
|
||||
To prevent collisions it's best to avoid naming your services and models anything that begins with a `$`.
|
||||
|
||||
If you inspect a Scope, you may also notice some properties that begin with `$$`. These
|
||||
properties are considered private, and should not be accessed or modified.
|
||||
|
||||
|
||||
### A Note on Minification
|
||||
|
||||
Since Angular infers the controller's dependencies from the names of arguments to the controller's
|
||||
constructor function, if you were to [minify](http://goo.gl/SAnnsm) the JavaScript code for
|
||||
`PhoneListCtrl` controller, all of its function arguments would be minified as well, and the
|
||||
dependency injector would not be able to identify services correctly.
|
||||
|
||||
We can overcome this problem by annotating the function with the names of the dependencies, provided
|
||||
as strings, which will not get minified. There are two ways to provide these injection annotations:
|
||||
|
||||
* Create a `$inject` property on the controller function which holds an array of strings.
|
||||
Each string in the array is the name of the service to inject for the corresponding parameter.
|
||||
In our example, we would write:
|
||||
|
||||
```js
|
||||
function PhoneListCtrl($scope, $http) {...}
|
||||
PhoneListCtrl.$inject = ['$scope', '$http'];
|
||||
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
|
||||
```
|
||||
|
||||
* Use an inline annotation where, instead of just providing the function, you provide an array.
|
||||
This array contains a list of the service names, followed by the function itself.
|
||||
|
||||
```js
|
||||
function PhoneListCtrl($scope, $http) {...}
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
|
||||
```
|
||||
|
||||
Both of these methods work with any function that can be injected by Angular, so it's up to your
|
||||
project's style guide to decide which one you use.
|
||||
|
||||
When using the second method, it is common to provide the constructor function inline as an
|
||||
anonymous function when registering the controller:
|
||||
|
||||
```js
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
|
||||
```
|
||||
|
||||
From this point onward, we're going to use the inline method in the tutorial. With that in mind,
|
||||
let's add the annotations to our `PhoneListCtrl`:
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
|
||||
function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
Because we started using dependency injection and our controller has dependencies, constructing the
|
||||
controller in our tests is a bit more complicated. We could use the `new` operator and provide the
|
||||
constructor with some kind of fake `$http` implementation. However, Angular provides a mock `$http`
|
||||
service that we can use in unit tests. We configure "fake" responses to server requests by calling
|
||||
methods on a service called `$httpBackend`:
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
// Load our app module definition before each test.
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
|
||||
// This allows us to inject a service but then attach it to a variable
|
||||
// with the same name as the service in order to avoid a name conflict.
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneListCtrl', {$scope: scope});
|
||||
}));
|
||||
```
|
||||
|
||||
Note: Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two helper
|
||||
methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we'll
|
||||
use to access and configure the injector.
|
||||
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
* We used the `inject` helper method to inject instances of
|
||||
{@link ng.$rootScope $rootScope},
|
||||
{@link ng.$controller $controller} and
|
||||
{@link ng.$httpBackend $httpBackend} services into the Jasmine's `beforeEach`
|
||||
function. These instances come from an injector which is recreated from scratch for every single
|
||||
test. This guarantees that each test starts from a well known starting point and each test is
|
||||
isolated from the work done in other tests.
|
||||
|
||||
* We created a new scope for our controller by calling `$rootScope.$new()`
|
||||
|
||||
* We called the injected `$controller` function passing the name of the `PhoneListCtrl` controller
|
||||
and the created scope as parameters.
|
||||
|
||||
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
|
||||
we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an
|
||||
incoming request from the controller. To do this we:
|
||||
|
||||
* Request `$httpBackend` service to be injected into our `beforeEach` function. This is a mock
|
||||
version of the service that in a production environment facilitates all XHR and JSONP requests.
|
||||
The mock version of this service allows you to write tests without having to deal with
|
||||
native APIs and the global state associated with them — both of which make testing a nightmare.
|
||||
|
||||
* Use the `$httpBackend.expectGET` method to train the `$httpBackend` service to expect an incoming
|
||||
HTTP request and tell it what to respond with. Note that the responses are not returned until we call
|
||||
the `$httpBackend.flush` method.
|
||||
|
||||
Now we will make assertions to verify that the `phones` model doesn't exist on `scope` before
|
||||
the response is received:
|
||||
|
||||
```js
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(scope.phones).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phones).toEqual([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
```
|
||||
|
||||
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
|
||||
promise returned by the `$http` service to be resolved with the trained response. See
|
||||
'Flushing HTTP requests' in the {@link ngMock.$httpBackend mock $httpBackend} documentation for
|
||||
a full explanation of why this is necessary.
|
||||
|
||||
* We make the assertions, verifying that the phone model now exists on the scope.
|
||||
|
||||
Finally, we verify that the default value of `orderProp` is set correctly:
|
||||
|
||||
```js
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)</pre>
|
||||
Much like Karma is the test runner for unit tests, we use Protractor to run E2E tests. Try it with
|
||||
`npm run protractor`. E2E tests take time, so unlike with unit tests, Protractor will exit after the
|
||||
tests run and will not automatically rerun the test suite on every file change.
|
||||
To rerun the test suite, execute `npm run protractor` again.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** In order for protractor to access and run tests against your application, it must be
|
||||
served via a web server. In a different terminal/command line window, run `npm start` to fire up
|
||||
the web server.
|
||||
</div>
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* At the bottom of `index.html`, add a `<pre>{{phones | filter:query | orderBy:orderProp | json}}</pre>`
|
||||
binding to see the list of phones displayed in json format.
|
||||
<div></div>
|
||||
|
||||
* In the `PhoneListCtrl` controller, pre-process the http response by limiting the number of phones
|
||||
to the first 5 in the list. Use the following code in the `$http` callback:
|
||||
* Display the current value of the `query` model by adding a `{{$ctrl.query}}` binding into the
|
||||
`phone-list.template.html` template and see how it changes, when you type in the input box.
|
||||
|
||||
$scope.phones = data.splice(0, 5);
|
||||
You might also try to add the `{{$ctrl.query}}` binding to `index.html`. 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 `<phone-list>` component.<br />
|
||||
Component isolation at work!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
|
||||
injection), go to {@link step_06 step 6}, where you will add some
|
||||
thumbnail images of phones and some links.
|
||||
We have now added full-text search and included a test to verify that it works! Now let's go on to
|
||||
{@link step_06 step 6} to learn how to add sorting capabilities to the PhoneCat application.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="5"></ul>
|
||||
|
||||
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[protractor-docs]: https://angular.github.io/protractor/#/api
|
||||
|
||||
@@ -1,108 +1,241 @@
|
||||
@ngdoc tutorial
|
||||
@name 6 - Templating Links & Images
|
||||
@name 6 - Two-way Data Binding
|
||||
@step 6
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="6"></ul>
|
||||
|
||||
|
||||
In this step, you will add thumbnail images for the phones in the phone list, and links that, for
|
||||
now, will go nowhere. In subsequent steps you will use the links to display additional information
|
||||
about the phones in the catalog.
|
||||
In this step, we will add a feature to let our users control the order of the items in the phone
|
||||
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
|
||||
the repeater, and letting the data binding magic do the rest of the work.
|
||||
|
||||
* In addition to the search box, the application displays a drop-down menu that allows users to
|
||||
control the order in which the phones are listed.
|
||||
|
||||
* There are now links and images of the phones in the list.
|
||||
|
||||
<div doc-tutorial-reset="6"></div>
|
||||
|
||||
## Data
|
||||
|
||||
Note that the `phones.json` file contains unique IDs and image URLs for each of the phones. The
|
||||
URLs point to the `app/img/phones/` directory.
|
||||
## Component Template
|
||||
|
||||
__`app/phones/phones.json`__ (sample snippet):
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
<p>
|
||||
Search:
|
||||
<input ng-model="$ctrl.query">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Sort by:
|
||||
<select ng-model="$ctrl.orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar double-curly brace binding in the `href` attribute values. In step 2, we added the
|
||||
`{{phone.name}}` binding as the element content. In this step the `{{phone.id}}` binding is used in
|
||||
the element attribute.
|
||||
We made the following changes to the `phone-list.template.html` template:
|
||||
|
||||
We also added phone images next to each record using an image tag with the {@link
|
||||
ng.directive:ngSrc ngSrc} directive. That directive prevents the
|
||||
browser from treating the Angular `{{ expression }}` markup literally, and initiating a request to
|
||||
an invalid URL `http://localhost:8000/app/{{phone.imageUrl}}`, which it would have done if we had
|
||||
only specified an attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`).
|
||||
Using the `ngSrc` directive prevents the browser from making an http request to an invalid location.
|
||||
* First, we added a `<select>` element bound to `$ctrl.orderProp`, so that our users can pick from
|
||||
the two provided sorting options.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_06.png">
|
||||
|
||||
* We then chained the `filter` filter with the {@link orderBy orderBy} filter to further process the
|
||||
input for the repeater. `orderBy` is a filter that takes an input array, copies it and reorders
|
||||
the copy which is then returned.
|
||||
|
||||
Angular creates a two way data-binding between the select element and the `$ctrl.orderProp` model.
|
||||
`$ctrl.orderProp` is then used as the input for the `orderBy` filter.
|
||||
|
||||
As we discussed in the section about data-binding and the repeater in {@link step_05 step 5},
|
||||
whenever the model changes (for example because a user changes the order with the select drop-down
|
||||
menu), Angular's data-binding will cause the view to automatically update. No bloated DOM
|
||||
manipulation code is necessary!
|
||||
|
||||
|
||||
## Test
|
||||
## Component Controller
|
||||
|
||||
__`test/e2e/scenarios.js`__:
|
||||
<br />
|
||||
**`app/phone-list/phone-list.components.js`:**
|
||||
|
||||
```js
|
||||
...
|
||||
it('should render phone specific links', function() {
|
||||
var query = element(by.model('query'));
|
||||
query.sendKeys('nexus');
|
||||
element.all(by.css('.phones li a')).first().click();
|
||||
browser.getLocationAbsUrl().then(function(url) {
|
||||
expect(url).toBe('/phones/nexus-s');
|
||||
});
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: function PhoneListController() {
|
||||
this.phones = [
|
||||
{
|
||||
name: 'Nexus S',
|
||||
snippet: 'Fast just got faster with Nexus S.',
|
||||
age: 1
|
||||
}, {
|
||||
name: 'Motorola XOOM™ with Wi-Fi',
|
||||
snippet: 'The Next, Next Generation tablet.',
|
||||
age: 2
|
||||
}, {
|
||||
name: 'MOTOROLA XOOM™',
|
||||
snippet: 'The Next, Next Generation tablet.',
|
||||
age: 3
|
||||
}
|
||||
];
|
||||
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
We added a new end-to-end test to verify that the app is generating correct links to the phone
|
||||
views that we will implement in the upcoming steps.
|
||||
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
|
||||
record. This property is used to order the phones by age.
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set a default value here, the `orderBy` filter would remain uninitialized until the user
|
||||
picked an option from the drop-down menu.
|
||||
|
||||
This is a good time to talk about two-way data-binding. Notice that when the application is loaded
|
||||
in the browser, "Newest" is selected in the drop-down menu. This is because we set `orderProp` to
|
||||
`'age'` in the controller. So the binding works in the direction from our model to the UI. Now if
|
||||
you select "Alphabetically" in the drop-down menu, the model will be updated as well and the phones
|
||||
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI to
|
||||
the model.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
The changes we made should be verified with both a unit test and an E2E test. Let's look at the unit
|
||||
test first.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
// Test the controller
|
||||
describe('PhoneListController', function() {
|
||||
var ctrl;
|
||||
|
||||
beforeEach(inject(function($componentController) {
|
||||
ctrl = $componentController('phoneList');
|
||||
}));
|
||||
|
||||
it('should create a `phones` model with 3 phones', function() {
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should set a default value for the `orderProp` model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
The unit test now verifies that the default ordering property is set.
|
||||
|
||||
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
|
||||
shared by all tests in the parent `describe` block.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 2 of 2 SUCCESS (0.136 secs / 0.08 secs)
|
||||
```
|
||||
|
||||
Let's turn our attention to the E2E tests.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneCat Application', function() {
|
||||
|
||||
describe('phoneList', function() {
|
||||
|
||||
...
|
||||
|
||||
it('should be possible to control phone order via the drop-down menu', function() {
|
||||
var queryField = element(by.model('$ctrl.query'));
|
||||
var orderSelect = element(by.model('$ctrl.orderProp'));
|
||||
var nameOption = orderSelect.element(by.css('option[value="name"]'));
|
||||
var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name'));
|
||||
|
||||
function getNames() {
|
||||
return phoneNameColumn.map(function(elem) {
|
||||
return elem.getText();
|
||||
});
|
||||
}
|
||||
|
||||
queryField.sendKeys('tablet'); // Let's narrow the dataset to make the assertions shorter
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
'Motorola XOOM\u2122 with Wi-Fi',
|
||||
'MOTOROLA XOOM\u2122'
|
||||
]);
|
||||
|
||||
nameOption.click();
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
'MOTOROLA XOOM\u2122',
|
||||
'Motorola XOOM\u2122 with Wi-Fi'
|
||||
]);
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The E2E test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Replace the `ng-src` directive with a plain old `src` attribute. Using tools such as Firebug,
|
||||
or Chrome's Web Inspector, or inspecting the webserver access logs, confirm that the app is indeed
|
||||
making an extraneous request to `/app/%7B%7Bphone.imageUrl%7D%7D` (or
|
||||
`/app/{{phone.imageUrl}}`).
|
||||
<div></div>
|
||||
|
||||
The issue here is that the browser will fire a request for that invalid image address as soon as
|
||||
it hits the `img` tag, which is before Angular has a chance to evaluate the expression and inject
|
||||
the valid address.
|
||||
* In the `phoneList` component's controller, remove the statement that sets the `orderProp` value
|
||||
and you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down
|
||||
list and the ordering will default to unordered/natural order.
|
||||
|
||||
* Add a `{{$ctrl.orderProp}}` binding into the `phone-list.template.html` template to display its
|
||||
current value as text.
|
||||
|
||||
* Reverse the sort order by adding a `-` symbol before the sorting value:
|
||||
`<option value="-age">Oldest</option>`
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have added phone images and links, go to {@link step_07 step 7} to learn about Angular
|
||||
layout templates and how Angular makes it easy to create applications that have multiple views.
|
||||
Now that you have added list sorting and tested the application, go to {@link step_07 step 7} to
|
||||
learn about Angular services and how Angular uses dependency injection.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="6"></ul>
|
||||
|
||||
@@ -1,380 +1,312 @@
|
||||
@ngdoc tutorial
|
||||
@name 7 - Routing & Multiple Views
|
||||
@name 7 - XHR & Dependency Injection
|
||||
@step 7
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="7"></ul>
|
||||
|
||||
|
||||
In this step, you will learn how to create a layout template and how to build an app that has
|
||||
multiple views by adding routing, using an Angular module called 'ngRoute'.
|
||||
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
|
||||
from our server using one of Angular's built-in {@link guide/services services} called
|
||||
{@link ng.$http $http}. We will use Angular's {@link guide/di dependency injection (DI)} to provide
|
||||
the service to the `phoneList` component's controller.
|
||||
|
||||
* There is now a list of 20 phones, loaded from the server.
|
||||
|
||||
* When you now navigate to `app/index.html`, you are redirected to `app/index.html/#/phones`
|
||||
and the phone list appears in the browser.
|
||||
* When you click on a phone link, the url changes to that specific phone and the stub of a
|
||||
phone detail page is displayed.
|
||||
|
||||
<div doc-tutorial-reset="7"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The routing functionality added by this step is provided by angular in the `ngRoute` module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
## Data
|
||||
|
||||
We are using [Bower][bower] to install client-side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
The `app/phones/phones.json` file in our project is a dataset that contains a larger list of phones,
|
||||
stored in JSON format.
|
||||
|
||||
Following is a sample of the file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "1.4.x"
|
||||
}
|
||||
}
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
The new dependency `"angular-route": "1.4.x"` tells bower to install a version of the
|
||||
angular-route component that is compatible with version 1.4.x. We must tell bower to download
|
||||
and install this dependency.
|
||||
|
||||
If you have bower installed globally, then you can run `bower install` but for this project, we have
|
||||
preconfigured npm to run bower install for us:
|
||||
## Component Controller
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
We will use Angular's {@link ng.$http $http} service in our controller for making an HTTP request to
|
||||
our web server to fetch the data in the `app/phones/phones.json` file. `$http` is just one of
|
||||
several built-in {@link guide/services Angular services} that handle common operations in web
|
||||
applications. Angular injects these services for you, right where you need them.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
|
||||
problem with the `bower install` due to a conflict between the versions of angular.js that need to
|
||||
be installed. If you get this then simply delete your `app/bower_components` folder before running
|
||||
`npm install`.
|
||||
</div>
|
||||
Services are managed by Angular's {@link guide/di DI subsystem}. Dependency injection helps to make
|
||||
your web applications both well-structured (e.g. separate entities for presentation, data, and
|
||||
control) and loosely coupled (dependencies between entities are not resolved by the entities
|
||||
themselves, but by the DI subsystem). As a result, applications are easier to test as well.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
|
||||
preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
|
||||
## Multiple Views, Routing and Layout Template
|
||||
|
||||
Our app is slowly growing and becoming more complex. Before step 7, the app provided our users with
|
||||
a single view (the list of all phones), and all of the template code was located in the
|
||||
`index.html` file. The next step in building the app is to add a view that will show detailed
|
||||
information about each of the devices in our list.
|
||||
|
||||
To add the detailed view, we could expand the `index.html` file to contain template code for both
|
||||
views, but that would get messy very quickly. Instead, we are going to turn the `index.html`
|
||||
template into what we call a "layout template". This is a template that is common for all views in
|
||||
our application. Other "partial templates" are then included into this layout template depending on
|
||||
the current "route" — the view that is currently displayed to the user.
|
||||
|
||||
Application routes in Angular are declared via the {@link ngRoute.$routeProvider $routeProvider},
|
||||
which is the provider of the {@link ngRoute.$route $route service}. This service makes it easy to
|
||||
wire together controllers, view templates, and the current URL location in the browser. Using this
|
||||
feature, we can implement [deep linking](http://en.wikipedia.org/wiki/Deep_linking), which lets us
|
||||
utilize the browser's history (back and forward navigation) and bookmarks.
|
||||
|
||||
|
||||
### A Note About DI, Injector and Providers
|
||||
|
||||
As you {@link tutorial/step_05 noticed}, {@link guide/di dependency injection} (DI) is at the core of
|
||||
AngularJS, so it's important for you to understand a thing or two about how it works.
|
||||
|
||||
When the application bootstraps, Angular creates an injector that will be used to find and inject all
|
||||
of the services that are required by your app. The injector itself doesn't know anything about what
|
||||
`$http` or `$route` services do. In fact, the injector doesn't even know about the existence of these services
|
||||
unless it is configured with proper module definitions.
|
||||
|
||||
The injector only carries out the following steps :
|
||||
|
||||
* load the module definition(s) that you specify in your app
|
||||
* register all Providers defined in these module definitions
|
||||
* when asked to do so, inject a specified function and any necessary dependencies (services) that
|
||||
it lazily instantiates via their Providers.
|
||||
|
||||
Providers are objects that provide (create) instances of services and expose configuration APIs
|
||||
that can be used to control the creation and runtime behavior of a service. In case of the `$route`
|
||||
service, the `$routeProvider` exposes APIs that allow you to define routes for your application.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Providers can only be injected into `config` functions. Thus you could not inject
|
||||
`$routeProvider` into `PhoneListCtrl`.
|
||||
</div>
|
||||
|
||||
Angular modules solve the problem of removing global state from the application and provide a way
|
||||
of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try to
|
||||
solve the problem of script load ordering or lazy script fetching. These goals are totally independent and
|
||||
both module systems can live side by side and fulfill their goals.
|
||||
|
||||
To deepen your understanding of DI on Angular, see
|
||||
[Understanding Dependency Injection](https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection).
|
||||
|
||||
## Template
|
||||
|
||||
The `$route` service is usually used in conjunction with the {@link ngRoute.directive:ngView
|
||||
ngView} directive. The role of the `ngView` directive is to include the view template for the current
|
||||
route into the layout template. This makes it a perfect fit for our `index.html` template.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** Starting with AngularJS version 1.2, `ngRoute` is in its own module and must be loaded by
|
||||
loading the additional `angular-route.js` file, which we download via Bower above.
|
||||
</div>
|
||||
|
||||
__`app/index.html`:__
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en" ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
We have added two new `<script>` tags in our index file to load up extra JavaScript files into our
|
||||
application:
|
||||
|
||||
- `angular-route.js` : defines the Angular `ngRoute` module, which provides us with routing.
|
||||
- `app.js` : this file now holds the root module of our application.
|
||||
|
||||
Note that we removed most of the code in the `index.html` template and replaced it with a single
|
||||
line containing a div with the `ng-view` attribute. The code that we removed was placed into the
|
||||
`phone-list.html` template:
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
<div style="display:none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_07_final.png">
|
||||
</div>
|
||||
|
||||
We also added a placeholder template for the phone details view:
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
|
||||
```html
|
||||
TBD: detail view for <span>{{phoneId}}</span>
|
||||
```
|
||||
|
||||
Note how we are using the `phoneId` expression which will be defined in the `PhoneDetailCtrl` controller.
|
||||
|
||||
## The App Module
|
||||
|
||||
To improve the organization of the app, we are making use of Angular's `ngRoute` module and we've
|
||||
moved the controllers into their own module `phonecatControllers` (as shown below).
|
||||
|
||||
We added `angular-route.js` to `index.html` and created a new `phonecatControllers` module in
|
||||
`controllers.js`. That's not all we need to do to be able to use their code, however. We also have
|
||||
to add the modules as dependencies of our app. By listing these two modules as dependencies of
|
||||
`phonecatApp`, we can use the directives and services they provide.
|
||||
|
||||
|
||||
__`app/js/app.js`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js:`**
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
'phonecatControllers'
|
||||
]);
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: function PhoneListController($http) {
|
||||
var self = this;
|
||||
self.orderProp = 'age';
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Notice the second argument passed to `angular.module`, `['ngRoute', 'phonecatControllers']`. This
|
||||
array lists the modules that `phonecatApp` depends on.
|
||||
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
phonecatApp.config(['$routeProvider',
|
||||
function($routeProvider) {
|
||||
$routeProvider.
|
||||
when('/phones', {
|
||||
templateUrl: 'partials/phone-list.html',
|
||||
controller: 'PhoneListCtrl'
|
||||
}).
|
||||
when('/phones/:phoneId', {
|
||||
templateUrl: 'partials/phone-detail.html',
|
||||
controller: 'PhoneDetailCtrl'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/phones'
|
||||
});
|
||||
}]);
|
||||
```
|
||||
|
||||
Using the `phonecatApp.config()` method, we request the `$routeProvider` to be injected into our
|
||||
config function and use the {@link ngRoute.$routeProvider#when `$routeProvider.when()`} method to
|
||||
define our routes.
|
||||
|
||||
Our application routes are defined as follows:
|
||||
|
||||
* `when('/phones')`: The phone list view will be shown when the URL hash fragment is `/phones`. To
|
||||
construct this view, Angular will use the `phone-list.html` template and the `PhoneListCtrl`
|
||||
controller.
|
||||
|
||||
* `when('/phones/:phoneId')`: The phone details view will be shown when the URL hash fragment
|
||||
matches '/phones/:phoneId', where `:phoneId` is a variable part of the URL. To construct the phone
|
||||
details view, Angular will use the `phone-detail.html` template and the `PhoneDetailCtrl`
|
||||
controller.
|
||||
|
||||
* `otherwise({redirectTo: '/phones'})`: triggers a redirection to `/phones` when the browser
|
||||
address doesn't match either of our routes.
|
||||
|
||||
|
||||
We reused the `PhoneListCtrl` controller that we constructed in previous steps and we added a new,
|
||||
empty `PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the phone details view.
|
||||
|
||||
|
||||
Note the use of the `:phoneId` parameter in the second route declaration. The `$route` service uses
|
||||
the route declaration — `'/phones/:phoneId'` — as a template that is matched against the current
|
||||
URL. All variables defined with the `:` notation are extracted into the
|
||||
{@link ngRoute.$routeParams `$routeParams`} object.
|
||||
|
||||
|
||||
## Controllers
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers', []);
|
||||
|
||||
phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
|
||||
function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
|
||||
function($scope, $routeParams) {
|
||||
$scope.phoneId = $routeParams.phoneId;
|
||||
}]);
|
||||
```
|
||||
|
||||
Again, note that we created a new module called `phonecatControllers`. For small AngularJS
|
||||
applications, it's common to create just one module for all of your controllers if there are just a
|
||||
few. As your application grows, it is quite common to refactor your code into additional modules.
|
||||
For larger apps, you will probably want to create separate modules for each major feature of
|
||||
your app.
|
||||
`$http` makes an HTTP GET request to our web server, asking for `phones.json` (the URL is relative
|
||||
to our `index.html` file). The server responds by providing the data in the JSON file.
|
||||
(The response might just as well have been dynamically generated by a backend server. To the
|
||||
browser and our app, they both look the same. For the sake of simplicity, we will use JSON files
|
||||
in this tutorial.)
|
||||
|
||||
Because our example app is relatively small, we'll just add all of our controllers to the
|
||||
`phonecatControllers` module.
|
||||
The `$http` service returns a {@link ng.$q promise object}, which has a `then()` method. We call
|
||||
this method to handle the asynchronous response and assign the phone data to the controller, as a
|
||||
property called `phones`. Notice that Angular detected the JSON response and parsed it for us into
|
||||
the `data` property of the `response` object passed to our callback!
|
||||
|
||||
Since we are making the assignment of the `phones` property in a callback function, where the `this`
|
||||
value is not defined, we also introduce a local variable called `self` that points back to the
|
||||
controller instance.
|
||||
|
||||
## Test
|
||||
|
||||
To automatically verify that everything is wired properly, we wrote end-to-end tests that navigate
|
||||
to various URLs and verify that the correct view was rendered.
|
||||
To use a service in Angular, you simply declare the names of the dependencies you need as arguments
|
||||
to the controller's constructor function, as follows:
|
||||
|
||||
```js
|
||||
...
|
||||
it('should redirect index.html to index.html#/phones', function() {
|
||||
browser.get('app/index.html');
|
||||
browser.getLocationAbsUrl().then(function(url) {
|
||||
expect(url).toEqual('/phones');
|
||||
});
|
||||
function PhoneListController($http) {...}
|
||||
```
|
||||
|
||||
Angular's dependency injector provides services to your controller, when the controller is being
|
||||
constructed. The dependency injector also takes care of creating any transitive dependencies the
|
||||
service may have (services often depend upon other services).
|
||||
|
||||
Note that the names of arguments are significant, because the injector uses these to look up the
|
||||
dependencies.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
|
||||
|
||||
### `$`-prefix Naming Convention
|
||||
|
||||
You can create your own services, and in fact we will do exactly that a few steps down the road. As
|
||||
a naming convention, Angular's built-in services, Scope methods and a few other Angular APIs have a
|
||||
`$` prefix in front of the name.
|
||||
|
||||
The `$` prefix is there to namespace Angular-provided services. To prevent collisions it's best to
|
||||
avoid naming your services and models anything that begins with a `$`.
|
||||
|
||||
If you inspect a Scope, you may also notice some properties that begin with `$$`. These properties
|
||||
are considered private, and should not be accessed or modified.
|
||||
|
||||
|
||||
### A Note on Minification
|
||||
|
||||
Since Angular infers the controller's dependencies from the names of arguments to the controller's
|
||||
constructor function, if you were to [minify][minification] the JavaScript code for the
|
||||
`PhoneListController` controller, all of its function arguments would be minified as well, and the
|
||||
dependency injector would not be able to identify services correctly.
|
||||
|
||||
We can overcome this problem by annotating the function with the names of the dependencies, provided
|
||||
as strings, which will not get minified. There are two ways to provide these injection annotations:
|
||||
|
||||
* Create an `$inject` property on the controller function which holds an array of strings.
|
||||
Each string in the array is the name of the service to inject for the corresponding parameter.
|
||||
In our example, we would write:
|
||||
|
||||
```js
|
||||
function PhoneListController($http) {...}
|
||||
PhoneListController.$inject = ['$http'];
|
||||
...
|
||||
.component('phoneList', {..., controller: PhoneListController});
|
||||
```
|
||||
|
||||
* Use an inline annotation where, instead of just providing the function, you provide an array.
|
||||
This array contains a list of the service names, followed by the function itself as the last item
|
||||
of the array.
|
||||
|
||||
```js
|
||||
function PhoneListController($http) {...}
|
||||
...
|
||||
.component('phoneList', {..., controller: ['$http', PhoneListController]});
|
||||
```
|
||||
|
||||
Both of these methods work with any function that can be injected by Angular, so it's up to your
|
||||
project's style guide to decide which one you use.
|
||||
|
||||
When using the second method, it is common to provide the constructor function inline, when
|
||||
registering the controller:
|
||||
|
||||
```js
|
||||
.component('phoneList', {..., controller: ['$http', function PhoneListController($http) {...}]});
|
||||
```
|
||||
|
||||
From this point onwards, we are going to use the inline method in the tutorial. With that in mind,
|
||||
let's add the annotations to our `PhoneListController`:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ['$http',
|
||||
function PhoneListController($http) {
|
||||
var self = this;
|
||||
self.orderProp = 'age';
|
||||
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Because we started using dependency injection and our controller has dependencies, constructing the
|
||||
controller in our tests is a bit more complicated. We could use the `new` operator and provide the
|
||||
constructor with some kind of fake `$http` implementation. However, Angular provides a mock `$http`
|
||||
service that we can use in unit tests. We configure "fake" responses to server requests by calling
|
||||
methods on a service called `$httpBackend`:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
describe('controller', function() {
|
||||
var $httpBackend, ctrl;
|
||||
|
||||
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
|
||||
// This allows us to inject a service and assign it to a variable with the same name
|
||||
// as the service while avoiding a name conflict.
|
||||
beforeEach(inject(function($componentController, _$httpBackend_) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json')
|
||||
.respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
ctrl = $componentController('phoneList');
|
||||
}));
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
|
||||
describe('Phone list view', function() {
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones');
|
||||
});
|
||||
...
|
||||
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display placeholder page with phoneId', function() {
|
||||
expect(element(by.binding('phoneId')).getText()).toBe('nexus-s');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two
|
||||
helper methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we
|
||||
can use to access and configure the injector.
|
||||
</div>
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
* We used the `inject()` helper method to inject instances of
|
||||
{@link ngMock.$componentController $componentController} and {@link ng.$httpBackend $httpBackend}
|
||||
services into Jasmine's `beforeEach()` function. These instances come from an injector which is
|
||||
recreated from scratch for every single test. This guarantees that each test starts from a well
|
||||
known starting point and each test is isolated from the work done in other tests.
|
||||
|
||||
* We called the injected `$componentController` function passing the name of the `phoneList`
|
||||
component (whose controller we wanted to instantiate) as a parameter.
|
||||
|
||||
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
|
||||
we create the `PhoneListController`, we need to tell the testing harness to expect an incoming
|
||||
request from the controller. To do this we:
|
||||
|
||||
* Inject the `$httpBackend` service into the `beforeEach()` function. This is a
|
||||
{@link ngMock.$httpBackend mock version} of the service that in a production environment
|
||||
facilitates all XHR and JSONP requests. The mock version of this service allows us to write tests
|
||||
without having to deal with native APIs and the global state associated with them — both of which
|
||||
make testing a nightmare. It also overcomes the asynchronous nature of these calls, which would
|
||||
slow down unit tests.
|
||||
|
||||
* Use the `$httpBackend.expectGET()` method to train the `$httpBackend` service to expect an
|
||||
incoming HTTP request and tell it what to respond with. Note that the responses are not returned
|
||||
until we call the `$httpBackend.flush()` method.
|
||||
|
||||
Now we will make assertions to verify that the `phones` property doesn't exist on the controller
|
||||
before the response is received:
|
||||
|
||||
```js
|
||||
it('should create a `phones` property with 2 phones fetched with `$http`', function() {
|
||||
expect(ctrl.phones).toBeUndefined();
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
});
|
||||
```
|
||||
|
||||
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
|
||||
promise returned by the `$http` service to be resolved with the trained response. See
|
||||
{@link ngMock.$httpBackend#flushing-http-requests Flushing HTTP requests} in the mock
|
||||
`$httpBackend` documentation for a full explanation of why this is necessary.
|
||||
|
||||
* We make the assertions, verifying that the `phones` property now exists on the controller.
|
||||
|
||||
Finally, we verify that the default value of `orderProp` is set correctly:
|
||||
|
||||
```js
|
||||
it('should set a default value for the `orderProp` property', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 2 of 2 SUCCESS (0.133 secs / 0.097 secs)
|
||||
```
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Try to add an `{{orderProp}}` binding to `index.html`, and you'll see that nothing happens even
|
||||
when you are in the phone list view. This is because the `orderProp` model is visible only in the
|
||||
scope managed by `PhoneListCtrl`, which is associated with the `<div ng-view>` element. If you add
|
||||
the same binding into the `phone-list.html` template, the binding will work as expected.
|
||||
<div></div>
|
||||
|
||||
<div style="display: none">
|
||||
* In `PhoneCatCtrl`, create a new model called "`hero`" with `this.hero = 'Zoro'`. In
|
||||
`PhoneListCtrl`, let's shadow it with `this.hero = 'Batman'`. In `PhoneDetailCtrl`, we'll use
|
||||
`this.hero = "Captain Proton"`. Then add the `<p>hero = {{hero}}</p>` to all three of our templates
|
||||
(`index.html`, `phone-list.html`, and `phone-detail.html`). Open the app and you'll see scope
|
||||
inheritance and model property shadowing do some wonders.
|
||||
</div>
|
||||
* At the bottom of `phone-list.template.html`, add a
|
||||
`<pre>{{$ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp | json}}</pre>` binding to see
|
||||
the list of phones displayed in JSON format.
|
||||
|
||||
* In the `PhoneListController` controller, pre-process the HTTP response by limiting the number of
|
||||
phones to the first 5 in the list. Use the following code in the `$http` callback:
|
||||
|
||||
```js
|
||||
self.phones = response.data.slice(0, 5);
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
With the routing set up and the phone list view implemented, we're ready to go to {@link step_08
|
||||
step 8} to implement the phone details view.
|
||||
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
|
||||
injection), go to {@link step_08 step 8}, where you will add some thumbnail images of phones and
|
||||
some links.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="7"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io
|
||||
[minification]: https://en.wikipedia.org/wiki/Minification_(programming)
|
||||
|
||||
@@ -1,197 +1,117 @@
|
||||
@ngdoc tutorial
|
||||
@name 8 - More Templating
|
||||
@name 8 - Templating Links & Images
|
||||
@step 8
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="8"></ul>
|
||||
|
||||
|
||||
In this step, you will implement the phone details view, which is displayed when a user clicks on a
|
||||
phone in the phone list.
|
||||
In this step, we will add thumbnail images for the phones in the phone list, and links that, for
|
||||
now, will go nowhere. In subsequent steps, we will use the links to display additional information
|
||||
about the phones in the catalog.
|
||||
|
||||
* When you click on a phone on the list, the phone details page with phone-specific information
|
||||
is displayed.
|
||||
* There are now links and images of the phones in the list.
|
||||
|
||||
To implement the phone details view we are going to use {@link ng.$http $http} to fetch our data,
|
||||
and then flesh out the `phone-detail.html` view template.
|
||||
|
||||
<div doc-tutorial-reset="8"></div>
|
||||
|
||||
|
||||
## Data
|
||||
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
|
||||
phone:
|
||||
Note that the `phones.json` file contains unique IDs and image URLs for each of the phones. The
|
||||
URLs point to the `app/img/phones/` directory.
|
||||
|
||||
__`app/phones/nexus-s.json`:__ (sample snippet)
|
||||
<br />
|
||||
**`app/phones/phones.json`** (sample snippet):
|
||||
|
||||
```js
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC),...",
|
||||
"android": {
|
||||
"os": "Android 2.3",
|
||||
"ui": "Android"
|
||||
```json
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
"images": [
|
||||
"img/phones/nexus-s.0.jpg",
|
||||
"img/phones/nexus-s.1.jpg",
|
||||
"img/phones/nexus-s.2.jpg",
|
||||
"img/phones/nexus-s.3.jpg"
|
||||
],
|
||||
"storage": {
|
||||
"flash": "16384MB",
|
||||
"ram": "512MB"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
Each of these files describes various properties of the phone using the same data structure. We'll
|
||||
show this data in the phone detail view.
|
||||
## Component Template
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We'll expand the `PhoneDetailCtrl` by using the `$http` service to fetch the JSON files. This works
|
||||
the same way as the phone list controller.
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers',[]);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
|
||||
function($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
});
|
||||
}]);
|
||||
```
|
||||
|
||||
To construct the URL for the HTTP request, we use `$routeParams.phoneId` extracted from the current
|
||||
route by the `$route` service.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The TBD placeholder line has been replaced with lists and bindings that comprise the phone details.
|
||||
Note where we use the Angular `{{expression}}` markup and `ngRepeat` to project phone data from
|
||||
our model into the view.
|
||||
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
<img ng-src="{{phone.images[0]}}" class="phone">
|
||||
|
||||
<h1>{{phone.name}}</h1>
|
||||
|
||||
<p>{{phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}">
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb">
|
||||
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
|
||||
</a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
<ul class="specs">
|
||||
<li>
|
||||
<span>Availability and Networks</span>
|
||||
<dl>
|
||||
<dt>Availability</dt>
|
||||
<dd ng-repeat="availability in phone.availability">{{availability}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar double-curly brace binding in the `href` attribute values. In step 2, we added the
|
||||
`{{phone.name}}` binding as the element content. In this step the `{{phone.id}}` binding is used in
|
||||
the element attribute.
|
||||
|
||||
We also added phone images next to each record using an image tag with the {@link ngSrc ngSrc}
|
||||
directive. That directive prevents the browser from treating the Angular `{{ expression }}` markup
|
||||
literally, and initiating a request to an invalid URL (`http://localhost:8000/{{phone.imageUrl}}`),
|
||||
which it would have done if we had only specified an attribute binding in a regular `src` attribute
|
||||
(`<img src="{{phone.imageUrl}}">`). Using the `ngSrc` directive, prevents the browser from making an
|
||||
HTTP request to an invalid location.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**:
|
||||
|
||||
```js
|
||||
...
|
||||
<li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_08-09_final.png">
|
||||
</div>
|
||||
it('should render phone specific links', function() {
|
||||
var query = element(by.model('$ctrl.query'));
|
||||
query.sendKeys('nexus');
|
||||
|
||||
## Test
|
||||
|
||||
We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
|
||||
step 5.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
```js
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
...
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqual({name:'phone xyz'});
|
||||
});
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 3 of 3 SUCCESS (0.039 secs / 0.012 secs)</pre>
|
||||
|
||||
|
||||
We also added a new end-to-end test that navigates to the Nexus S detail page and verifies that the
|
||||
heading on the page is "Nexus S".
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones/nexus-s');
|
||||
element.all(by.css('.phones li a')).first().click();
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display nexus-s page', function() {
|
||||
expect(element(by.binding('phone.name')).getText()).toBe('Nexus S');
|
||||
});
|
||||
});
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
We added a new E2E test to verify that the application is generating correct links to the phone
|
||||
views, that we will implement in the upcoming steps.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Using the [Protractor API](http://angular.github.io/protractor/#/api),
|
||||
write a test that verifies that we display 4 thumbnail images on the Nexus S details page.
|
||||
<div></div>
|
||||
|
||||
* Replace the `ngSrc` directive with a plain old `src` attribute. Using tools such as your browser's
|
||||
developer tools or inspecting the web server access logs, confirm that the application is indeed
|
||||
making an extraneous request to `%7B%7Bphone.imageUrl%7D%7D` (or `{{phone.imageUrl}}`).
|
||||
|
||||
The issue here is that the browser will fire a request for that invalid image address as soon as
|
||||
it hits the `<img>` tag, which is before Angular has a chance to evaluate the expression and
|
||||
inject the valid address.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that the phone details view is in place, proceed to {@link step_09 step 9} to learn how to
|
||||
write your own custom display filter.
|
||||
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about Angular
|
||||
layout templates and how Angular makes it easy to create applications that have multiple views.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="8"></ul>
|
||||
|
||||
@@ -1,144 +1,429 @@
|
||||
@ngdoc tutorial
|
||||
@name 9 - Filters
|
||||
@name 9 - Routing & Multiple Views
|
||||
@step 9
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="9"></ul>
|
||||
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
* In the previous step, the details page displayed either "true" or "false" to indicate whether
|
||||
certain phone features were present or not. We have used a custom filter to convert those text
|
||||
strings into glyphs: ✓ for "true", and ✘ for "false". Let's see what the filter code looks like.
|
||||
In this step, you will learn how to create a layout template and how to build an application that
|
||||
has multiple views by adding routing, using an Angular module called {@link ngRoute ngRoute}.
|
||||
|
||||
* When you now navigate to `/index.html`, you are redirected to `/index.html#!/phones` and the phone
|
||||
list appears in the browser.
|
||||
* When you click on a phone link, the URL changes to that specific phone and the stub of a phone
|
||||
detail page is displayed.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="9"></div>
|
||||
|
||||
|
||||
## Custom Filter
|
||||
## Dependencies
|
||||
|
||||
In order to create a new filter, you are going to create a `phonecatFilters` module and register
|
||||
your custom filter with this module:
|
||||
The routing functionality added in this step is provided by Angular in the `ngRoute` module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
|
||||
__`app/js/filters.js`:__
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
|
||||
```js
|
||||
angular.module('phonecatFilters', []).filter('checkmark', function() {
|
||||
return function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
};
|
||||
});
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"bootstrap": "3.3.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
|
||||
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) or false (`\u2718` -> ✘).
|
||||
The new dependency `"angular-route": "1.5.x"` tells bower to install a version of the angular-route
|
||||
module that is compatible with version 1.5.x of Angular. We must tell bower to download and install
|
||||
this dependency.
|
||||
|
||||
Now that our filter is ready, we need to register the `phonecatFilters` module as a dependency for
|
||||
our main `phonecatApp` module.
|
||||
|
||||
__`app/js/app.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
angular.module('phonecatApp', ['ngRoute','phonecatControllers','phonecatFilters']);
|
||||
...
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## Multiple Views, Routing and Layout Templates
|
||||
|
||||
Our app is slowly growing and becoming more complex. Prior to this step, the app provided our users
|
||||
with a single view (including the list of all phones), and all of the template code was located in
|
||||
the `phone-list.template.html` file. The next step in building the application is to add a view that
|
||||
will show detailed information about each of the devices in our list.
|
||||
|
||||
To add the detailed view, we are going to turn `index.html` into what we call a "layout template".
|
||||
This is a template that is common for all views in our application. Other "partial templates" are
|
||||
then included into this layout template depending on the current "route" — the view that is
|
||||
currently displayed to the user.
|
||||
|
||||
Application routes in Angular are declared via the {@link ngRoute.$routeProvider $routeProvider},
|
||||
which is the provider of the {@link ngRoute.$route $route} service. This service makes it easy to
|
||||
wire together controllers, view templates, and the current URL location in the browser. Using this
|
||||
feature, we can implement [deep linking][deep-linking], which lets us utilize the browser's history
|
||||
(back and forward navigation) and bookmarks.
|
||||
|
||||
<div class="alert alert-success">
|
||||
<p>
|
||||
`ngRoute` allows as to associate a controller and a template with a specific URL (or URL
|
||||
pattern). This is pretty close to what we did with `ngController` and `index.html` back in
|
||||
{@link step_02 step 2}.
|
||||
</p>
|
||||
<p>
|
||||
Since we have already learned that components allow us to combine controllers with templates in
|
||||
a modular, testable way, we are going to use components for routing as well.
|
||||
Each route will be associated with a component and that component will be in charge of providing
|
||||
the view template and the controller.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
### A Note about DI, Injector and Providers
|
||||
|
||||
As you {@link step_07 noticed}, {@link guide/di dependency injection} (DI) is at the core of
|
||||
AngularJS, so it's important for you to understand a thing or two about how it works.
|
||||
|
||||
When the application bootstraps, Angular creates an injector that will be used to find and inject
|
||||
all of the services that are required by your application. The injector itself doesn't know anything
|
||||
about what the `$http` or `$route` services do. In fact, the injector doesn't even know about the
|
||||
existence of these services, unless it is configured with proper module definitions.
|
||||
|
||||
The injector only carries out the following steps:
|
||||
|
||||
* Load the module definition(s) that you specify in your application.
|
||||
* Register all Providers defined in these module definition(s).
|
||||
* When asked to do so, lazily instantiate services and their dependencies, via their Providers, as
|
||||
parameters to an injectable function.
|
||||
|
||||
Providers are objects that provide (create) instances of services and expose configuration APIs,
|
||||
that can be used to control the creation and runtime behavior of a service. In case of the `$route`
|
||||
service, the `$routeProvider` exposes APIs that allow you to define routes for your application.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Providers can only be injected into `config` functions. Thus you could not inject
|
||||
`$routeProvider` into `PhoneListController` at runtime.
|
||||
</div>
|
||||
|
||||
Angular modules solve the problem of removing global variables from the application and provide a
|
||||
way of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try
|
||||
to solve the problem of script load ordering or lazy script fetching. These goals are totally
|
||||
independent and both module systems can live side-by-side and fulfill their goals.
|
||||
|
||||
To deepen your understanding on Angular's DI, see [Understanding Dependency Injection][wiki-di].
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
|
||||
layout template.
|
||||
The `$route` service is usually used in conjunction with the {@link ngRoute.directive:ngView ngView}
|
||||
directive. The role of the `ngView` directive is to include the view template for the current route
|
||||
into the layout template. This makes it a perfect fit for our `index.html` template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="js/filters.js"></script>
|
||||
...
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="app.module.js"></script>
|
||||
<script src="app.config.js"></script>
|
||||
...
|
||||
<script src="phone-detail/phone-detail.module.js"></script>
|
||||
<script src="phone-detail/phone-detail.component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
```
|
||||
|
||||
The syntax for using filters in Angular templates is as follows:
|
||||
We have added four new `<script>` tags in our `index.html` file to load some extra JavaScript files
|
||||
into our application:
|
||||
|
||||
{{ expression | filter }}
|
||||
* `angular-route.js`: Defines the Angular `ngRoute` module, which provides us with routing.
|
||||
* `app.config.js`: Configures the providers available to our main module (see
|
||||
[below](tutorial/step_09#configuring-a-module)).
|
||||
* `phone-detail.module.js`: Defines a new module containing a `phoneDetail` component.
|
||||
* `phone-detail.component.js`: Defines a dummy `phoneDetail` component (see
|
||||
[below](tutorial/step_09#the-phonedetail-component)).
|
||||
|
||||
Let's employ the filter in the phone details template:
|
||||
Note that we removed the `<phone-list></phone-list>` line from the `index.html` template and
|
||||
replaced it with a `<div>` with the `ng-view` attribute.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_09.png">
|
||||
|
||||
|
||||
## Configuring a Module
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
A module's {@link ng.angular.Module#config .config()} method gives us access to the available
|
||||
providers for configuration. To make the providers, services and directives defined in `ngRoute`
|
||||
available to our application, we need to add `ngRoute` as a dependency of our `phonecatApp` module.
|
||||
|
||||
```html
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Filters, like any other component, should be tested and these tests are very easy to write.
|
||||
|
||||
__`test/unit/filtersSpec.js`:__
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
describe('filter', function() {
|
||||
|
||||
beforeEach(module('phonecatFilters'));
|
||||
|
||||
describe('checkmark', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross',
|
||||
inject(function(checkmarkFilter) {
|
||||
expect(checkmarkFilter(true)).toBe('\u2713');
|
||||
expect(checkmarkFilter(false)).toBe('\u2718');
|
||||
}));
|
||||
});
|
||||
});
|
||||
angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
We must call `beforeEach(module('phonecatFilters'))` before any of
|
||||
our filter tests execute. This call loads our `phonecatFilters` module into the injector
|
||||
for this test run.
|
||||
Now, in addition to the core services and directives, we can also configure the `$route` service
|
||||
(using it's provider) for our application. In order to be able to quickly locate the configuration
|
||||
code, we put it into a separate file and used the `.config` suffix.
|
||||
|
||||
Note that we call the helper function, `inject(function(checkmarkFilter) { ... })`, to get
|
||||
access to the filter that we want to test. See {@link angular.mock.inject angular.mock.inject()}.
|
||||
<br />
|
||||
**`app/app.config.js`:**
|
||||
|
||||
Notice that the suffix 'Filter' is appended to your filter name when injected.
|
||||
See the {@link guide/filter#using-filters-in-controllers-services-and-directives Filter Guide}
|
||||
section where this is outlined.
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
config(['$locationProvider', '$routeProvider',
|
||||
function config($locationProvider, $routeProvider) {
|
||||
$locationProvider.hashPrefix('!');
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
$routeProvider.
|
||||
when('/phones', {
|
||||
template: '<phone-list></phone-list>'
|
||||
}).
|
||||
when('/phones/:phoneId', {
|
||||
template: '<phone-detail></phone-detail>'
|
||||
}).
|
||||
otherwise('/phones');
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
<pre>Chrome 22.0: Executed 4 of 4 SUCCESS (0.034 secs / 0.012 secs)</pre>
|
||||
Using the `.config()` method, we request the necessary providers (for example the `$routeProvider`)
|
||||
to be injected into our configuration function and then use their methods to specify the behavior of
|
||||
the corresponding services. Here, we use the
|
||||
{@link ngRoute.$routeProvider#when $routeProvider.when()} and
|
||||
{@link ngRoute.$routeProvider#otherwise $routeProvider.otherwise()} methods to define our
|
||||
application routes.
|
||||
|
||||
<div class="alert alert-success">
|
||||
<p>
|
||||
We also used {@link $locationProvider#hashPrefix $locationProvider.hashPrefix()} to set the
|
||||
hash-prefix to `!`. This prefix will appear in the links to our client-side routes, right after
|
||||
the hash (`#`) symbol and before the actual path (e.g. `index.html#!/some/path`).
|
||||
</p>
|
||||
<p>
|
||||
Setting a prefix is not necessary, but it is considered a good practice (for reasons that are
|
||||
outside the scope of this tutorial). `!` is the most commonly used prefix.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Our routes are defined as follows:
|
||||
|
||||
* `when('/phones')`: Determines the view that will be shown, when the URL hash fragment is
|
||||
`/phones`. According to the specified template, Angular will create an instance of the `phoneList`
|
||||
component to manage the view. Note that this is the same markup that we used to have in the
|
||||
`index.html` file.
|
||||
|
||||
* `when('/phones/:phoneId')`: Determines the view that will be shown, when the URL hash fragment
|
||||
matches `/phones/<phoneId>`, where `<phoneId>` is a variable part of the URL. In charge of the
|
||||
view will be the `phoneDetail` component.
|
||||
|
||||
* `otherwise('/phones')`: Defines a fallback route to redirect to, when no route definition matches
|
||||
the current URL.(Here it will redirect to `/phones`.)
|
||||
|
||||
We reused the `phoneList` component that we have already built and a new "dummy" `phoneDetail`
|
||||
component. For now, the `phoneDetail` component will just display the selected phone's ID.
|
||||
(Not too impressive, but we will enhance it in the {@link step_10 next step}.)
|
||||
|
||||
Note the use of the `:phoneId` parameter in the second route declaration. The `$route` service uses
|
||||
the route declaration — `'/phones/:phoneId'` — as a template that is matched against the current
|
||||
URL. All variables defined with the `:` prefix are extracted into the (injectable)
|
||||
{@link ngRoute.$routeParams $routeParams} object.
|
||||
|
||||
|
||||
## The `phoneDetail` Component
|
||||
|
||||
We created a `phoneDetail` component to handle the phone details view. We followed the same
|
||||
conventions as with `phoneList`, using a separate directory and creating a `phoneDetail` module,
|
||||
which we added as a dependency of the `phonecatApp` module.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phoneDetail', [
|
||||
'ngRoute'
|
||||
]);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
template: 'TBD: Detail view for <span>{{$ctrl.phoneId}}</span>',
|
||||
controller: ['$routeParams',
|
||||
function PhoneDetailController($routeParams) {
|
||||
this.phoneId = $routeParams.phoneId;
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phonecatApp', [
|
||||
...
|
||||
'phoneDetail',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
### A Note on Sub-module Dependencies
|
||||
|
||||
The `phoneDetail` module depends on the `ngRoute` module for providing the `$routeParams` object,
|
||||
which is used in the `phoneDetail` component's controller. Since `ngRoute` is also a dependency of
|
||||
the main `phonecatApp` module, its services and directives are already available everywhere in the
|
||||
application (including the `phoneDetail` component).
|
||||
|
||||
This means that our application would continue to work even if we didn't include `ngRoute` in the
|
||||
list of dependencies for the `phoneDetail` component. Although it might be tempting to omit
|
||||
dependencies of a sub-module that are already imported by the main module, it breaks our hard-earned
|
||||
modularity.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Imagine what would happen if we decided to copy the `phoneDetail` feature over to another project
|
||||
that does not declare a dependency on `ngRoute`. The injector would not be able to provide
|
||||
`$routeParams` and our application would break.
|
||||
</div>
|
||||
|
||||
The takeaway here is:
|
||||
|
||||
* Always be explicit about the dependecies of a sub-module. Do not rely on dependencies inherited
|
||||
from a parent module (because that parent module might not be there some day).
|
||||
|
||||
<div class="alert alert-success">
|
||||
Declaring the same dependency in multiple modules does not incur extra "cost", because Angular
|
||||
will still load each dependency once. For more info on modules and their dependencies take a look
|
||||
at the [Modules](guide/module) section of the Developer Guide.
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Since some of our modules depend on {@link ngRoute ngRoute} now, it is necessary to update the Karma
|
||||
configuration file with angular-route. Other than that, the unit tests should (still) pass without
|
||||
any modification.
|
||||
|
||||
<br />
|
||||
**`karma.conf.js`:**
|
||||
|
||||
```js
|
||||
files: [
|
||||
'bower_components/angular/angular.js',
|
||||
'bower_components/angular-route/angular-route.js',
|
||||
...
|
||||
],
|
||||
```
|
||||
|
||||
<br />
|
||||
To automatically verify that everything is wired properly, we wrote E2E tests for navigating to
|
||||
various URLs and verifying that the correct view was rendered.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
it('should redirect `index.html` to `index.html#!/phones', function() {
|
||||
browser.get('index.html');
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones');
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
describe('View: Phone list', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones');
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
describe('View: Phone details', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones/nexus-s');
|
||||
});
|
||||
|
||||
it('should display placeholder page with `phoneId`', function() {
|
||||
expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run (and hopefully pass).
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's experiment with some of the {@link api/ng/filter built-in Angular filters} and add the
|
||||
following bindings to `index.html`:
|
||||
* `{{ "lower cap string" | uppercase }}`
|
||||
* `{{ {foo: "bar", baz: 23} | json }}`
|
||||
* `{{ 1304375948024 | date }}`
|
||||
* `{{ 1304375948024 | date:"MM/dd/yyyy @ h:mma" }}`
|
||||
<div></div>
|
||||
|
||||
* We can also create a model with an input element, and combine it with a filtered binding. Add
|
||||
the following to index.html:
|
||||
* Try to add a `{{$ctrl.phoneId}` binding in the template string for the phone details view:
|
||||
|
||||
```html
|
||||
<input ng-model="userInput"> Uppercased: {{ userInput | uppercase }}
|
||||
```js
|
||||
when('/phones/:phoneId', {
|
||||
template: '{{$ctrl.phoneId}} <phone-detail></phone-detail>'
|
||||
...
|
||||
```
|
||||
|
||||
You will see that nothing happens, even when you are in the phone details view. This is because
|
||||
the `phoneId` model is visible only in the context set by the `phoneDetail` component. Again,
|
||||
component isolation at work!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have learned how to write and test a custom filter, go to {@link step_10 step 10} to
|
||||
learn how we can use Angular to enhance the phone details page further.
|
||||
With the routing set up and the phone list view implemented, we are ready to go to
|
||||
{@link step_10 step 10} and implement a proper phone details view.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="9"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io
|
||||
[deep-linking]: https://en.wikipedia.org/wiki/Deep_linking
|
||||
[wiki-di]: https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
|
||||
|
||||
@@ -1,180 +1,214 @@
|
||||
@ngdoc tutorial
|
||||
@name 10 - Event Handlers
|
||||
@name 10 - More Templating
|
||||
@step 10
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
In this step, you will add a clickable phone image swapper to the phone details page.
|
||||
In this step, we will implement the phone details view, which is displayed when a user clicks on a
|
||||
phone in the phone list.
|
||||
|
||||
* When you click on a phone on the list, the phone details page with phone-specific information is
|
||||
displayed.
|
||||
|
||||
To implement the phone details view we are going to use {@link ng.$http $http} to fetch our data,
|
||||
and then flesh out the `phoneDetail` component's template.
|
||||
|
||||
* The phone details view displays one large image of the current phone and several smaller thumbnail
|
||||
images. It would be great if we could replace the large image with any of the thumbnails just by
|
||||
clicking on the desired thumbnail image. Let's have a look at how we can do this with Angular.
|
||||
|
||||
<div doc-tutorial-reset="10"></div>
|
||||
|
||||
## Controller
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
## Data
|
||||
|
||||
```js
|
||||
...
|
||||
var phonecatControllers = angular.module('phonecatControllers',[]);
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
|
||||
phone:
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
|
||||
function($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
$scope.mainImageUrl = data.images[0];
|
||||
});
|
||||
<br />
|
||||
**`app/phones/nexus-s.json`:** (sample snippet)
|
||||
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
};
|
||||
}]);
|
||||
```json
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC), ...",
|
||||
"android": {
|
||||
"os": "Android 2.3",
|
||||
"ui": "Android"
|
||||
},
|
||||
...
|
||||
"images": [
|
||||
"img/phones/nexus-s.0.jpg",
|
||||
"img/phones/nexus-s.1.jpg",
|
||||
"img/phones/nexus-s.2.jpg",
|
||||
"img/phones/nexus-s.3.jpg"
|
||||
],
|
||||
"storage": {
|
||||
"flash": "16384MB",
|
||||
"ram": "512MB"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the `PhoneDetailCtrl` controller, we created the `mainImageUrl` model property and set its
|
||||
default value to the first phone image URL.
|
||||
|
||||
We also created a `setImage` event handler function that will change the value of `mainImageUrl`.
|
||||
Each of these files describes various properties of the phone using the same data structure. We will
|
||||
show this data in the phone details view.
|
||||
|
||||
|
||||
## Template
|
||||
## Component Controller
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
We will expand the `phoneDetail` component's controller by using the `$http` service to fetch the
|
||||
appropriate JSON files. This works the same way as the `phoneList` component's controller.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
templateUrl: 'phone-detail/phone-detail.template.html',
|
||||
controller: ['$http', '$routeParams',
|
||||
function PhoneDetailController($http, $routeParams) {
|
||||
var self = this;
|
||||
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
|
||||
self.phone = response.data;
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
To construct the URL for the HTTP request, we use `$routeParams.phoneId`, which is extracted from
|
||||
the current route by the `$route` service.
|
||||
|
||||
|
||||
## Component Template
|
||||
|
||||
The inline, TBD placeholder template has been replaced with a full blown external template,
|
||||
including lists and bindings that comprise the phone details. Note how we use the Angular
|
||||
`{{expression}}` markup and `ngRepeat` to project phone data from our model into the view.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
```html
|
||||
<img ng-src="{{mainImageUrl}}" class="phone">
|
||||
<img ng-src="{{$ctrl.phone.images[0]}}" class="phone" />
|
||||
|
||||
...
|
||||
<h1>{{$ctrl.phone.name}}</h1>
|
||||
|
||||
<p>{{$ctrl.phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}" ng-click="setImage(img)">
|
||||
<li ng-repeat="img in $ctrl.phone.images">
|
||||
<img ng-src="{{img}}" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="specs">
|
||||
<li>
|
||||
<span>Availability and Networks</span>
|
||||
<dl>
|
||||
<dt>Availability</dt>
|
||||
<dd ng-repeat="availability in $ctrl.phone.availability">{{availability}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
...
|
||||
<li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{$ctrl.phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
We bound the `ngSrc` directive of the large image to the `mainImageUrl` property.
|
||||
<img class="diagram" src="img/tutorial/tutorial_10.png">
|
||||
|
||||
We also registered an {@link ng.directive:ngClick `ngClick`}
|
||||
handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will
|
||||
use the `setImage` event handler function to change the value of the `mainImageUrl` property to the
|
||||
URL of the thumbnail image.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_10-11_final.png">
|
||||
</div>
|
||||
# Testing
|
||||
|
||||
## Test
|
||||
We wrote a new unit test that is similar to the one we wrote for the `phoneList` component's
|
||||
controller in {@link step_07#testing step 7}.
|
||||
|
||||
To verify this new feature, we added two end-to-end tests. One verifies that the main image is set
|
||||
to the first phone image by default. The second test clicks on several thumbnail images and
|
||||
verifies that the main image changed appropriately.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.spec.js`:**
|
||||
|
||||
```js
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
describe('phoneDetail', function() {
|
||||
|
||||
...
|
||||
// Load the module that contains the `phoneDetail` component before each test
|
||||
beforeEach(module('phoneDetail'));
|
||||
|
||||
it('should display the first phone image as the main phone image', function() {
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
// Test the controller
|
||||
describe('PhoneDetailController', function() {
|
||||
var $httpBackend, ctrl;
|
||||
|
||||
|
||||
it('should swap main image if a thumbnail image is clicked on', function() {
|
||||
element(by.css('.phone-thumbs li:nth-child(3) img')).click();
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
|
||||
|
||||
element(by.css('.phone-thumbs li:nth-child(1) img')).click();
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
You also have to refactor one of your unit tests because of the addition of the `mainImageUrl`
|
||||
model property to the `PhoneDetailCtrl` controller. Below, we create the function `xyzPhoneData`
|
||||
which returns the appropriate json with the `images` attribute in order to get the test to pass.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
...
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl,
|
||||
xyzPhoneData = function() {
|
||||
return {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
beforeEach(inject(function($componentController, _$httpBackend_, $routeParams) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
|
||||
$httpBackend.expectGET('phones/xyz.json').respond({name: 'phone xyz'});
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
|
||||
ctrl = $componentController('phoneDetail');
|
||||
}));
|
||||
|
||||
it('should fetch the phone details', function() {
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqual(xyzPhoneData());
|
||||
expect(ctrl.phone).toEqual({name: 'phone xyz'});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Your unit tests should now be passing.
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 3 of 3 SUCCESS (0.159 secs / 0.136 secs)
|
||||
```
|
||||
|
||||
We also added a new E2E test that navigates to the 'Nexus S' details page and verifies that the
|
||||
heading on the page is "Nexus S".
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
describe('View: Phone detail', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones/nexus-s');
|
||||
});
|
||||
|
||||
it('should display the `nexus-s` page', function() {
|
||||
expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You can run the tests with `npm run protractor`.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's add a new controller method to `PhoneDetailCtrl`:
|
||||
<div></div>
|
||||
|
||||
$scope.hello = function(name) {
|
||||
alert('Hello ' + (name || 'world') + '!');
|
||||
}
|
||||
|
||||
and add:
|
||||
|
||||
<button ng-click="hello('Elmo')">Hello</button>
|
||||
|
||||
to the `phone-detail.html` template.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
The controller methods are inherited between controllers/scopes, so you can use the same snippet
|
||||
in the `phone-list.html` template as well.
|
||||
|
||||
* Move the `hello` method from `PhoneCatCtrl` to `PhoneListCtrl` and you'll see that the button
|
||||
declared in `index.html` will stop working, while the one declared in the `phone-list.html`
|
||||
template remains operational.
|
||||
</div>
|
||||
* Using [Protractor's API][protractor-docs], write a test that verifies that we display 4 thumbnail
|
||||
images on the 'Nexus S' details page.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
With the phone image swapper in place, we're ready for {@link step_11 step 11} to
|
||||
learn an even better way to fetch data.
|
||||
Now that the phone details view is in place, proceed to {@link step_11 step 11} to learn how to
|
||||
write your own custom display filter.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
[protractor-docs]: https://angular.github.io/protractor/#/api
|
||||
|
||||
@@ -1,293 +1,176 @@
|
||||
@ngdoc tutorial
|
||||
@name 11 - REST and Custom Services
|
||||
@name 11 - Custom Filters
|
||||
@step 11
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="11"></ul>
|
||||
|
||||
|
||||
In this step, you will change the way our app fetches data.
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
* We define a custom service that represents a [RESTful][restful] client. Using this client we
|
||||
can make requests to the server for data in an easier way, without having to deal with the
|
||||
lower-level {@link ng.$http $http} API, HTTP methods and URLs.
|
||||
* In the previous step, the details page displayed either "true" or "false" to indicate whether
|
||||
certain phone features were present or not. In this step, we are using a custom filter to convert
|
||||
those text strings into glyphs: ✓ for "true", and ✘ for "false".
|
||||
|
||||
Let's see what the filter code looks like.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="11"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The RESTful functionality is provided by Angular in the `ngResource` module, which is distributed
|
||||
separately from the core Angular framework.
|
||||
## The `checkmark` Filter
|
||||
|
||||
We are using [Bower][bower] to install client side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
Since this filter is generic (i.e. it is not specific to any view or component), we are going to
|
||||
register it in a `core` module, which contains "application-wide" features.
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-seed",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-seed",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "1.4.x",
|
||||
"angular-resource": "1.4.x"
|
||||
}
|
||||
}
|
||||
<br />
|
||||
**`app/core/core.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('core', []);
|
||||
```
|
||||
|
||||
The new dependency `"angular-resource": "1.4.x"` tells bower to install a version of the
|
||||
angular-resource component that is compatible with version 1.4.x. We must ask bower to download
|
||||
and install this dependency. We can do this by running:
|
||||
<br />
|
||||
**`app/core/checkmark/checkmark.filter.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('core').
|
||||
filter('checkmark', function() {
|
||||
return function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
};
|
||||
});
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
|
||||
problem with the `bower install` due to a conflict between the versions of angular.js that need to
|
||||
be installed. If you get this then simply delete your `app/bower_components` folder before running
|
||||
`npm install`.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
|
||||
preconfigured `npm install` to run bower for us.
|
||||
As you may have noticed, we (unsurprisingly) gave our file a `.filter` suffix.
|
||||
</div>
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
|
||||
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) and false
|
||||
(`\u2718` -> ✘).
|
||||
|
||||
## Template
|
||||
Now that our filter is ready, we need to register the `core` module as a dependency of our main
|
||||
`phonecatApp` module.
|
||||
|
||||
Our custom resource service will be defined in `app/js/services.js` so we need to include this file
|
||||
in our layout template. Additionally, we also need to load the `angular-resource.js` file, which
|
||||
contains the {@link module:ngResource ngResource} module:
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
__`app/index.html`.__
|
||||
```js
|
||||
angular.module('phonecatApp', [
|
||||
...
|
||||
'core',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
|
||||
## Templates
|
||||
|
||||
Since we have created two new files (**core.module.js**, **checkmark.filter.js**), we need to
|
||||
include them in our layout template.
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="js/services.js"></script>
|
||||
...
|
||||
...
|
||||
<script src="core/core.module.js"></script>
|
||||
<script src="core/checkmark/checkmark.filter.js"></script>
|
||||
...
|
||||
```
|
||||
|
||||
## Service
|
||||
The syntax for using filters in Angular templates is as follows:
|
||||
|
||||
We create our own service to provide access to the phone data on the server:
|
||||
|
||||
__`app/js/services.js`.__
|
||||
|
||||
```js
|
||||
var phonecatServices = angular.module('phonecatServices', ['ngResource']);
|
||||
|
||||
phonecatServices.factory('Phone', ['$resource',
|
||||
function($resource){
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
|
||||
});
|
||||
}]);
|
||||
```
|
||||
{{expression | filter}}
|
||||
```
|
||||
|
||||
We used the module API to register a custom service using a factory function. We passed in the name
|
||||
of the service - 'Phone' - and the factory function. The factory function is similar to a
|
||||
controller's constructor in that both can declare dependencies to be injected via function
|
||||
arguments. The Phone service declared a dependency on the `$resource` service.
|
||||
Let's employ the filter in the phone details template:
|
||||
|
||||
The {@link ngResource.$resource `$resource`} service makes it easy to create a
|
||||
[RESTful][restful] client with just a few lines of code. This client can then be used in our
|
||||
application, instead of the lower-level {@link ng.$http $http} service.
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
__`app/js/app.js`.__
|
||||
|
||||
```js
|
||||
...
|
||||
angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']).
|
||||
...
|
||||
```html
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{$ctrl.phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{$ctrl.phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
```
|
||||
|
||||
We need to add the 'phonecatServices' module dependency to 'phonecatApp' module's requires array.
|
||||
|
||||
# Testing
|
||||
|
||||
## Controller
|
||||
Filters, like any other code, should be tested. Luckily, these tests are very easy to write.
|
||||
|
||||
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
|
||||
lower-level {@link ng.$http $http} service, replacing it with a new service called
|
||||
`Phone`. Angular's {@link ngResource.$resource `$resource`} service is easier to
|
||||
use than `$http` for interacting with data sources exposed as RESTful resources. It is also easier
|
||||
now to understand what the code in our controllers is doing.
|
||||
|
||||
__`app/js/controllers.js`.__
|
||||
<br />
|
||||
**`app/core/checkmark/checkmark.filter.spec.js`:**
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers', []);
|
||||
describe('checkmark', function() {
|
||||
|
||||
...
|
||||
beforeEach(module('core'));
|
||||
|
||||
phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) {
|
||||
$scope.phones = Phone.query();
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
it('should convert boolean values to unicode checkmark or cross',
|
||||
inject(function(checkmarkFilter) {
|
||||
expect(checkmarkFilter(true)).toBe('\u2713');
|
||||
expect(checkmarkFilter(false)).toBe('\u2718');
|
||||
})
|
||||
);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) {
|
||||
$scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
|
||||
$scope.mainImageUrl = phone.images[0];
|
||||
});
|
||||
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
}
|
||||
}]);
|
||||
```
|
||||
|
||||
Notice how in `PhoneListCtrl` we replaced:
|
||||
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
with:
|
||||
|
||||
$scope.phones = Phone.query();
|
||||
|
||||
This is a simple statement that we want to query for all phones.
|
||||
|
||||
An important thing to notice in the code above is that we don't pass any callback functions when
|
||||
invoking methods of our Phone service. Although it looks as if the result were returned
|
||||
synchronously, that is not the case at all. What is returned synchronously is a "future" — an
|
||||
object, which will be filled with data when the XHR response returns. Because of the data-binding
|
||||
in Angular, we can use this future and bind it to our template. Then, when the data arrives, the
|
||||
view will automatically update.
|
||||
|
||||
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
|
||||
we require, so in these cases, we can add a callback to process the server response. The
|
||||
`PhoneDetailCtrl` controller illustrates this by setting the `mainImageUrl` in a callback.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Because we're now using the {@link ngResource ngResource} module, it's necessary to
|
||||
update the Karma config file with angular-resource so the new tests will pass.
|
||||
|
||||
__`test/karma.conf.js`:__
|
||||
|
||||
```js
|
||||
files : [
|
||||
'app/bower_components/angular/angular.js',
|
||||
'app/bower_components/angular-route/angular-route.js',
|
||||
'app/bower_components/angular-resource/angular-resource.js',
|
||||
'app/bower_components/angular-mocks/angular-mocks.js',
|
||||
'app/js/**/*.js',
|
||||
'test/unit/**/*.js'
|
||||
],
|
||||
```
|
||||
|
||||
We have modified our unit tests to verify that our new service is issuing HTTP requests and
|
||||
processing them as expected. The tests also check that our controllers are interacting with the
|
||||
service correctly.
|
||||
|
||||
The {@link ngResource.$resource $resource} service augments the response object
|
||||
with methods for updating and deleting the resource. If we were to use the standard `toEqual`
|
||||
matcher, our tests would fail because the test values would not match the responses exactly. To
|
||||
solve the problem, we use a newly-defined `toEqualData` [Jasmine matcher][jasmine-matchers]. When
|
||||
the `toEqualData` matcher compares two objects, it takes only object properties into account and
|
||||
ignores methods.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toEqualData: function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
beforeEach(module('phonecatServices'));
|
||||
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneListCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(scope.phones).toEqualData([]);
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phones).toEqualData(
|
||||
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl,
|
||||
xyzPhoneData = function() {
|
||||
return {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toEqualData({});
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqualData(xyzPhoneData());
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The call to `beforeEach(module('core'))` loads the `core` module (which contains the `checkmark`
|
||||
filter) into the injector, before every test.
|
||||
|
||||
Note that we call the helper function `inject(function(checkmarkFilter) {...})`, to get access to
|
||||
the filter that we want to test. See also {@link angular.mock.inject angular.mock.inject()}.
|
||||
|
||||
<div class="alert alert-info">
|
||||
When injecting a filter, we need to suffix the filter name with 'Filter'. For example, our
|
||||
`checkmark` filter is injected as `checkmarkFilter`.
|
||||
See the [Filters](guide/filter#using-filters-in-controllers-services-and-directives) section of
|
||||
the Developer Guide for more info.
|
||||
</div>
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 5 of 5 SUCCESS (0.038 secs / 0.01 secs)</pre>
|
||||
```
|
||||
Chrome 49.0: Executed 4 of 4 SUCCESS (0.091 secs / 0.075 secs)
|
||||
```
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Let's experiment with some of the {@link api/ng/filter built-in Angular filters}.
|
||||
Add the following bindings to `index.html`:
|
||||
|
||||
* `{{'lower cap string' | uppercase}}`
|
||||
* `{{{foo: 'bar', baz: 42} | json}}`
|
||||
* `{{1459461289000 | date}}`
|
||||
* `{{1459461289000 | date:'MM/dd/yyyy @ h:mma'}}`
|
||||
|
||||
|
||||
* We can also create a model with an input element, and combine it with a filtered binding.
|
||||
Add the following to `index.html`:
|
||||
|
||||
```html
|
||||
<input ng-model="userInput" /> Uppercased: {{userInput | uppercase}}
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that we've seen how to build a custom service as a RESTful client, we're ready for {@link step_12 step 12} (the last step!) to
|
||||
learn how to improve this application with animations.
|
||||
Now that we have learned how to write and test a custom filter, let's go to {@link step_12 step 12}
|
||||
to learn how we can use Angular to enhance the phone details page further.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="11"></ul>
|
||||
|
||||
[restful]: http://en.wikipedia.org/wiki/Representational_State_Transfer
|
||||
[jasmine-matchers]: http://jasmine.github.io/1.3/introduction.html#section-Matchers
|
||||
[bower]: http://bower.io/
|
||||
|
||||
@@ -1,538 +1,185 @@
|
||||
@ngdoc tutorial
|
||||
@name 12 - Applying Animations
|
||||
@name 12 - Event Handlers
|
||||
@step 12
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="12"></ul>
|
||||
|
||||
|
||||
In this final step, we will enhance our phonecat web application by attaching CSS and JavaScript
|
||||
animations on top of the template code we created before.
|
||||
In this step, you will add a clickable phone image swapper to the phone details page.
|
||||
|
||||
* The phone details view displays one large image of the current phone and several smaller thumbnail
|
||||
images. It would be great if we could replace the large image with any of the thumbnails just by
|
||||
clicking on the desired thumbnail image. Let's have a look at how we can do this with Angular.
|
||||
|
||||
* We now use the `ngAnimate` module to enable animations throughout the application.
|
||||
* We also use common `ng` directives to automatically trigger hooks for animations to tap into.
|
||||
* When an animation is found then the animation will run in between the standard DOM operation that
|
||||
is being issued on the element at the given time (e.g. inserting and removing nodes on
|
||||
{@link ngRepeat `ngRepeat`} or adding and removing classes on
|
||||
{@link ngClass `ngClass`}).
|
||||
|
||||
<div doc-tutorial-reset="12"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The animation functionality is provided by Angular in the `ngAnimate` module, which is distributed
|
||||
separately from the core Angular framework. In addition we will use `jQuery` in this project to do
|
||||
extra JavaScript animations.
|
||||
## Component Controller
|
||||
|
||||
We are using [Bower][bower] to install client side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-seed",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-seed",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "1.4.x",
|
||||
"angular-resource": "1.4.x",
|
||||
"angular-animate": "1.4.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `"angular-animate": "1.4.x"` tells bower to install a version of the
|
||||
angular-animate component that is compatible with version 1.4.x.
|
||||
* `"jquery": "~2.1.1"` tells bower to install the 2.1.1 version of jQuery. Note that this is not an
|
||||
Angular library, it is the standard jQuery library. We can use bower to install a wide range of 3rd
|
||||
party libraries.
|
||||
|
||||
We must ask bower to download and install this dependency. We can do this by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
|
||||
problem with the `bower install` due to a conflict between the versions of angular.js that need to
|
||||
be installed. If you get this then simply delete your `app/bower_components` folder before running
|
||||
`npm install`.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
|
||||
preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
|
||||
## How Animations work with `ngAnimate`
|
||||
|
||||
To get an idea of how animations work with AngularJS, please read the
|
||||
{@link guide/animations AngularJS Animation Guide} first.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The changes required within the HTML template code is to link the asset files which define the animations as
|
||||
well as the `angular-animate.js` file. The animation module, known as {@link module:ngAnimate `ngAnimate`}, is
|
||||
defined within `angular-animate.js` and contains the code necessary to make your application become animation
|
||||
aware.
|
||||
|
||||
Here's what needs to be changed in the index file:
|
||||
|
||||
__`app/index.html`.__
|
||||
|
||||
```html
|
||||
...
|
||||
<!-- for CSS Transitions and/or Keyframe Animations -->
|
||||
<link rel="stylesheet" href="css/animations.css">
|
||||
|
||||
...
|
||||
|
||||
<!-- jQuery is used for JavaScript animations (include this before angular.js) -->
|
||||
<script src="bower_components/jquery/dist/jquery.js"></script>
|
||||
|
||||
...
|
||||
|
||||
<!-- required module to enable animation support in AngularJS -->
|
||||
<script src="bower_components/angular-animate/angular-animate.js"></script>
|
||||
|
||||
<!-- for JavaScript Animations -->
|
||||
<script src="js/animations.js"></script>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
<div class="alert alert-error">
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.4; jQuery 1.x is
|
||||
not officially supported.
|
||||
Be sure to load jQuery before all AngularJS scripts, otherwise AngularJS won't detect jQuery and
|
||||
animations will not work as expected.
|
||||
</div>
|
||||
|
||||
Animations can now be created within the CSS code (`animations.css`) as well as the JavaScript code (`animations.js`).
|
||||
But before we start, let's create a new module which uses the ngAnimate module as a dependency just like we did before
|
||||
with `ngResource`.
|
||||
|
||||
## Module & Animations
|
||||
|
||||
__`app/js/animations.js`.__
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phonecatAnimations', ['ngAnimate']);
|
||||
// ...
|
||||
// this module will later be used to define animations
|
||||
// ...
|
||||
...
|
||||
controller: ['$http', '$routeParams',
|
||||
function PhoneDetailController($http, $routeParams) {
|
||||
var self = this;
|
||||
|
||||
self.setImage = function setImage(imageUrl) {
|
||||
self.mainImageUrl = imageUrl;
|
||||
};
|
||||
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
|
||||
self.phone = response.data;
|
||||
self.setImage(self.phone.images[0]);
|
||||
});
|
||||
}
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
And now let's attach this module to our application module...
|
||||
In the `phoneDetail` component's controller, we created the `mainImageUrl` model property and set
|
||||
its default value to the first phone image URL.
|
||||
|
||||
__`app/js/app.js`.__
|
||||
|
||||
```js
|
||||
// ...
|
||||
angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
|
||||
'phonecatAnimations',
|
||||
'phonecatControllers',
|
||||
'phonecatFilters',
|
||||
'phonecatServices',
|
||||
]);
|
||||
// ...
|
||||
```
|
||||
|
||||
Now, the phonecat module is animation aware. Let's make some animations!
|
||||
We also created a `setImage()` method (to be used as event handler), that will change the value of
|
||||
`mainImageUrl`.
|
||||
|
||||
|
||||
## Animating ngRepeat with CSS Transition Animations
|
||||
## Component Template
|
||||
|
||||
We'll start off by adding CSS transition animations to our `ngRepeat` directive present on the `phone-list.html` page.
|
||||
First let's add an extra CSS class to our repeated element so that we can hook into it with our CSS animation code.
|
||||
|
||||
__`app/partials/phone-list.html`.__
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
```html
|
||||
<!--
|
||||
Let's change the repeater HTML to include a new CSS class
|
||||
which we will later use for animations:
|
||||
-->
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp"
|
||||
class="thumbnail phone-listing">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
```
|
||||
|
||||
Notice how we added the `phone-listing` CSS class? This is all we need in our HTML code to get animations working.
|
||||
|
||||
Now for the actual CSS transition animation code:
|
||||
|
||||
__`app/css/animations.css`__
|
||||
|
||||
```css
|
||||
.phone-listing.ng-enter,
|
||||
.phone-listing.ng-leave,
|
||||
.phone-listing.ng-move {
|
||||
-webkit-transition: 0.5s linear all;
|
||||
-moz-transition: 0.5s linear all;
|
||||
-o-transition: 0.5s linear all;
|
||||
transition: 0.5s linear all;
|
||||
}
|
||||
|
||||
.phone-listing.ng-enter,
|
||||
.phone-listing.ng-move {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-listing.ng-move.ng-move-active,
|
||||
.phone-listing.ng-enter.ng-enter-active {
|
||||
opacity: 1;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.phone-listing.ng-leave {
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-listing.ng-leave.ng-leave-active {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
```
|
||||
|
||||
As you can see our `phone-listing` CSS class is combined together with the animation hooks that occur when items are
|
||||
inserted into and removed from the list:
|
||||
|
||||
* The `ng-enter` class is applied to the element when a new phone is added to the list and rendered on the page.
|
||||
* The `ng-move` class is applied when items are moved around in the list.
|
||||
* The `ng-leave` class is applied when they're removed from the list.
|
||||
|
||||
The phone listing items are added and removed depending on the data passed to the `ng-repeat` attribute.
|
||||
For example, if the filter data changes, the items will be animated in and out of the repeat list.
|
||||
|
||||
Something important to note is that when an animation occurs, two sets of CSS classes
|
||||
are added to the element:
|
||||
|
||||
1. a "starting" class that represents the style at the beginning of the animation
|
||||
2. an "active" class that represents the style at the end of the animation
|
||||
|
||||
The name of the starting class is the name of the event that is fired (like `enter`, `move` or `leave`) prefixed with
|
||||
`ng-`. So an `enter` event will result in a class called `ng-enter`.
|
||||
|
||||
The active class name is the same as the starting class's but with an `-active` suffix.
|
||||
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
|
||||
|
||||
In our example above, elements are expanded from a height of **0** to **120 pixels** when they're added to the
|
||||
list and are collapsed back down to **0 pixels** before being removed from the list.
|
||||
There's also a nice fade-in and fade-out effect that occurs at the same time. All of this is handled
|
||||
by the CSS transition declarations at the top of the example code above.
|
||||
|
||||
Although most modern browsers have good support for [CSS transitions](http://caniuse.com/#feat=css-transitions)
|
||||
and [CSS animations](http://caniuse.com/#feat=css-animation), IE9 and earlier do not.
|
||||
If you want animations that are backwards-compatible with older browsers, consider using JavaScript-based animations,
|
||||
which are described in detail below.
|
||||
|
||||
|
||||
## Animating `ngView` with CSS Keyframe Animations
|
||||
|
||||
Next let's add an animation for transitions between route changes in {@link ngRoute.directive:ngView `ngView`}.
|
||||
|
||||
To start, let's add a new CSS class to our HTML like we did in the example above.
|
||||
This time, instead of the `ng-repeat` element, let's add it to the element containing the `ng-view` directive.
|
||||
In order to do this, we'll have to make some small changes to the HTML code so that we can have more control over our
|
||||
animations between view changes.
|
||||
|
||||
__`app/index.html`.__
|
||||
|
||||
```html
|
||||
<div class="view-container">
|
||||
<div ng-view class="view-frame"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
With this change, the `ng-view` directive is nested inside a parent element with
|
||||
a `view-container` CSS class. This class adds a `position: relative` style so that the positioning of the `ng-view`
|
||||
is relative to this parent as it animates transitions.
|
||||
|
||||
With this in place, let's add the CSS for this transition animation to our `animations.css` file:
|
||||
|
||||
__`app/css/animations.css`.__
|
||||
|
||||
```css
|
||||
.view-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter, .view-frame.ng-leave {
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter {
|
||||
-webkit-animation: 0.5s fade-in;
|
||||
-moz-animation: 0.5s fade-in;
|
||||
-o-animation: 0.5s fade-in;
|
||||
animation: 0.5s fade-in;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.view-frame.ng-leave {
|
||||
-webkit-animation: 0.5s fade-out;
|
||||
-moz-animation: 0.5s fade-out;
|
||||
-o-animation: 0.5s fade-out;
|
||||
animation: 0.5s fade-out;
|
||||
z-index:99;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@-moz-keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@-webkit-keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@-moz-keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@-webkit-keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/* don't forget about the vendor-prefixes! */
|
||||
```
|
||||
|
||||
Nothing crazy here! Just a simple fade in and fade out effect between pages. The only out of the
|
||||
ordinary thing here is that we're using absolute positioning to position the next page (identified
|
||||
via `ng-enter`) on top of the previous page (the one that has the `ng-leave` class) while performing
|
||||
a cross fade animation in between. So as the previous page is just about to be removed, it fades out
|
||||
while the new page fades in right on top of it.
|
||||
|
||||
Once the leave animation is over then element is removed and once the enter animation is complete
|
||||
then the `ng-enter` and `ng-enter-active` CSS classes are removed from the element, causing it to rerender and
|
||||
reposition itself with its default CSS code (so no more absolute positioning once the animation is
|
||||
over). This works fluidly so that pages flow naturally between route changes without anything
|
||||
jumping around.
|
||||
|
||||
The CSS classes applied (the start and end classes) are much the same as with `ng-repeat`. Each time
|
||||
a new page is loaded the `ng-view` directive will create a copy of itself, download the template and
|
||||
append the contents. This ensures that all views are contained within a single HTML element which
|
||||
allows for easy animation control.
|
||||
|
||||
For more on CSS animations, see the
|
||||
[Web Platform documentation](http://docs.webplatform.org/wiki/css/properties/animations).
|
||||
|
||||
|
||||
## Animating `ngClass` with JavaScript
|
||||
|
||||
Let's add another animation to our application. Switching to our `phone-detail.html` page,
|
||||
we see that we have a nice thumbnail swapper. By hovering over the thumbnails listed on the page,
|
||||
the profile phone image changes. But how can we change this around to add animations?
|
||||
|
||||
Let's think about it first. Basically, when you hover over a thumbnail image, you're changing the
|
||||
state of the profile image to reflect the newly selected thumbnail image.
|
||||
The best way to specify state changes within HTML is to use classes.
|
||||
Much like before, how we used a CSS class to specify an animation, this time the animation will
|
||||
occur whenever the CSS class itself changes.
|
||||
|
||||
Whenever a new phone thumbnail is selected, the state changes and the `.active` CSS class is added
|
||||
to the matching profile image and the animation plays.
|
||||
|
||||
Let's get started and tweak our HTML code on the `phone-detail.html` page first. Notice that we
|
||||
have changed the way we display our large image:
|
||||
|
||||
__`app/partials/phone-detail.html`.__
|
||||
|
||||
```html
|
||||
<!-- We're only changing the top of the file -->
|
||||
<div class="phone-images">
|
||||
<img ng-src="{{img}}"
|
||||
class="phone"
|
||||
ng-repeat="img in phone.images"
|
||||
ng-class="{active:mainImageUrl==img}">
|
||||
</div>
|
||||
|
||||
<h1>{{phone.name}}</h1>
|
||||
|
||||
<p>{{phone.description}}</p>
|
||||
|
||||
<img ng-src="{{$ctrl.mainImageUrl}}" class="phone" />
|
||||
...
|
||||
<ul class="phone-thumbs">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}" ng-mouseenter="setImage(img)">
|
||||
<li ng-repeat="img in $ctrl.phone.images">
|
||||
<img ng-src="{{img}}" ng-click="$ctrl.setImage(img)" />
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Just like with the thumbnails, we're using a repeater to display **all** the profile images as a
|
||||
list, however we're not animating any repeat-related animations. Instead, we're keeping our eye on
|
||||
the ng-class directive since whenever the `active` class is true then it will be applied to the
|
||||
element and will render as visible. Otherwise, the profile image is hidden. In our case, there is
|
||||
always one element that has the active class, and, therefore, there will always be one phone profile
|
||||
image visible on screen at all times.
|
||||
|
||||
When the active class is added to the element, the `active-add` and the `active-add-active` classes
|
||||
are added just before to signal AngularJS to fire off an animation. When removed, the
|
||||
`active-remove` and the `active-remove-active` classes are applied to the element which in turn
|
||||
trigger another animation.
|
||||
|
||||
To ensure that the phone images are displayed correctly when the page is first loaded we also tweak
|
||||
the detail page CSS styles:
|
||||
|
||||
__`app/css/app.css`__
|
||||
```css
|
||||
.phone-images {
|
||||
background-color: white;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
img.phone {
|
||||
float: left;
|
||||
margin-right: 3em;
|
||||
margin-bottom: 2em;
|
||||
background-color: white;
|
||||
padding: 2em;
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
img.phone:first-child {
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
We bound the `ngSrc` directive of the large image to the `$ctrl.mainImageUrl` property.
|
||||
|
||||
You may be thinking that we're just going to create another CSS-enabled animation.
|
||||
Although we could do that, let's take the opportunity to learn how to create JavaScript-enabled
|
||||
animations with the `animation()` module method.
|
||||
We also registered an {@link ng.directive:ngClick ngClick} handler with thumbnail images. When a
|
||||
user clicks on one of the thumbnail images, the handler will use the `$ctrl.setImage()` method
|
||||
callback to change the value of the `$ctrl.mainImageUrl` property to the URL of the clicked
|
||||
thumbnail image.
|
||||
|
||||
__`app/js/animations.js`.__
|
||||
<img class="diagram" src="img/tutorial/tutorial_12.png">
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
To verify this new feature, we added two E2E tests. One verifies that `mainImageUrl` is set to the
|
||||
first phone image URL by default. The second test clicks on several thumbnail images and verifies
|
||||
that the main image URL changes accordingly.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
var phonecatAnimations = angular.module('phonecatAnimations', ['ngAnimate']);
|
||||
...
|
||||
|
||||
phonecatAnimations.animation('.phone', function() {
|
||||
describe('View: Phone detail', function() {
|
||||
|
||||
var animateUp = function(element, className, done) {
|
||||
if(className != 'active') {
|
||||
return;
|
||||
}
|
||||
element.css({
|
||||
position: 'absolute',
|
||||
top: 500,
|
||||
left: 0,
|
||||
display: 'block'
|
||||
...
|
||||
|
||||
it('should display the first phone image as the main phone image', function() {
|
||||
var mainImage = element(by.css('img.phone'));
|
||||
|
||||
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
|
||||
jQuery(element).animate({
|
||||
top: 0
|
||||
}, done);
|
||||
it('should swap the main image when clicking on a thumbnail image', function() {
|
||||
var mainImage = element(by.css('img.phone'));
|
||||
var thumbnails = element.all(by.css('.phone-thumbs img'));
|
||||
|
||||
return function(cancel) {
|
||||
if(cancel) {
|
||||
element.stop();
|
||||
}
|
||||
};
|
||||
}
|
||||
thumbnails.get(2).click();
|
||||
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
|
||||
|
||||
var animateDown = function(element, className, done) {
|
||||
if(className != 'active') {
|
||||
return;
|
||||
}
|
||||
element.css({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0
|
||||
thumbnails.get(0).click();
|
||||
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
|
||||
jQuery(element).animate({
|
||||
top: -500
|
||||
}, done);
|
||||
});
|
||||
|
||||
return function(cancel) {
|
||||
if(cancel) {
|
||||
element.stop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
addClass: animateUp,
|
||||
removeClass: animateDown
|
||||
};
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
Note that we're using [jQuery](http://jquery.com/) to implement the animation. jQuery
|
||||
isn't required to do JavaScript animations with AngularJS, but we're going to use it because writing
|
||||
your own JavaScript animation library is beyond the scope of this tutorial. For more on
|
||||
`jQuery.animate`, see the [jQuery documentation](http://api.jquery.com/animate/).
|
||||
You can now rerun the tests with `npm run protractor`.
|
||||
|
||||
The `addClass` and `removeClass` callback functions are called whenever a class is added or removed
|
||||
on the element that contains the class we registered, which is in this case `.phone`. When the `.active`
|
||||
class is added to the element (via the `ng-class` directive) the `addClass` JavaScript callback will
|
||||
be fired with `element` passed in as a parameter to that callback. The last parameter passed in is the
|
||||
`done` callback function. The purpose of `done` is so you can let Angular know when the JavaScript
|
||||
animation has ended by calling it.
|
||||
We also have to refactor one of our unit tests, because of the addition of the `mainImageUrl` model
|
||||
property to the controller. As previously, we will use a mocked response.
|
||||
|
||||
The `removeClass` callback works the same way, but instead gets triggered when a class is removed
|
||||
from the element.
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.spec.js`:**
|
||||
|
||||
Within your JavaScript callback, you create the animation by manipulating the DOM. In the code above,
|
||||
that's what the `element.css()` and the `element.animate()` are doing. The callback positions the next
|
||||
element with an offset of `500 pixels` and animates both the previous and the new items together by
|
||||
shifting each item up `500 pixels`. This results in a conveyor-belt like animation. After the `animate`
|
||||
function does its business, it calls `done`.
|
||||
```js
|
||||
...
|
||||
|
||||
Notice that `addClass` and `removeClass` each return a function. This is an **optional** function that's
|
||||
called when the animation is cancelled (when another animation takes place on the same element)
|
||||
as well as when the animation has completed. A boolean parameter is passed into the function which
|
||||
lets the developer know if the animation was cancelled or not. This function can be used to
|
||||
do any cleanup necessary for when the animation finishes.
|
||||
describe('controller', function() {
|
||||
var $httpBackend, ctrl
|
||||
var xyzPhoneData = {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
};
|
||||
|
||||
beforeEach(inject(function($componentController, _$httpBackend_, _$routeParams_) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);
|
||||
|
||||
...
|
||||
}));
|
||||
|
||||
it('should fetch phone details', function() {
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(ctrl.phone).toEqual(xyzPhoneData);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Our unit tests should now be passing again.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Similar to the `ngClick` directive, which binds an Angular expression to the `click` event, there
|
||||
are built-in directives for all native events, such as `dblclick`, `focus`/`blur`, mouse and key
|
||||
events, etc.
|
||||
|
||||
Let's add a new controller method to the `phoneDetail` component's controller:
|
||||
|
||||
```js
|
||||
self.onDblclick = function onDblclick(imageUrl) {
|
||||
alert('You double-clicked image: ' + imageUrl);
|
||||
};
|
||||
```
|
||||
|
||||
and add the following to the `<img>` element in `phone-detail.template.html`:
|
||||
|
||||
```html
|
||||
<img ... ng-dblclick="$ctrl.onDblclick(img)" />
|
||||
```
|
||||
|
||||
Now, whenever you double-click on a thumbnail, an alert pops-up. Pretty annoying!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
There you have it! We have created a web app in a relatively short amount of time. In the {@link
|
||||
the_end closing notes} we'll cover where to go from here.
|
||||
With the phone image swapper in place, we are ready for {@link step_13 step 13} to learn an even
|
||||
better way to fetch data.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="12"></ul>
|
||||
|
||||
[bower]: http://bower.io/
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
@ngdoc tutorial
|
||||
@name 13 - REST and Custom Services
|
||||
@step 13
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="13"></ul>
|
||||
|
||||
|
||||
In this step, we will change the way our application fetches data.
|
||||
|
||||
* We define a custom service that represents a [RESTful][restful] client. Using this client we can
|
||||
make requests for data to the server in an easier way, without having to deal with the lower-level
|
||||
{@link ng.$http $http} API, HTTP methods and URLs.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="13"></div>
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
The RESTful functionality is provided by Angular in the {@link ngResource ngResource} module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-resource": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"bootstrap": "3.3.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The new dependency `"angular-resource": "1.5.x"` tells bower to install a version of the
|
||||
angular-resource module that is compatible with version 1.5.x of Angular. We must tell bower to
|
||||
download and install this dependency.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## Service
|
||||
|
||||
We create our own service to provide access to the phone data on the server. We will put the service
|
||||
in its own module, under `core`, so we can explicitly declare its dependency on `ngResource`:
|
||||
|
||||
<br />
|
||||
**`app/core/phone/phone.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('core.phone', ['ngResource']);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/core/phone/phone.service.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('core.phone').
|
||||
factory('Phone', ['$resource',
|
||||
function($resource) {
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {
|
||||
method: 'GET',
|
||||
params: {phoneId: 'phones'},
|
||||
isArray: true
|
||||
}
|
||||
});
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
We used the {@link angular.Module module API} to register a custom service using a factory function.
|
||||
We passed in the name of the service — `'Phone'` — and the factory function. The factory
|
||||
function is similar to a controller's constructor in that both can declare dependencies to be
|
||||
injected via function arguments. The `Phone` service declares a dependency on the `$resource`
|
||||
service, provided by the `ngResource` module.
|
||||
|
||||
The {@link ngResource.$resource $resource} service makes it easy to create a [RESTful][restful]
|
||||
client with just a few lines of code. This client can then be used in our application, instead of
|
||||
the lower-level {@link ng.$http $http} service.
|
||||
|
||||
<br />
|
||||
**`app/core/core.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('core', ['core.phone']);
|
||||
```
|
||||
|
||||
We need to add the `core.phone` module as a dependency of the `core` module.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
Our custom resource service will be defined in `app/core/phone/phone.service.js`, so we need to
|
||||
include this file and the associated `.module.js` file in our layout template. Additionally, we also
|
||||
need to load the `angular-resource.js` file, which contains the `ngResource` module:
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
...
|
||||
<script src="core/phone/phone.module.js"></script>
|
||||
<script src="core/phone/phone.service.js"></script>
|
||||
...
|
||||
</head>
|
||||
```
|
||||
|
||||
|
||||
## Component Controllers
|
||||
|
||||
We can now simplify our component controllers (`PhoneListController` and `PhoneDetailController`) by
|
||||
factoring out the lower-level `$http` service, replacing it with the new `Phone` service. Angular's
|
||||
`$resource` service is easier to use than `$http` for interacting with data sources exposed as
|
||||
RESTful resources. It is also easier now to understand what the code in our controllers is doing.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phoneList', ['core.phone']);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ['Phone',
|
||||
function PhoneListController(Phone) {
|
||||
this.phones = Phone.query();
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phoneDetail', ['core.phone']);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
templateUrl: 'phone-detail/phone-detail.template.html',
|
||||
controller: ['$routeParams', 'Phone',
|
||||
function PhoneDetailController($routeParams, Phone) {
|
||||
var self = this;
|
||||
self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
|
||||
self.setImage(phone.images[0]);
|
||||
});
|
||||
|
||||
self.setImage = function setImage(imageUrl) {
|
||||
self.mainImageUrl = imageUrl;
|
||||
};
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
Notice how in `PhoneListController` we replaced:
|
||||
|
||||
```js
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
```
|
||||
|
||||
with just:
|
||||
|
||||
```js
|
||||
this.phones = Phone.query();
|
||||
```
|
||||
|
||||
This is a simple and declarative statement that we want to query for all phones.
|
||||
|
||||
An important thing to notice in the code above is that we don't pass any callback functions, when
|
||||
invoking methods of our `Phone` service. Although it looks as if the results were returned
|
||||
synchronously, that is not the case at all. What is returned synchronously is a "future" — an
|
||||
object, which will be filled with data, when the XHR response is received. Because of the
|
||||
data-binding in Angular, we can use this future and bind it to our template. Then, when the data
|
||||
arrives, the view will be updated automatically.
|
||||
|
||||
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
|
||||
we require, so in these cases, we can add a callback to process the server response. The
|
||||
`phoneDetail` component's controller illustrates this by setting the `mainImageUrl` in a callback.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
Because we are now using the {@link ngResource ngResource} module, it is necessary to update the
|
||||
Karma configuration file with angular-resource.
|
||||
|
||||
<br />
|
||||
**`karma.conf.js`:**
|
||||
|
||||
```js
|
||||
files: [
|
||||
'bower_components/angular/angular.js',
|
||||
'bower_components/angular-resource/angular-resource.js',
|
||||
...
|
||||
],
|
||||
```
|
||||
|
||||
We have added a unit test to verify that our new service is issuing HTTP requests and returns the
|
||||
expected "future" objects/arrays.
|
||||
|
||||
The {@link ngResource.$resource $resource} service augments the response object with extra methods
|
||||
— e.g. for updating and deleting the resource — and properties (some of which are only
|
||||
meant to be accessed by Angular). If we were to use Jasmine's standard `.toEqual()` matcher, our
|
||||
tests would fail, because the test values would not match the responses exactly.
|
||||
|
||||
To solve the problem, we instruct Jasmine to use a [custom equality tester][jasmine-equality] for
|
||||
comparing objects. We specify {@link angular.equals angular.equals} as our equality tester, which
|
||||
ignores functions and `$`-prefixed properties, such as those added by the `$resource` service.<br />
|
||||
(Remember that Angular uses the `$` prefix for its proprietary API.)
|
||||
|
||||
<br />
|
||||
**`app/core/phone/phone.service.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('Phone', function() {
|
||||
...
|
||||
var phonesData = [...];
|
||||
|
||||
// Add a custom equality tester before each test
|
||||
beforeEach(function() {
|
||||
jasmine.addCustomEqualityTester(angular.equals);
|
||||
});
|
||||
|
||||
// Load the module that contains the `Phone` service before each test
|
||||
...
|
||||
|
||||
// Instantiate the service and "train" `$httpBackend` before each test
|
||||
...
|
||||
|
||||
// Verify that there are no outstanding expectations or requests after each test
|
||||
afterEach(function () {
|
||||
$httpBackend.verifyNoOutstandingExpectation();
|
||||
$httpBackend.verifyNoOutstandingRequest();
|
||||
});
|
||||
|
||||
it('should fetch the phones data from `/phones/phones.json`', function() {
|
||||
var phones = Phone.query();
|
||||
|
||||
expect(phones).toEqual([]);
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(phones).toEqual(phonesData);
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Here we are using `$httpBackend`'s
|
||||
{@link ngMock.$httpBackend#verifyNoOutstandingExpectation verifyNoOutstandingExpectation()} and
|
||||
{@link ngMock.$httpBackend#verifyNoOutstandingExpectation verifyNoOutstandingRequest()} methods to
|
||||
verify that all expected requests have been sent and that no extra request is scheduled for later.
|
||||
|
||||
Note that we have also modified our component tests to use the custom matcher when appropriate.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs)
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that we have seen how to build a custom service as a RESTful client, we are ready for
|
||||
{@link step_14 step 14} to learn how to enhance the user experience with animations.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="13"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io/
|
||||
[jasmine-equality]: https://jasmine.github.io/2.4/custom_equality.html
|
||||
[restful]: https://en.wikipedia.org/wiki/Representational_State_Transfer
|
||||
@@ -0,0 +1,564 @@
|
||||
@ngdoc tutorial
|
||||
@name 14 - Animations
|
||||
@step 14
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="14"></ul>
|
||||
|
||||
|
||||
In this step, we will enhance our web application by adding CSS and JavaScript animations on top of
|
||||
the template code we created earlier.
|
||||
|
||||
* We now use the {@link ngAnimate ngAnimate} module to enable animations throughout the application.
|
||||
* We also rely on built-in directives to automatically trigger hooks for animations to tap into.
|
||||
* When an animation is found, it will run along with the actual DOM operation that is being issued
|
||||
on the element at the given time (e.g. inserting/removing nodes on {@link ngRepeat ngRepeat} or
|
||||
adding/removing classes on {@link ngClass ngClass}).
|
||||
|
||||
|
||||
<div doc-tutorial-reset="14"></div>
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
The animation functionality is provided by Angular in the `ngAnimate` module, which is distributed
|
||||
separately from the core Angular framework. In addition we will use [jQuery][jquery] in this project
|
||||
to do extra JavaScript animations.
|
||||
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependencies:
|
||||
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-animate": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-resource": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"bootstrap": "3.3.x",
|
||||
"jquery": "2.2.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `"angular-animate": "1.5.x"` tells bower to install a version of the angular-animate module that
|
||||
is compatible with version 1.5.x of Angular.
|
||||
* `"jquery": "2.2.x"` tells bower to install the latest patch release of the 2.2 version of jQuery.
|
||||
Note that this is not an Angular library; it is the standard jQuery library. We can use bower to
|
||||
install a wide range of 3rd party libraries.
|
||||
|
||||
Now, we must tell bower to download and install these dependencies.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## How Animations work with `ngAnimate`
|
||||
|
||||
To get an idea of how animations work with AngularJS, you might want to read the
|
||||
[Animations](guide/animations) section of the Developer Guide first.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
In order to enable animations, we need to update `index.html`, loading the necessary dependencies
|
||||
(**angular-animate.js** and **jquery.js**) and the files that contain the CSS and JavaScript code
|
||||
used in CSS/JavaScript animations. The animation module, {@link ngAnimate ngAnimate}, contains the
|
||||
code necessary to make your application "animation aware".
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
|
||||
<!-- Defines CSS necessary for animations -->
|
||||
<link rel="stylesheet" href="app.animations.css" />
|
||||
|
||||
...
|
||||
|
||||
<!-- Used for JavaScript animations (include this before angular.js) -->
|
||||
<script src="bower_components/jquery/dist/jquery.js"></script>
|
||||
|
||||
...
|
||||
|
||||
<!-- Adds animation support in AngularJS -->
|
||||
<script src="bower_components/angular-animate/angular-animate.js"></script>
|
||||
|
||||
<!-- Defines JavaScript animations -->
|
||||
<script src="app.animations.js"></script>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
<div class="alert alert-error">
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer, when using Angular 1.5; jQuery 1.x is
|
||||
not officially supported.
|
||||
In order for Angular to detect jQuery and take advantage of it, make sure to include `jquery.js`
|
||||
before `angular.js`.
|
||||
</div>
|
||||
|
||||
Animations can now be created within the CSS code (`app.animations.css`) as well as the JavaScript
|
||||
code (`app.animations.js`).
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
We need to add a dependency on `ngAnimate` to our main module first:
|
||||
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp', [
|
||||
'ngAnimate',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
Now that our application is "animation aware", let's create some fancy animations!
|
||||
|
||||
|
||||
## CSS Transition Animations: Animating `ngRepeat`
|
||||
|
||||
We will start off by adding CSS transition animations to our `ngRepeat` directive present on the
|
||||
`phoneList` component's template. We need to add an extra CSS class to our repeated element, in
|
||||
order to be able to hook into it with our CSS animation code.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp"
|
||||
class="thumbnail phone-list-item">
|
||||
<a href="#!/phones/{{phone.id}}" class="thumb">
|
||||
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
|
||||
</a>
|
||||
<a href="#!/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
Did you notice the added `phone-list-item` CSS class? This is all we need in our HTML code to get
|
||||
animations working.
|
||||
|
||||
Now for the actual CSS transition animation code:
|
||||
|
||||
<br />
|
||||
**`app/app.animations.css`:**
|
||||
|
||||
```css
|
||||
.phone-list-item.ng-enter,
|
||||
.phone-list-item.ng-leave,
|
||||
.phone-list-item.ng-move {
|
||||
transition: 0.5s linear all;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-enter,
|
||||
.phone-list-item.ng-move {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-enter.ng-enter-active,
|
||||
.phone-list-item.ng-move.ng-move-active {
|
||||
height: 120px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-leave {
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-leave.ng-leave-active {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, our `phone-list-item` CSS class is combined together with the animation hooks that
|
||||
occur when items are inserted into and removed from the list:
|
||||
|
||||
* The `ng-enter` class is applied to the element when a new phone is added to the list and rendered
|
||||
on the page.
|
||||
* The `ng-move` class is applied to the element when a phone's relative position in the list
|
||||
changes.
|
||||
* The `ng-leave` class is applied to the element when a phone is removed from the list.
|
||||
|
||||
The phone list items are added and removed based on the data passed to the `ngRepeat` directive.
|
||||
For example, if the filter data changes, the items will be animated in and out of the repeat list.
|
||||
|
||||
Something important to note is that, when an animation occurs, two sets of CSS classes are added to
|
||||
the element:
|
||||
|
||||
1. A "starting" class that represents the style at the beginning of the animation.
|
||||
2. An "active" class that represents the style at the end of the animation.
|
||||
|
||||
The name of the starting class is the name of the event that is fired (like `enter`, `move` or
|
||||
`leave`) prefixed with `ng-`. So an `enter` event will result in adding the `ng-enter` class.
|
||||
|
||||
The active class name is derived from the starting class by appending an `-active` suffix.
|
||||
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
|
||||
|
||||
In the example above, animated elements are expanded from a height of **0px** to **120px** when they
|
||||
are added to the list and are collapsed back down to **0px** before being removed from the list.
|
||||
There is also a catchy fade-in/fade-out effect that occurs at the same time. All this is handled by
|
||||
the CSS transition declaration at the top of the CSS file.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Although all modern browsers have good support for [CSS transitions][caniuse-css-transitions] and
|
||||
[CSS animations][caniuse-css-animation], IE9 and earlier IE versions do not.
|
||||
If you want animations that are backwards-compatible with older browsers, consider using
|
||||
JavaScript-based animations, which are demonstrated below.
|
||||
</div>
|
||||
|
||||
|
||||
## CSS Keyframe Animations: Animating `ngView`
|
||||
|
||||
Next, let's add an animation for transitions between route changes in
|
||||
{@link ngRoute.directive:ngView ngView}.
|
||||
|
||||
Again, we need to prepare our HTML template by adding a new CSS class, this time to the `ng-view`
|
||||
element. In order to gain more "expressive power" for our animations, we will also wrap the
|
||||
`[ng-view]` element in a container element.
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<div class="view-container">
|
||||
<div ng-view class="view-frame"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We have applied a `position: relative` CSS style to the `.view-container` wrapper, so that it is
|
||||
easier for us to manage the `.view-frame` element's positioning during the animation.
|
||||
|
||||
With our preparation code in place, let's move on to the actual CSS styles for this transition
|
||||
animation.
|
||||
|
||||
<br />
|
||||
**`app/app.animations.css`:**
|
||||
|
||||
```css
|
||||
...
|
||||
|
||||
.view-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter,
|
||||
.view-frame.ng-leave {
|
||||
background: white;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter {
|
||||
animation: 1s fade-in;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.view-frame.ng-leave {
|
||||
animation: 1s fade-out;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/* Older browsers might need vendor-prefixes for keyframes and animation! */
|
||||
```
|
||||
|
||||
Nothing fancy here! Just a simple fade-in/fade-out effect between pages. The only thing out of the
|
||||
ordinary here is that we are using absolute positioning to position the entering page (identified
|
||||
by the `ng-enter` class) on top of the leaving page (identified by the `ng-leave` class). At the
|
||||
same time a cross-fade animation is performed. So, as the previous page is just about to be removed,
|
||||
it fades out, while the new page fades in right on top of it.
|
||||
|
||||
Once the `leave` animation is over, the element is removed from the DOM. Likewise, once the `enter`
|
||||
animation is complete, the `ng-enter` and `ng-enter-active` CSS classes are removed from the
|
||||
element, causing it to rerender and reposition itself with its default CSS styles (so no more
|
||||
absolute positioning once the animation is over). This works fluidly and the pages flow naturally
|
||||
between route changes, without anything jumping around.
|
||||
|
||||
The applied CSS classes are much the same as with `ngRepeat`. Each time a new page is loaded the
|
||||
`ngView` directive will create a copy of itself, download the template and append the contents. This
|
||||
ensures that all views are contained within a single HTML element, which allows for easy animation
|
||||
control.
|
||||
|
||||
For more on CSS animations, see the [Web Platform documentation][webplatform-animations].
|
||||
|
||||
|
||||
## Animating `ngClass` with JavaScript
|
||||
|
||||
Let's add another animation to our application. On our `phone-detail.template.html` view, we have a
|
||||
nice thumbnail swapper. By clicking on the thumbnails listed on the page, the profile phone image
|
||||
changes. But how can we incorporate animations?
|
||||
|
||||
Let's give it some thought first. Basically, when a user clicks on a thumbnail image, they are
|
||||
changing the state of the profile image to reflect the newly selected thumbnail image. The best way
|
||||
to specify state changes within HTML is to use classes. Much like before — when we used a CSS
|
||||
class to drive the animation — this time the animation will occur when the CSS class itself
|
||||
changes.
|
||||
|
||||
Every time a phone thumbnail is selected, the state changes and the `.selected` CSS class is added
|
||||
to the matching profile image. This will trigger the animation.
|
||||
|
||||
We will start by tweaking our HTML code in `phone-detail.template.html`. Notice that we have changed
|
||||
the way we display our large image:
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
```html
|
||||
<div class="phone-images">
|
||||
<img ng-src="{{img}}" class="phone"
|
||||
ng-class="{selected: img === $ctrl.mainImageUrl}"
|
||||
ng-repeat="img in $ctrl.phone.images" />
|
||||
</div>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Just like with the thumbnails, we are using a repeater to display **all** the profile images as a
|
||||
list, however we're not animating any repeat-related transitions. Instead, we will be keeping our
|
||||
eye on each element's classes and especially the `selected` class, since its presence or absence
|
||||
will determine if the element is visible or hidden. The addition/removal of the `selected` class is
|
||||
managed by the {@link ngClass ngClass} directive, based on the specified condition
|
||||
(`img === $ctrl.mainImageUrl`).
|
||||
In our case, there is always exactly one element that has the `selected` class, and therefore there
|
||||
will be exactly one phone profile image visible on the screen at all times.
|
||||
|
||||
When the `selected` class is added to an element, the `selected-add` and `selected-add-active`
|
||||
classes are added just before to signal AngularJS to fire off an animation. When the `selected`
|
||||
class is removed from an element, the `selected-remove` and `selected-remove-active` classes are
|
||||
applied to the element, triggering another animation.
|
||||
|
||||
Finally, in order to ensure that the phone images are displayed correctly when the page is first
|
||||
loaded, we also tweak the detail page CSS styles:
|
||||
|
||||
<br />
|
||||
**`app/app.css`:**
|
||||
|
||||
```css
|
||||
...
|
||||
|
||||
.phone {
|
||||
background-color: white;
|
||||
display: none;
|
||||
float: left;
|
||||
height: 400px;
|
||||
margin-bottom: 2em;
|
||||
margin-right: 3em;
|
||||
padding: 2em;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.phone:first-child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.phone-images {
|
||||
background-color: white;
|
||||
float: left;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You may be thinking that we are just going to create another CSS-based animation. Although we could
|
||||
do that, let's take the opportunity to learn how to create JavaScript-based animations with the
|
||||
{@link ng.angular.Module#animation .animation()} module method.
|
||||
|
||||
<br />
|
||||
**`app/app.animations.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
animation('.phone', function phoneAnimationFactory() {
|
||||
return {
|
||||
addClass: animateIn,
|
||||
removeClass: animateOut
|
||||
};
|
||||
|
||||
function animateIn(element, className, done) {
|
||||
if (className !== 'selected') return;
|
||||
|
||||
element.
|
||||
css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: 500,
|
||||
left: 0
|
||||
}).
|
||||
animate({
|
||||
top: 0
|
||||
}, done);
|
||||
|
||||
return function animateInEnd(wasCanceled) {
|
||||
if (wasCanceled) element.stop();
|
||||
};
|
||||
}
|
||||
|
||||
function animateOut(element, className, done) {
|
||||
if (className !== 'selected') return;
|
||||
|
||||
element.
|
||||
css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0
|
||||
}).
|
||||
animate({
|
||||
top: -500
|
||||
}, done);
|
||||
|
||||
return function animateOutEnd(wasCanceled) {
|
||||
if (wasCanceled) element.stop();
|
||||
};
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
We are creating a custom animation by specifying the target elements via a CSS class selector (here
|
||||
`.phone`) and an animation _factory_ function (here `phoneAnimationFactory()`). The factory function
|
||||
returns an object associating specific _events_ (object keys) to animation _callbacks_ (object
|
||||
values). The _events_ correspond to DOM actions that `ngAnimate` recognizes and can hook into, such
|
||||
as `addClass`/`removeClass`/`setClass`, `enter`/`move`/`leave` and `animate`. The associated
|
||||
callbacks are called by `ngAnimate` at appropriate times.
|
||||
|
||||
For more info on animation factories, check out the
|
||||
{@link ng.$animateProvider#register API Reference}.
|
||||
|
||||
In this case, we are interested in a class getting added to/removed from a `.phone` element, thus we
|
||||
specify callbacks for the `addClass` and `removeClass` events. When the `selected` class is added to
|
||||
an element (via the `ngClass` directive), the `addClass` JavaScript callback will be executed with
|
||||
`element` passed in as a parameter. The last parameter passed in is the `done` callback function. We
|
||||
call `done()` to let Angular know that our custom JavaScript animation has ended. The `removeClass`
|
||||
callback works the same way, but instead gets executed when a class is removed.
|
||||
|
||||
Note that we are using [jQuery][jquery]'s `animate()` helper to implement the animation. jQuery
|
||||
isn't required to do JavaScript animations with AngularJS, but we use it here anyway in order to
|
||||
keep the example simple. More info on `jQuery.animate()` can be found in the
|
||||
[jQuery documentation][jquery-animate].
|
||||
|
||||
Within the event callbacks, we create the animation by manipulating the DOM. In the code above,
|
||||
this is achieved using `element.css()` and `element.animate()`. As a result the new element is
|
||||
positioned with an offset of **500px** and then both elements — the previous and the new
|
||||
— are animated together by shifting each one up by **500px**. The outcome is a conveyor-belt
|
||||
like animation. After the `animate()` function has completed the animation, it calls `done` to
|
||||
notify Angular.
|
||||
|
||||
You may have noticed that each animation callback returns a function. This is an **optional**
|
||||
function, which (if provided) will be called when the animation ends, either because it ran to
|
||||
completion or because it was canceled (for example another animation took place on the same
|
||||
element). A boolean parameter (`wasCanceled`) is passed to the function, letting the developer know
|
||||
if the animation was canceled or not. Use this function to do any necessary clean-up.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Reverse the animation, so that the elements animate downwards.
|
||||
|
||||
* Make the animation run faster or slower, by passing a `duration` argument to `.animate()`:
|
||||
|
||||
```js
|
||||
element.css({...}).animate({...}, 1000 /* 1 second */, done);
|
||||
```
|
||||
|
||||
* Make the animations "asymmetrical". For example, have the previous element fade out, while the new
|
||||
element zooms in:
|
||||
|
||||
```js
|
||||
// animateIn()
|
||||
element.css({
|
||||
display: 'block',
|
||||
opacity: 1,
|
||||
position: 'absolute',
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 200,
|
||||
left: 200
|
||||
}).animate({
|
||||
width: 400,
|
||||
height: 400,
|
||||
top: 0,
|
||||
left: 0
|
||||
}, done);
|
||||
|
||||
// animateOut()
|
||||
element.animate({
|
||||
opacity: 0
|
||||
}, done);
|
||||
```
|
||||
|
||||
* Go crazy and come up with your own funky animations!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Our application is now much more pleasant to use, thanks to the smooth transitions between pages
|
||||
and UI states.
|
||||
|
||||
There you have it! We have created a web application in a relatively short amount of time. In the
|
||||
{@link the_end closing notes} we will cover where to go from here.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="14"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io/
|
||||
[caniuse-css-animation]: http://caniuse.com/#feat=css-animation
|
||||
[caniuse-css-transitions]: http://caniuse.com/#feat=css-transitions
|
||||
[jquery]: https://jquery.com/
|
||||
[jquery-animate]: https://api.jquery.com/animate/
|
||||
[webplatform-animations]: https://docs.webplatform.org/wiki/css/properties/animations
|
||||
@@ -9,11 +9,18 @@ previous steps using the `git checkout` command.
|
||||
For more details and examples of the Angular concepts we touched on in this tutorial, see the
|
||||
{@link guide/ Developer Guide}.
|
||||
|
||||
When you are ready to start developing a project using Angular, we recommend that you bootstrap
|
||||
your development with the [angular-seed](https://github.com/angular/angular-seed) project.
|
||||
When you are ready to start developing a project using AngularJS, we recommend that you bootstrap
|
||||
your development with the [angular-seed project][angular-seed].
|
||||
|
||||
We hope this tutorial was useful to you and that you learned enough about Angular to make you want
|
||||
to learn more. We especially hope you are inspired to go out and develop Angular web apps of your
|
||||
own, and that you might be interested in {@link misc/contribute contributing} to Angular.
|
||||
We hope this tutorial was useful to you and that you learned enough about AngularJS to make you want
|
||||
to learn more. We especially hope you are inspired to go out and develop Angular web applications of
|
||||
your own, and that you might be interested in {@link misc/contribute contributing} to AngularJS.
|
||||
|
||||
If you have questions or feedback or just want to say "hi", please post a message at (https://groups.google.com/forum/#!forum/angular).
|
||||
If you have questions or feedback or just want to say "hi", please post a message to the
|
||||
[mailing list][mailing-list]. You can also find us on [IRC][irc] or [Gitter][gitter].
|
||||
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
[gitter]: https://gitter.im/angular/angular.js
|
||||
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
|
||||
[mailing-list]: https://groups.google.com/forum/#!forum/angular
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 576 733" width="48pc" height="733pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.5.2 <dc:date>2016-04-12 13:18:31 +0000</dc:date></metadata><defs><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Notes</title><rect fill="white" width="576" height="733"/><g><title>Layer 1</title><text transform="translate(57 78)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x="0" y="11" textLength="106.01367">Guiding Principles</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="25" textLength="126.73828">* Convention over Confi</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="126.73828" y="25" textLength="43.365234">guration</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="39" textLength="158.73047">* Declarative / Self Describing</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="53" textLength="8.0039062">* </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="7.7929688" y="53" textLength="7.330078">T</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="13.792969" y="53" textLength="38.695312">estable</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="67" textLength="25.335938">* DR</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="25.125" y="67" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="32.917969" y="67" textLength="80.33203"> (Don't Repeat </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="113.03906" y="67" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="119.941406" y="67" textLength="40.013672">ourself)</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="81" textLength="171.38086">* CRUD ~ 80% -> make it trivial </tspan></text></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 667 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 756 553" width="63pc" height="553pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.5.2 <dc:date>2013-11-06 19:46:59 +0000</dc:date></metadata><defs/><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>filter</title><rect fill="white" width="756" height="553"/></g></svg>
|
||||
|
After Width: | Height: | Size: 611 B |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 756 553" width="63pc" height="553pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.5.2 <dc:date>2013-11-06 19:46:59 +0000</dc:date></metadata><defs><font-face font-family="Courier" font-size="11" units-per-em="1000" underline-position="-178.22266" underline-thickness="57.617188" slope="0" x-height="462.40234" cap-height="594.72656" ascent="753.90625" descent="-246.09375" font-weight="500"><font-face-src><font-face-name name="Courier"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>scope</title><rect fill="white" width="756" height="553"/><g><title>Runtime</title><rect x="13.333394" y="39.39392" width="523" height="343" id="path"/><clipPath id="clip_path"><use xl:href="#path"/></clipPath><g clip-path="url(#clip_path)"><image xl:href="image4.png" width="523" height="343" transform="translate(13.333394 39.39392)"/></g><rect x="23.272705" y="52.298843" width="613.1692" height="335.03439" fill="maroon" fill-opacity=".1"/><rect x="23.272705" y="52.298843" width="613.1692" height="335.03439" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(28.272705 52.298843)" fill="red"><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="563.56274" y="10" textLength="39.606445">$scope</tspan></text><rect x="57.40342" y="149.9501" width="540.7439" height="202.0499" fill="maroon" fill-opacity=".1"/><rect x="57.40342" y="149.9501" width="540.7439" height="202.0499" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(62.40342 149.9501)" fill="red"><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="491.13745" y="10" textLength="39.606445">$scope</tspan><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="458.13208" y="23" textLength="72.611816">names=[...]</tspan></text><rect x="89.999985" y="271.81976" width="477.3466" height="37.28366" fill="maroon" fill-opacity=".1"/><rect x="89.999985" y="271.81976" width="477.3466" height="37.28366" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(94.999985 271.81976)" fill="red"><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="427.74014" y="10" textLength="39.606445">$scope</tspan><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="388.1337" y="23" textLength="79.21289">name='Vojta'</tspan></text><rect x="89.999985" y="191.33336" width="477.3466" height="37.28366" fill="maroon" fill-opacity=".1"/><rect x="89.999985" y="191.33336" width="477.3466" height="37.28366" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(94.999985 191.33336)" fill="red"><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="427.74014" y="10" textLength="39.606445">$scope</tspan><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="394.73477" y="23" textLength="72.611816">name='Igor'</tspan></text><rect x="89.999985" y="231.70264" width="477.3466" height="37.28366" fill="maroon" fill-opacity=".1"/><rect x="89.999985" y="231.70264" width="477.3466" height="37.28366" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(94.999985 231.70264)" fill="red"><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="427.74014" y="10" textLength="39.606445">$scope</tspan><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="388.1337" y="23" textLength="79.21289">name='Misko'</tspan></text><rect x="56.87275" y="105.99997" width="540.7439" height="41.36985" fill="maroon" fill-opacity=".1"/><rect x="56.87275" y="105.99997" width="540.7439" height="41.36985" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(61.87275 105.99997)" fill="red"><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="491.13745" y="10" textLength="39.606445">$scope</tspan><tspan font-family="Courier" font-size="11" font-weight="500" fill="red" x="451.531" y="23" textLength="79.21289">name='World'</tspan></text></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.1 KiB |