docs: remove public/docs/dart (#2956)
* tutorial when back ported * api when backported * guide when backported * quickstart, cheatsheet and glossary when backported * remove Jekyll front matter from each back ported file * backport Dart _util-fns.jade * remove docs/dart * gitignore docs/dart * only include Dart in language menu if it exists * README: remove mention of Dart
This commit is contained in:
parent
cf97326344
commit
e469929123
|
@ -32,4 +32,5 @@ public/docs/*/latest/guide/cheatsheet.json
|
||||||
protractor-results.txt
|
protractor-results.txt
|
||||||
link-checker-results.txt
|
link-checker-results.txt
|
||||||
*a2docs.css
|
*a2docs.css
|
||||||
/dist
|
/dist
|
||||||
|
/public/docs/dart
|
11
README.md
11
README.md
|
@ -35,19 +35,13 @@ if not install [nvm](https://github.com/creationix/nvm) to get node going on you
|
||||||
|
|
||||||
- this repo
|
- this repo
|
||||||
- [angular/angular source code repo](https://github.com/angular/angular)
|
- [angular/angular source code repo](https://github.com/angular/angular)
|
||||||
- (OPTIONAL) [dart-lang/angular2 source code repo](https://github.com/dart-lang/angular2);
|
|
||||||
cloning the Angular Dart repo is only necessary if you plan on doing full site builds
|
|
||||||
|
|
||||||
to the same parent directory. The **cloned repo directories must be siblings**, with the latter two repo directories named **angular** and **angular-dart**, respectively.
|
to the same parent directory. The **cloned repo directories must be siblings**, with the latter named **angular**.
|
||||||
|
|
||||||
1. cd into root directory `angular.io/`
|
1. cd into root directory `angular.io/`
|
||||||
|
|
||||||
1. Install local npm packages by running `./scripts/install.sh`
|
1. Install local npm packages by running `./scripts/install.sh`
|
||||||
|
|
||||||
1. (OPTIONAL) If you intend on doing **full site builds** then you must have the
|
|
||||||
Angular Dart repo (see the Clone step above), _and_ Dart tooling available.
|
|
||||||
Both can be installed by running `./scripts/deploy-install.sh`
|
|
||||||
|
|
||||||
1. See [below](#code-sample-development) for code sample development preparation.
|
1. See [below](#code-sample-development) for code sample development preparation.
|
||||||
|
|
||||||
## Content Development
|
## Content Development
|
||||||
|
@ -116,8 +110,7 @@ You must check that your example is free of lint errors.
|
||||||
|
|
||||||
All samples should be covered to some degree by end-to-end tests:
|
All samples should be covered to some degree by end-to-end tests:
|
||||||
- `gulp run-e2e-tests` to run all TypeScript and JavaScript tests
|
- `gulp run-e2e-tests` to run all TypeScript and JavaScript tests
|
||||||
- `gulp run-e2e-tests --lang=dart` to run all Dart tests
|
- `gulp run-e2e-tests --lang=all` to run TypeScript and JavaScript tests
|
||||||
- `gulp run-e2e-tests --lang=all` to run TypeScript, JavaScript, and Dart tests
|
|
||||||
- `gulp run-e2e-tests --filter=quickstart` to filter the examples to run, by name
|
- `gulp run-e2e-tests --filter=quickstart` to filter the examples to run, by name
|
||||||
- `gulp run-e2e-tests --fast` to ignore npm install, webdriver update and boilerplate copy
|
- `gulp run-e2e-tests --fast` to ignore npm install, webdriver update and boilerplate copy
|
||||||
|
|
||||||
|
|
|
@ -72,5 +72,6 @@ if current.path[4] !== 'change-log'
|
||||||
mixin tree(public.docs.ts, "/docs/ts", "Angular for TypeScript")
|
mixin tree(public.docs.ts, "/docs/ts", "Angular for TypeScript")
|
||||||
mixin tree(public.docs.js, "/docs/js", "Angular for JavaScript")
|
mixin tree(public.docs.js, "/docs/js", "Angular for JavaScript")
|
||||||
//- Disable cross-language link for API entry pages (but keep for top API search page):
|
//- Disable cross-language link for API entry pages (but keep for top API search page):
|
||||||
if ! (current.path[3] === 'api' && public.docs[current.path[1]][current.path[2]][current.path[3]][current.path[4]])
|
- var isApiEntryPage = current.path[3] === 'api' && public.docs[current.path[1]][current.path[2]][current.path[3]][current.path[4]]
|
||||||
|
if public.docs.dart && !isApiEntryPage
|
||||||
mixin tree(public.docs.dart, "/docs/dart", "Angular for Dart")
|
mixin tree(public.docs.dart, "/docs/dart", "Angular for Dart")
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
{
|
|
||||||
"index": {
|
|
||||||
"icon": "home",
|
|
||||||
"title": "Angular Docs",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"menuTitle": "Docs Home",
|
|
||||||
"banner": "AngularDart is <b>2.2</b>. View the <a href='https://github.com/dart-lang/angular2/blob/master/CHANGELOG.md' target='_blank'>change log</a> to see enhancements, fixes, and breaking changes."
|
|
||||||
},
|
|
||||||
|
|
||||||
"cli-quickstart": {
|
|
||||||
"icon": "query-builder",
|
|
||||||
"title": "CLI Quickstart",
|
|
||||||
"subtitle": "TypeScript",
|
|
||||||
"description": "Use the CLI tool to quickly build Angular applications",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"quickstart": {
|
|
||||||
"icon": "query-builder",
|
|
||||||
"title": "Quickstart",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"banner": "A quick look at Angular basics"
|
|
||||||
},
|
|
||||||
|
|
||||||
"tutorial": {
|
|
||||||
"icon": "list",
|
|
||||||
"title": "Tutorial",
|
|
||||||
"subtitle": "Dart"
|
|
||||||
},
|
|
||||||
|
|
||||||
"guide": {
|
|
||||||
"icon": "list",
|
|
||||||
"title": "Guide",
|
|
||||||
"subtitle": "Dart"
|
|
||||||
},
|
|
||||||
|
|
||||||
"cookbook": {
|
|
||||||
"icon": "list",
|
|
||||||
"title": "Cookbook",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"banner": "How to solve common implementation challenges."
|
|
||||||
},
|
|
||||||
|
|
||||||
"api/": {
|
|
||||||
"icon": "book",
|
|
||||||
"title": "API Reference",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"reference": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"cheatsheet": {
|
|
||||||
"title": "Cheat Sheet",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"reference": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"glossary": {
|
|
||||||
"title": "Glossary",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"intro": "Brief definitions of the most important words in the Angular vocabulary",
|
|
||||||
"reference": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"resources": {
|
|
||||||
"icon": "play-circle-fill",
|
|
||||||
"title": "Angular Resources",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"resources": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"help": {
|
|
||||||
"icon": "chat",
|
|
||||||
"title": "Help & Support",
|
|
||||||
"subtitle": "From our team & community",
|
|
||||||
"resources": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"styleguide": {
|
|
||||||
"title": "Docs Style Guide",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"intro": "Design & Layout Patterns For Documentation"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
include ../../../_includes/_util-fns
|
|
||||||
|
|
||||||
//- See the _util-fns file included above for a description of the use of these variables.
|
|
||||||
- var _docsFor = 'dart';
|
|
||||||
- var _decorator = 'annotation';
|
|
||||||
- var _Array = 'List';
|
|
||||||
- var _array = 'list';
|
|
||||||
- var _a = 'an';
|
|
||||||
- var _an = 'a';
|
|
||||||
- var _priv = '_';
|
|
||||||
- var _Lang = 'Dart';
|
|
||||||
- var _Promise = 'Future';
|
|
||||||
- var _FutureUrl = 'https://api.dartlang.org/dart_async/Future.html';
|
|
||||||
- var _PromiseLinked = '<a href="' + _FutureUrl + '">' + _Promise + '</a>';
|
|
||||||
- var _Observable = 'Stream';
|
|
||||||
- var _liveLink = 'sample repo';
|
|
||||||
- var _truthy = 'true';
|
|
||||||
- var _falsey = 'false';
|
|
||||||
- var _appDir = 'lib';
|
|
||||||
- var _indexHtmlDir = 'web';
|
|
||||||
- var _mainDir = 'web';
|
|
||||||
- var _ngRepoURL = 'https://github.com/dart-lang/angular2';
|
|
||||||
//- Don't override this value quite yet:
|
|
||||||
//- var _ngDocRepoURL = 'https://github.com/dart-lang/site-webdev';
|
|
||||||
- var _qsRepo = 'https://github.com/angular-examples/quickstart'
|
|
||||||
- var _qsRepoZip = _qsRepo + '/archive/master.zip';
|
|
||||||
|
|
||||||
- var _npm = 'pub';
|
|
||||||
|
|
||||||
//- NgModule related
|
|
||||||
- var _AppModuleVsAppComp = 'AppComponent'
|
|
||||||
- var _appModuleTsVsAppCompTs = 'app/app_component.dart'
|
|
||||||
- var _appModuleTsVsMainTs = 'web/main.dart'
|
|
||||||
- var _bootstrapModule = 'bootstrap'
|
|
||||||
- var _declsVsDirectives = 'directives'
|
|
||||||
- var _moduleVsComp = 'component'
|
|
||||||
- var _moduleVsRootComp = 'root component'
|
|
||||||
- var _platformBrowserDynamicVsBootStrap = 'bootstrap'
|
|
||||||
|
|
||||||
- var adjustTsExamplePathForDart = function(_path) {
|
|
||||||
- if(!_path) return _path;
|
|
||||||
- var path = _path.trim();
|
|
||||||
- var folder = getFolder(path);
|
|
||||||
- var extn = getExtn(path);
|
|
||||||
- // if(extn == 'dart') return path;
|
|
||||||
- var baseName = getBaseFileName(path) || path; // TODO: have getBaseFileName() return path
|
|
||||||
- var baseNameNoExt = baseName.substr(0,baseName.length - (extn.length + 1));
|
|
||||||
- var inWebFolder = baseNameNoExt.match(/^(main|index)(\.\d)?$/);
|
|
||||||
- // Adjust the folder path, e.g., ts -> dart
|
|
||||||
- folder = folder.replace(/(^|\/)ts($|\/)/, '$1dart$2').replace(/(^|\/)app($|\/)/, inWebFolder ? '$1web$2' : '$1lib$2');
|
|
||||||
- // Special case not handled above: e.g., index.html -> web/index.html
|
|
||||||
- if(baseNameNoExt.match(/^(index|styles)(\.\d)?$/) && !folder.match(/web$/)) folder = (folder ? folder + '/' : '') + 'web';
|
|
||||||
- // In file name, replace special characters with underscore
|
|
||||||
- baseNameNoExt = baseNameNoExt.replace(/[\-\.]/g, '_');
|
|
||||||
- // Adjust the file extension
|
|
||||||
- if(extn == 'ts') extn = 'dart';
|
|
||||||
- return (folder ? folder + '/' : '') + baseNameNoExt + (extn ? '.' + extn : '');
|
|
||||||
- };
|
|
||||||
|
|
||||||
- var adjustTsExampleTitleForDart = function(_title) {
|
|
||||||
- if(!_title || !adjustTsExamplePathForDart) return _title;
|
|
||||||
- var title = _title.trim();
|
|
||||||
- // Assume title is a path if it ends with an extension like '.foo',
|
|
||||||
- // optionally followed by some comment in parentheses.
|
|
||||||
- var matches = title.match(/(.*\.\w+)($|\s*\([\w ]+\)$)/);
|
|
||||||
- if(matches && matches.length == 3) {
|
|
||||||
- // e.g. matches == ['abc.ts (excerpt)', 'abc.ts', ' (excerpt)']
|
|
||||||
- var path = adjustTsExamplePathForDart(matches[1]);
|
|
||||||
- title = path + matches[2];
|
|
||||||
- }
|
|
||||||
- return title;
|
|
||||||
- }
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"index" : {
|
|
||||||
"title" : "API Reference"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
> **Known issues:** The Angular issue tracker contains
|
|
||||||
[all known issues][api-issues]; if you notice others, please
|
|
||||||
[report them][new-issue]. Thanks!
|
|
||||||
|
|
||||||
[new-issue]: !{_ngRepoURL}/issues/new?title=%5BAPI%20docs%5D
|
|
||||||
[api-issues]: !{_ngRepoURL}/issues
|
|
||||||
//- ?q=label%3Aapi
|
|
||||||
|
|
||||||
api-list(src="api-list.json" lang="dart")
|
|
|
@ -1,4 +0,0 @@
|
||||||
- var base = current.path[4] ? '.' : './guide';
|
|
||||||
|
|
||||||
.l-content-small.docs-content.cheatsheet
|
|
||||||
ngio-cheatsheet(src= base + '/cheatsheet.json')
|
|
|
@ -1,72 +0,0 @@
|
||||||
{
|
|
||||||
"index": {
|
|
||||||
"title": "Cookbook",
|
|
||||||
"navTitle": "Overview",
|
|
||||||
"intro": "A collection of recipes for common Angular application scenarios"
|
|
||||||
},
|
|
||||||
|
|
||||||
"a1-a2-quick-reference": {
|
|
||||||
"title": "Angular 1 to 2 Quick Reference",
|
|
||||||
"navTitle": "Angular 1 to 2 Quick Ref",
|
|
||||||
"intro": "Learn how Angular 1 concepts and techniques map to Angular 2",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"ngmodule-faq": {
|
|
||||||
"title": "Angular Module FAQs",
|
|
||||||
"intro": "Answers to frequently asked questions about @NgModule",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"component-communication": {
|
|
||||||
"title": "Component Interaction",
|
|
||||||
"intro": "Share information between different directives and components"
|
|
||||||
},
|
|
||||||
|
|
||||||
"component-relative-paths": {
|
|
||||||
"title": "Component-relative Paths",
|
|
||||||
"intro": "Use relative URLs for component templates and styles.",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"dependency-injection": {
|
|
||||||
"title": "Dependency Injection",
|
|
||||||
"intro": "Techniques for Dependency Injection",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"dynamic-forms": {
|
|
||||||
"title": "Dynamic Form",
|
|
||||||
"intro": "Render dynamic forms with NgFormModel",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"form-validation": {
|
|
||||||
"title": "Form Validation",
|
|
||||||
"intro": "Validate user's form entries",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"i18n": {
|
|
||||||
"title": "Internationalization (i18n)",
|
|
||||||
"intro": "Translate the app's template text into multiple languages",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"set-document-title": {
|
|
||||||
"title": "Set the Document Title",
|
|
||||||
"intro": "Setting the document or window title using the Title service."
|
|
||||||
},
|
|
||||||
|
|
||||||
"ts-to-js": {
|
|
||||||
"title": "TypeScript to JavaScript",
|
|
||||||
"intro": "Convert Angular TypeScript examples into ES6 and ES5 JavaScript",
|
|
||||||
"hide": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"visual-studio-2015": {
|
|
||||||
"title": "Visual Studio 2015 QuickStart",
|
|
||||||
"intro": "Use Visual Studio 2015 with the QuickStart files",
|
|
||||||
"hide": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1,59 +0,0 @@
|
||||||
extends ../../ts/_cache/glossary.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include _util-fns
|
|
||||||
|
|
||||||
block annotation-defn
|
|
||||||
:marked
|
|
||||||
When unqualified, _annotation_ refers to a Dart [metadata
|
|
||||||
annotation][metadata] (as opposed to, say, a type annotation). A metadata
|
|
||||||
annotation begins with the character `@`, followed by either a reference
|
|
||||||
to a compile-time constant (such as [`Component`](#component)) or a call
|
|
||||||
to a constant constructor. See the [Dart Language Guide][metadata] for
|
|
||||||
details.
|
|
||||||
|
|
||||||
The corresponding term in TypeScript and JavaScript is
|
|
||||||
[_decorator_](https://angular.io/docs/ts/latest/glossary.html#decorator).
|
|
||||||
|
|
||||||
[metadata]: https://www.dartlang.org/guides/language/language-tour#metadata
|
|
||||||
|
|
||||||
block bootstrap-defn-top
|
|
||||||
:marked
|
|
||||||
You launch an Angular application by "bootstrapping" it with the
|
|
||||||
[bootstrap][bootstrap] method. Bootstraping identifies an
|
|
||||||
application's top level "root" [component](#component), which is
|
|
||||||
the first component that is loaded for the application, and optionally
|
|
||||||
registers service [providers](#provider) with the [dependency injection
|
|
||||||
system](#dependency-injection).
|
|
||||||
For more information, see the [Setup](!{docsLatest}/guide/setup.html) page.
|
|
||||||
|
|
||||||
[bootstrap]: !{docsLatest}/api/angular2.platform.browser/bootstrap.html
|
|
||||||
|
|
||||||
block decorator-defn
|
|
||||||
:marked
|
|
||||||
When used in this guide, these JavaScript terms are taken as synonymous with
|
|
||||||
[annotation](#annotation).
|
|
||||||
|
|
||||||
block module-defn
|
|
||||||
//- Taken from the Dart Difference in guide/architecture.jade
|
|
||||||
:marked
|
|
||||||
In this guide, the term _module_ refers to a Dart compilation unit, such
|
|
||||||
as a library, or a package. (If a Dart file has no `library` or `part`
|
|
||||||
directive, then that file itself is a library and thus a compilation
|
|
||||||
unit.) For more information about compilation units, see
|
|
||||||
the chapter on "Libraries and Scripts" in the
|
|
||||||
[Dart Language Specification](https://www.dartlang.org/docs/spec/).
|
|
||||||
|
|
||||||
block append snake-case-defn
|
|
||||||
:marked
|
|
||||||
Library and file names are often spelled in snake_case. Examples include:
|
|
||||||
`angular_tour_of_heroes` and `app_component.dart`.
|
|
||||||
|
|
||||||
block zone-defn
|
|
||||||
:marked
|
|
||||||
Zones are a mechanism for encapsulating and intercepting
|
|
||||||
a Dart application's asynchronous activity.
|
|
||||||
|
|
||||||
Learn more about zones in this [article][zones].
|
|
||||||
|
|
||||||
[zones]: https://www.dartlang.org/articles/libraries/zones
|
|
|
@ -1,197 +0,0 @@
|
||||||
{
|
|
||||||
"index": {
|
|
||||||
"title": "Documentation Overview",
|
|
||||||
"navTitle": "Overview",
|
|
||||||
"intro": "How to read and use this documentation",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"setup": {
|
|
||||||
"title": "Setup for Development",
|
|
||||||
"navTitle": "Setup",
|
|
||||||
"intro": "How to use Dart tools to create and run Angular apps",
|
|
||||||
"nextable": true,
|
|
||||||
"hideNextPage": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"learning-angular": {
|
|
||||||
"title": "Learning Angular",
|
|
||||||
"navTitle": "Learning Angular",
|
|
||||||
"intro": "A suggested path through the documentation for Angular newcomers",
|
|
||||||
"nextable": true,
|
|
||||||
"hideNextPage": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"architecture": {
|
|
||||||
"title": "Architecture Overview",
|
|
||||||
"navTitle": "Architecture",
|
|
||||||
"intro": "The basic building blocks of Angular applications",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"displaying-data": {
|
|
||||||
"title": "Displaying Data",
|
|
||||||
"intro": "Property binding helps show app data in the UI.",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"user-input": {
|
|
||||||
"title": "User Input",
|
|
||||||
"intro": "User input triggers DOM events. We listen to those events with event bindings that funnel updated values back into our components and models.",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"forms": {
|
|
||||||
"title": "Forms",
|
|
||||||
"intro": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors.",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"dependency-injection": {
|
|
||||||
"title": "Dependency Injection",
|
|
||||||
"intro": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\".",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"template-syntax": {
|
|
||||||
"title": "Template Syntax",
|
|
||||||
"intro": "Learn how to write templates that display data and consume user events with the help of data binding.",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"cheatsheet": {
|
|
||||||
"title": "Cheat Sheet",
|
|
||||||
"subtitle": "Dart",
|
|
||||||
"nextable": true,
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"style-guide": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Style Guide",
|
|
||||||
"intro": "Write Angular with style.",
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"glossary": {
|
|
||||||
"title": "Glossary",
|
|
||||||
"intro": "Brief definitions of the most important words in the Angular vocabulary",
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"change-log": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Change Log",
|
|
||||||
"intro": "An annotated history of recent documentation improvements.",
|
|
||||||
"basics": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"ngmodule": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Angular Modules (NgModule)",
|
|
||||||
"intro": "Define application modules with @NgModule"
|
|
||||||
},
|
|
||||||
|
|
||||||
"animations": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Animations",
|
|
||||||
"intro": "A guide to Angular's animation system."
|
|
||||||
},
|
|
||||||
|
|
||||||
"attribute-directives": {
|
|
||||||
"title": "Attribute Directives",
|
|
||||||
"intro": "Attribute directives attach behavior to elements."
|
|
||||||
},
|
|
||||||
|
|
||||||
"browser-support": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Browser support",
|
|
||||||
"intro": "Browser support and polyfills guide."
|
|
||||||
},
|
|
||||||
|
|
||||||
"component-styles": {
|
|
||||||
"title": "Component Styles",
|
|
||||||
"intro": "Learn how to apply CSS styles to components."
|
|
||||||
},
|
|
||||||
|
|
||||||
"hierarchical-dependency-injection": {
|
|
||||||
"title": "Hierarchical Dependency Injectors",
|
|
||||||
"navTitle": "Hierarchical Injectors",
|
|
||||||
"intro": "Angular's hierarchical dependency injection system supports nested injectors in parallel with the component tree."
|
|
||||||
},
|
|
||||||
|
|
||||||
"server-communication": {
|
|
||||||
"title": "HTTP Client",
|
|
||||||
"intro": "Use an HTTP Client to talk to a remote server."
|
|
||||||
},
|
|
||||||
|
|
||||||
"lifecycle-hooks": {
|
|
||||||
"title": "Lifecycle Hooks",
|
|
||||||
"intro": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
|
|
||||||
},
|
|
||||||
|
|
||||||
"npm-packages": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Npm Packages",
|
|
||||||
"intro": "Recommended npm packages, and how to specify package dependencies"
|
|
||||||
},
|
|
||||||
|
|
||||||
"pipes": {
|
|
||||||
"title": "Pipes",
|
|
||||||
"intro": "Pipes transform displayed values within a template."
|
|
||||||
},
|
|
||||||
|
|
||||||
"router": {
|
|
||||||
"title": "Routing & Navigation",
|
|
||||||
"intro": "Discover the basics of screen navigation with the Angular Router."
|
|
||||||
},
|
|
||||||
|
|
||||||
"security": {
|
|
||||||
"title": "Security",
|
|
||||||
"intro": "Developing for content security in Angular applications"
|
|
||||||
},
|
|
||||||
|
|
||||||
"setup-systemjs-anatomy": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Setup Anatomy",
|
|
||||||
"intro": "Inside the local development environment for SystemJS"
|
|
||||||
},
|
|
||||||
|
|
||||||
"structural-directives": {
|
|
||||||
"title": "Structural Directives",
|
|
||||||
"intro": "Angular has a powerful template engine that lets us easily manipulate the DOM structure of our elements."
|
|
||||||
},
|
|
||||||
|
|
||||||
"testing": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Testing",
|
|
||||||
"intro": "Techniques and practices for testing an Angular app"
|
|
||||||
},
|
|
||||||
|
|
||||||
"typescript-configuration": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "TypeScript Configuration",
|
|
||||||
"intro": "TypeScript configuration for Angular developers"
|
|
||||||
},
|
|
||||||
|
|
||||||
"upgrade": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Upgrading from 1.x",
|
|
||||||
"intro": "Incrementally upgrade an Angular 1 application to Angular 2."
|
|
||||||
},
|
|
||||||
|
|
||||||
"webpack": {
|
|
||||||
"hide": true,
|
|
||||||
"title": "Webpack: an introduction",
|
|
||||||
"intro": "Create Angular applications with a Webpack based tooling"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1,72 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/architecture.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _library_module = 'library'
|
|
||||||
- var _at_angular = 'angular2'
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Angular is a framework to help us build client applications in HTML and
|
|
||||||
either JavaScript or a language (like Dart or TypeScript) that compiles to JavaScript.
|
|
||||||
|
|
||||||
block angular-parts
|
|
||||||
:marked
|
|
||||||
Angular for Dart is published as the `angular2` package, which
|
|
||||||
(like many other Dart packages) is available via the Pub tool.
|
|
||||||
|
|
||||||
block modules-in-dart
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Modules are compilation units or packages
|
|
||||||
:marked
|
|
||||||
In this guide, the term _module_ refers to a Dart compilation unit, such
|
|
||||||
as a library, or a package. (If a Dart file has no `library` or `part`
|
|
||||||
directive, then that file itself is a library and thus a compilation
|
|
||||||
unit.) For more information about compilation units, see
|
|
||||||
the chapter on "Libraries and Scripts" in the
|
|
||||||
[Dart Language Specification](https://www.dartlang.org/docs/spec/).
|
|
||||||
|
|
||||||
block modules-are-optional
|
|
||||||
//- N/A
|
|
||||||
|
|
||||||
block export-qualifier
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Public names are exported by default
|
|
||||||
:marked
|
|
||||||
Contrary to TypeScript, a Dart library always exports all names and
|
|
||||||
declarations in its **public** namespace, making explicit `export`
|
|
||||||
qualifiers unnecessary.
|
|
||||||
|
|
||||||
When we say that a module _exports_ a declaration, we mean that the
|
|
||||||
declaration is _public_. For more details about name spaces and export
|
|
||||||
statements, see the section on "Exports" in the
|
|
||||||
[Dart Language Specification](https://www.dartlang.org/docs/spec/).
|
|
||||||
|
|
||||||
block ts-import
|
|
||||||
//- N/A
|
|
||||||
|
|
||||||
block angular-library-modules
|
|
||||||
:marked
|
|
||||||
Angular ships as a collection of libraries within the
|
|
||||||
[**angular2**](https://pub.dartlang.org/packages/angular2) package.
|
|
||||||
|
|
||||||
block angular-imports
|
|
||||||
+makeExcerpt('app/app.component.ts', 'import')
|
|
||||||
|
|
||||||
block ts-decorator
|
|
||||||
:marked
|
|
||||||
Annotations often have configuration parameters.
|
|
||||||
The `@Component` annotation takes parameters to provide the
|
|
||||||
information Angular needs to create and present the component and its view.
|
|
||||||
|
|
||||||
Here are a few of the possible `@Component` parameters:
|
|
||||||
|
|
||||||
block dart-bool
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Only true is true
|
|
||||||
:marked
|
|
||||||
In Dart, **the only value that is true is the boolean value `true`**; all
|
|
||||||
other values are false. JavaScript and TypeScript, in contrast, treat values
|
|
||||||
such as 1 and most non-null objects as true. For this reason, the JavaScript
|
|
||||||
and TypeScript versions of this app can use just `selectedHero` as the value
|
|
||||||
of the `*ngIf` expression. The Dart version must use a boolean operator such
|
|
||||||
as `!=` instead.
|
|
|
@ -1,9 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/attribute-directives.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
block highlight-directive-1
|
|
||||||
:marked
|
|
||||||
We begin by importing the Angular `core`.
|
|
||||||
Then we define the directive metadata by means of the `@Directive` annotation.
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
extends ../cheatsheet
|
|
|
@ -1,34 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/component-styles.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
//- TODO: consider adding material equivalent to TS Appendices 1 & 2 if relevant.
|
|
||||||
|
|
||||||
block style-url
|
|
||||||
:marked
|
|
||||||
Note that the URLs in `styleUrls` are relative to the component.
|
|
||||||
|
|
||||||
block module-bundlers
|
|
||||||
//- TODO: determine if an equivalent of the TS material is relevant for Dart.
|
|
||||||
//- Leaving empty for now.
|
|
||||||
|
|
||||||
block css-import-url
|
|
||||||
:marked
|
|
||||||
In *this* case the URL is relative to the CSS file into which we are importing.
|
|
||||||
.alert.is-important
|
|
||||||
:marked
|
|
||||||
URLs are currently not interpreted in this way, see
|
|
||||||
[issue 8518](https://github.com/angular/angular/issues/8518).
|
|
||||||
Until this issue is fixed, absolute package-reference style URLs must
|
|
||||||
be given as is illustrated below.
|
|
||||||
|
|
||||||
block module-id
|
|
||||||
:marked
|
|
||||||
Thankfully, this is the default interpretation of relative URLs in
|
|
||||||
Angular2 for Dart:
|
|
||||||
|
|
||||||
+makeExcerpt('app/quest-summary.component.ts', 'urls', '')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/dependency-injection.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _thisDot = '';
|
|
||||||
|
|
||||||
block ctor-syntax
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We also leveraged Dart's constructor syntax for declaring parameters and
|
|
||||||
initializing properties simultaneously.
|
|
||||||
|
|
||||||
block register-provider-ngmodule
|
|
||||||
:marked
|
|
||||||
Before we do, let's see an example of provider registration during bootstrapping:
|
|
||||||
|
|
||||||
+makeExcerpt('app/main.1.ts (discouraged)', 'bootstrap-discouraged', '')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The injector now knows about our `HeroService`.
|
|
||||||
An instance of our `HeroService` will be available for injection across our entire application.
|
|
||||||
|
|
||||||
Of course we can't help wondering about that comment telling us not to do it this way.
|
|
||||||
It *will* work. It's just not a best practice.
|
|
||||||
The bootstrap provider option is intended for configuring and overriding Angular's own
|
|
||||||
preregistered services, such as its routing support.
|
|
||||||
|
|
||||||
The preferred approach is to register application providers in application components.
|
|
||||||
Because the `HeroService` is used within the *Heroes* feature area —
|
|
||||||
and nowhere else — the ideal place to register it is in the top-level `HeroesComponent`.
|
|
||||||
|
|
||||||
block ngmodule-vs-component
|
|
||||||
:marked
|
|
||||||
Look at the `providers` part of the `@Component` annotation.
|
|
||||||
An instance of the `HeroService` is now available for injection in this `HeroesComponent`
|
|
||||||
and all of its child components.
|
|
||||||
|
|
||||||
The `HeroesComponent` itself doesn't happen to need the `HeroService`.
|
|
||||||
But its child `HeroListComponent` does, so we head there next.
|
|
||||||
|
|
||||||
block injectable-not-always-needed-in-ts
|
|
||||||
//- The [Angular Dart Transformer](https://github.com/dart-lang/angular2/wiki/Transformer)
|
|
||||||
//- generates static code to replace the use of dart:mirrors. It requires that types be
|
|
||||||
//- identified as targets for static code generation. Generally this is achieved
|
|
||||||
//- by marking the class as @Injectable (though there are other mechanisms).
|
|
||||||
|
|
||||||
block always-include-paren
|
|
||||||
:marked
|
|
||||||
Always write `@Injectable()`, not just `@Injectable`.
|
|
||||||
A metadata annotation must be either a reference to a
|
|
||||||
compile-time constant variable or a call to a constant
|
|
||||||
constructor such as `Injectable()`.
|
|
||||||
|
|
||||||
If we forget the parentheses, the analyzer will complain:
|
|
||||||
"Annotation creation must have arguments". If we try to run the
|
|
||||||
app anyway, it won't work, and the console will say
|
|
||||||
"expression must be a compile-time constant".
|
|
||||||
|
|
||||||
block real-logger
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
A real implementation would probably use the
|
|
||||||
[logging package](https://pub.dartlang.org/packages/logging).
|
|
||||||
|
|
||||||
block provider-shorthand
|
|
||||||
:marked
|
|
||||||
This is actually a shorthand expression for a provider registration
|
|
||||||
that creates a new instance of the
|
|
||||||
[Provider](../api/angular2.core/Provider-class.html) class:
|
|
||||||
|
|
||||||
block provider-ctor-args
|
|
||||||
- var _secondParam = 'named parameter, such as <code>useClass</code>'
|
|
||||||
:marked
|
|
||||||
We supply two arguments (or more) to the `Provider` constructor.
|
|
||||||
|
|
||||||
block dart-diff-const-metadata
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Constants in metadata
|
|
||||||
:marked
|
|
||||||
In Dart, the value of a metadata annotation must be a compile-time constant.
|
|
||||||
For that reason, we can't call functions to get values
|
|
||||||
to use within an annotation.
|
|
||||||
Instead, we use constant literals or constant constructors.
|
|
||||||
For example, a TypeScript program will use the
|
|
||||||
object literal `{ provide: Logger, useClass: BetterLogger }`.
|
|
||||||
A Dart annotation would instead use the constant value
|
|
||||||
`const Provider(Logger, useClass: BetterLogger)`.
|
|
||||||
|
|
||||||
block dart-diff-const-metadata-ctor
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Constants in metadata
|
|
||||||
:marked
|
|
||||||
Because Dart annotations must be compile-time constants,
|
|
||||||
`useValue` is often used with string or list literals.
|
|
||||||
However, `useValue` works with any constant object.
|
|
||||||
|
|
||||||
To create a class that can provide constant objects,
|
|
||||||
ensure all its instance variables are `final`,
|
|
||||||
and give it a `const` constructor.
|
|
||||||
|
|
||||||
Create a constant instance of the class by using `const` instead of `new`.
|
|
||||||
|
|
||||||
// - var stylePattern = { otl: /(useValue.*\))/gm };
|
|
||||||
// +makeExample('dependency-injection/dart/lib/providers_component.dart','providers-9','', stylePattern)(format='.')
|
|
||||||
|
|
||||||
block non-class-dep-eg
|
|
||||||
span string, list, map, or maybe a function.
|
|
||||||
|
|
||||||
block config-obj-maps
|
|
||||||
| . They can be
|
|
||||||
| <b><a href="https://api.dartlang.org/stable/dart-core/Map-class.html">Map</a></b>
|
|
||||||
| literals
|
|
||||||
|
|
||||||
block what-should-we-use-as-token
|
|
||||||
:marked
|
|
||||||
But what should we use as the token?
|
|
||||||
While we _could_ use **[Map][]**, we _should not_ because (like
|
|
||||||
`String`) `Map` is too general. Our app might depend on several maps, each
|
|
||||||
for a different purpose.
|
|
||||||
|
|
||||||
[Map]: https://api.dartlang.org/stable/dart-core/Map-class.html
|
|
||||||
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Interfaces are valid tokens
|
|
||||||
:marked
|
|
||||||
In TypeScript, interfaces don't work as provider tokens.
|
|
||||||
Dart doesn't have this limitation;
|
|
||||||
every class implicitly defines an interface,
|
|
||||||
so interface names are just class names.
|
|
||||||
`Map` is a *valid* token even though it's the name of an abstract class;
|
|
||||||
it's just *unsuitable* as a token because it's too general.
|
|
||||||
|
|
||||||
block dart-map-alternative
|
|
||||||
:marked
|
|
||||||
As an alternative to using a configuration `Map`, we can define
|
|
||||||
a custom configuration class:
|
|
||||||
|
|
||||||
+makeExcerpt('lib/app_config.dart (alternative config)','config-alt')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Defining a configuration class has a few benefits. One key benefit
|
|
||||||
is strong static checking: we'll be warned early if we misspell a property
|
|
||||||
name or assign it a value of the wrong type.
|
|
||||||
The Dart [cascade notation][cascade] (`..`) provides a convenient means of initializing
|
|
||||||
a configuration object.
|
|
||||||
|
|
||||||
If we use cascades, the configuration object can't be declared `const` and
|
|
||||||
we can't use a [value provider](#value-provider).
|
|
||||||
A solution is to use a [factory provider](#factory-provider).
|
|
||||||
We illustrate this next. We also show how to provide and inject the
|
|
||||||
configuration object in our top-level `AppComponent`:
|
|
||||||
|
|
||||||
[cascade]: https://www.dartlang.org/docs/dart-up-and-running/ch02.html#cascade
|
|
||||||
|
|
||||||
+makeExcerpt('lib/app_component.dart','providers')
|
|
||||||
+makeExcerpt('lib/app_component.dart','ctor')
|
|
|
@ -1,20 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/displaying-data.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _iterableUrl = 'https://api.dartlang.org/stable/dart-core/Iterable-class.html';
|
|
||||||
- var _boolean = 'boolean';
|
|
||||||
|
|
||||||
block hero-class
|
|
||||||
:marked
|
|
||||||
We've defined a class with a constructor, two properties (`id` and `name`),
|
|
||||||
and a `toString()` method.
|
|
||||||
|
|
||||||
block final-code
|
|
||||||
+makeTabs(`displaying-data/dart/lib/app_component.dart,
|
|
||||||
displaying-data/dart/lib/hero.dart,
|
|
||||||
displaying-data/dart/pubspec.yaml,
|
|
||||||
displaying-data/dart/web/index.html,
|
|
||||||
displaying-data/dart/web/main.dart`,
|
|
||||||
',,,,final',
|
|
||||||
'lib/app_component.dart, lib/hero.dart, pubspec.yaml, web/index.html, web/main.dart')
|
|
|
@ -1,656 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We’ve all used a form to log in, submit a help request, place an order, book a flight,
|
|
||||||
schedule a meeting, and perform countless other data entry tasks.
|
|
||||||
Forms are the mainstay of business applications.
|
|
||||||
|
|
||||||
Any seasoned web developer can slap together an HTML form with all the right tags.
|
|
||||||
It's more challenging to create a cohesive data entry experience that guides the
|
|
||||||
user efficiently and effectively through the workflow behind the form.
|
|
||||||
|
|
||||||
*That* takes design skills that are, to be frank, well out of scope for this chapter.
|
|
||||||
|
|
||||||
It also takes framework support for
|
|
||||||
**two-way data binding, change tracking, validation, and error handling**
|
|
||||||
... which we shall cover in this chapter on Angular forms.
|
|
||||||
|
|
||||||
We will build a simple form from scratch, one step at a time. Along the way we'll learn:
|
|
||||||
|
|
||||||
- How to build an Angular form with a component and template
|
|
||||||
|
|
||||||
- The `ngModel` two-way data binding syntax for reading and writing values to input controls
|
|
||||||
|
|
||||||
- The `ngControl` directive to track the change state and validity of form controls
|
|
||||||
|
|
||||||
- The special CSS classes that `ngControl` adds to form controls and how to use them to provide strong visual feedback
|
|
||||||
|
|
||||||
- How to display validation errors to users and enable/disable form controls
|
|
||||||
|
|
||||||
- How to share information across controls with template reference variables
|
|
||||||
|
|
||||||
Run the <live-example></live-example>.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Template-driven forms
|
|
||||||
|
|
||||||
Many of us will build forms by writing templates in the Angular
|
|
||||||
template syntax
|
|
||||||
<!-- link to ./template-syntax.html -->
|
|
||||||
with the form-specific directives and techniques described in this chapter.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
That's not the only way to create a form but it's the way we'll cover in this chapter.
|
|
||||||
:marked
|
|
||||||
We can build almost any form we need with an Angular template—login forms, contact forms, pretty much any business form.
|
|
||||||
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
|
||||||
conditionally enable or disable specific controls, trigger built-in visual feedback, and much more.
|
|
||||||
|
|
||||||
It will be pretty easy because Angular handles many of the repetitive, boilerplate tasks we'd
|
|
||||||
otherwise wrestle with ourselves.
|
|
||||||
|
|
||||||
We'll discuss and learn to build a template-driven form that looks like this:
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
|
||||||
heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis!
|
|
||||||
|
|
||||||
Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot.
|
|
||||||
|
|
||||||
If we delete the hero name, the form displays a validation error in an attention-grabbing style:
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Note that the submit button is disabled, and the "required" bar to the left of the input control changed from green to red.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We'll customize the colors and location of the "required" bar with standard CSS.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We'll build this form in small steps:
|
|
||||||
|
|
||||||
1. Create the `Hero` model class.
|
|
||||||
1. Create the component that controls the form.
|
|
||||||
1. Create a template with the initial form layout.
|
|
||||||
1. Add the **ngModel** directive to each form input control.
|
|
||||||
1. Add the **ngControl** directive to each form input control.
|
|
||||||
1. Add custom CSS to provide visual feedback.
|
|
||||||
1. Show and hide validation error messages.
|
|
||||||
1. Handle form submission with **ngSubmit**.
|
|
||||||
1. Change the form's display after submission.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
## Setup
|
|
||||||
Create a new project folder (`angular_forms`) and create 3 files:
|
|
||||||
`pubspec.yaml`, `web/index.html`, and `web/main.dart`.
|
|
||||||
(These files should be familiar from the
|
|
||||||
[QuickStart](../quickstart.html).) Put these contents in the files:
|
|
||||||
|
|
||||||
+makeTabs('forms/dart/pubspec.yaml, forms/dart/web/index.html, forms/dart/web/main.dart', ',initial,', 'pubspec.yaml, web/index.html, web/main.dart')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
So that the code can run,
|
|
||||||
let's create a stub for the `<hero-form>` component.
|
|
||||||
|
|
||||||
Create a new directory called `lib`.
|
|
||||||
In it, put a file called `hero_form_component.dart`
|
|
||||||
with the following code:
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_initial.dart', null, 'lib/hero_form_component.dart')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The app should now run, but it won't do anything interesting.
|
|
||||||
Let's add some data.
|
|
||||||
|
|
||||||
|
|
||||||
## Create the Hero model class
|
|
||||||
|
|
||||||
As users enter form data, we'll capture their changes and update an instance of a model.
|
|
||||||
We can't lay out the form until we know what the model looks like.
|
|
||||||
|
|
||||||
A model can be as simple as a "property bag" that holds facts about a thing of application importance.
|
|
||||||
That describes well our `Hero` class with its three required fields (`id`, `name`, `power`)
|
|
||||||
and one optional field (`alterEgo`).
|
|
||||||
|
|
||||||
In the `lib` directory, add a file called `hero.dart`
|
|
||||||
with the following code:
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero.dart', 'all', 'lib/hero.dart')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
|
||||||
|
|
||||||
The `alterEgo` is optional, so the constructor lets us omit it: note the
|
|
||||||
`[]` in `[this.alterEgo]`.
|
|
||||||
|
|
||||||
We can create a new hero like this:
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero.dart', 'newhero')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Create a form component
|
|
||||||
|
|
||||||
An Angular form has two parts: an HTML-based template and a code-based component to handle data and user interactions.
|
|
||||||
|
|
||||||
We begin with the component because it states, in brief, what the Hero editor can do.
|
|
||||||
|
|
||||||
Edit `hero_form_component.dart`, replacing all of its contents
|
|
||||||
with the following code:
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component.dart', null, 'lib/hero_form_component.dart')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
There’s nothing special about this component, nothing form-specific,
|
|
||||||
nothing to distinguish it from any component we've written before.
|
|
||||||
|
|
||||||
Understanding this component requires only the Angular concepts covered in previous chapters.
|
|
||||||
|
|
||||||
1. The code imports a standard set of symbols from the Angular library.
|
|
||||||
|
|
||||||
1. The `@Component` selector value of "hero-form" means we can drop this form in a parent template with a `<hero-form>` tag.
|
|
||||||
|
|
||||||
1. The `moduleId` property sets the base for module-relative URLs such as the `templateUrl`.
|
|
||||||
|
|
||||||
1. The `templateUrl` property points to a separate file for template HTML called `hero_form_component.html`.
|
|
||||||
|
|
||||||
1. We defined dummy data for `model` and `powers`, as befits a demo.
|
|
||||||
Down the road, we can inject a data service to get and save real data
|
|
||||||
or perhaps expose these properties as inputs and outputs
|
|
||||||
<!--TODO: link to (./template-syntax.html#inputs-outputs) --> for binding to a
|
|
||||||
parent component. None of this concerns us now, and these future changes won't affect our form.
|
|
||||||
|
|
||||||
1. We threw in a `diagnostic` property to return a
|
|
||||||
string describing our model.
|
|
||||||
It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later.
|
|
||||||
|
|
||||||
Why isn't the template inline in the component file?
|
|
||||||
|
|
||||||
Inline templates can be nice when they are short,
|
|
||||||
but most form templates aren't short. Dart files generally aren't the best place to
|
|
||||||
write (or read) large stretches of HTML, and few editors are much help with files that have a mix of HTML and code.
|
|
||||||
It's also nice to have short files with a clear and obvious purpose.
|
|
||||||
|
|
||||||
We made a good choice to put the HTML template elsewhere. Let's write it.
|
|
||||||
|
|
||||||
<!-- NOTE: I deleted the Dart equivalent of "Revise the app.ts"
|
|
||||||
because we started with example-specific index.html & main.dart
|
|
||||||
files. -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Create an initial HTML form template
|
|
||||||
|
|
||||||
Create a new file under `lib` called `hero_form_component.html`,
|
|
||||||
and put the following template code in it:
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_initial.html', null, 'lib/hero_form_component.html')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and
|
|
||||||
opening them up for user input in input boxes.
|
|
||||||
|
|
||||||
The Name `<input>` control has the HTML5 `required` attribute;
|
|
||||||
the Alter Ego `<input>` control does not because `alterEgo` is optional.
|
|
||||||
|
|
||||||
We've got a Submit button at the bottom with some classes on it.
|
|
||||||
|
|
||||||
**We are not using Angular yet**. There are no bindings. No extra directives. Just layout.
|
|
||||||
|
|
||||||
|
|
||||||
The `container`,`form-group`, `form-control`, and `btn` classes are [Bootstrap](http://getbootstrap.com/) CSS. Purely cosmetic.
|
|
||||||
We're using Bootstrap to gussy up our form.
|
|
||||||
Hey, what's a form without a little style!
|
|
||||||
|
|
||||||
.callout.is-important
|
|
||||||
header Angular forms do not require a style library
|
|
||||||
:marked
|
|
||||||
Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or
|
|
||||||
the styles of any external library. Angular apps can use any CSS library
|
|
||||||
... or none at all.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Let's add the stylesheet.
|
|
||||||
|
|
||||||
1. Download the Bootstrap stylesheet from
|
|
||||||
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css,
|
|
||||||
and put it in the `web` directory.
|
|
||||||
|
|
||||||
2. Edit `web/index.html`, adding a link to `bootstrap.min.css`:
|
|
||||||
|
|
||||||
- var stylePattern = { otl: /(<link rel.*$)/gm };
|
|
||||||
+makeExample('forms/dart/web/index.html', 'bootstrap-and-script', 'web/index.html (excerpt)', stylePattern)(format=".")
|
|
||||||
|
|
||||||
[PENDING: runnable now? Remind about pub get? remind them to look in the browser console]
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Add powers with ***ngFor**
|
|
||||||
Our hero must choose one super power from a fixed list of Agency-approved powers.
|
|
||||||
We maintain that list internally (in `HeroFormComponent`).
|
|
||||||
|
|
||||||
We'll add a `select` to our
|
|
||||||
form and bind the options to the `powers` list using `ngFor`,
|
|
||||||
a technique used before in [Displaying Data](./displaying-data.html).
|
|
||||||
|
|
||||||
Add the following HTML *immediately below* the Alter Ego group.
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_ngmodel_ngfor.html', 'powers', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
This code repeats the `<option>` tag for each power in the list of powers.
|
|
||||||
The `p` template input variable is a different power in each iteration;
|
|
||||||
we display its name using the interpolation syntax with the double curly braces.
|
|
||||||
|
|
||||||
.l-main-section#ngModel
|
|
||||||
:marked
|
|
||||||
## Two-way data binding with ***ngModel**
|
|
||||||
Running the app right now would be disappointing.
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding")
|
|
||||||
:marked
|
|
||||||
We don't see hero data because we are not binding to the `Hero` yet.
|
|
||||||
We know how to do that from earlier chapters.
|
|
||||||
[Displaying Data](./displaying-data.html) taught us property binding.
|
|
||||||
[User Input](./user-input.html) showed us how to listen for DOM events with an
|
|
||||||
event binding and how to update a component property with the displayed value.
|
|
||||||
|
|
||||||
Now we need to display, listen, and extract at the same time.
|
|
||||||
|
|
||||||
We could use the techniques we already know, but
|
|
||||||
instead we'll introduce something new: the `NgModel` directive, which
|
|
||||||
makes binding the form to the model super easy.
|
|
||||||
|
|
||||||
Find the `<input>` tag for Name and update it like this:
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_ngmodel_ngfor.html', 'ngModel-1', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We added a diagnostic interpolation after the input tag
|
|
||||||
so we can see what we're doing.
|
|
||||||
We left ourselves a note to throw it way when we're done.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Focus on the binding syntax: `[(ngModel)]="..."`.
|
|
||||||
|
|
||||||
If we ran the app right now and started typing in the Name input box,
|
|
||||||
adding and deleting characters, we'd see them appearing and disappearing
|
|
||||||
from the interpolated text.
|
|
||||||
At some point it might look like this.
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action")
|
|
||||||
:marked
|
|
||||||
The diagnostic is evidence that values really are flowing from the input box to the model and
|
|
||||||
back again. **That's two-way data binding!**
|
|
||||||
|
|
||||||
Let's add similar `[(ngModel)]` bindings to Alter Ego and Hero Power.
|
|
||||||
We'll ditch the input box binding message
|
|
||||||
and add a new binding at the top to the component's `diagnostic` property.
|
|
||||||
Then we can confirm that two-way data binding works *for the entire Hero model*.
|
|
||||||
|
|
||||||
After revision, the core of the form should have three `[(ngModel)]` bindings that
|
|
||||||
look much like this:
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_ngmodel2.html', 'ngModel-2', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
If we ran the app right now and changed every Hero model property, the form might look like this:
|
|
||||||
figure.image-display
|
|
||||||
img(src="images/ng-model-in-action-2.png" width="500px" alt="ngModel in super action")
|
|
||||||
:marked
|
|
||||||
The diagnostic near the top of the form
|
|
||||||
confirms that our changes to the values are reflected in the model.
|
|
||||||
|
|
||||||
**Delete the diagnostic binding.** It has served its purpose.
|
|
||||||
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
h3 Inside [(ngModel)]
|
|
||||||
:marked
|
|
||||||
*This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!*
|
|
||||||
|
|
||||||
The punctuation in the binding syntax, <span style="font-family:courier"><b>[()]</b></span>, is a good clue to what's going on.
|
|
||||||
|
|
||||||
A property binding makes data flow from the model to a target property on screen.
|
|
||||||
We identify that target property by surrounding its name in brackets, <span style="font-family:courier"><b>[]</b></span>.
|
|
||||||
This is a one-way data binding **from the model to the view**.
|
|
||||||
|
|
||||||
An event binding makes data flow from the target property onscreen to the model.
|
|
||||||
We identify that target property by surrounding its name in parentheses, <span style="font-family:courier"><b>()</b></span>.
|
|
||||||
This is a one-way data binding in the opposite direction **from the view to the model**.
|
|
||||||
|
|
||||||
No wonder Angular uses the combined punctuation, <span style="font-family:courier"><b>[()]</b></span>,
|
|
||||||
to signify a two-way data binding and a **flow of data in both directions**.
|
|
||||||
|
|
||||||
In fact, we can break the `NgModel` binding into its two separate modes
|
|
||||||
as in this rewrite of the Name `<input>` binding:
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_ngmodelchange.html', 'ngModel-3', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
<br>The property binding should feel familiar. The event binding might seem strange.
|
|
||||||
|
|
||||||
The name `ngModelChange` specifies an event property of the `NgModel` directive.
|
|
||||||
When Angular sees a binding target in the form <span style="font-family:courier">[(x)]</span>,
|
|
||||||
it expects the `x` directive to have an `x` input property and an `xChange` output property.
|
|
||||||
|
|
||||||
The other oddity is the template expression, `model.name = $event`.
|
|
||||||
We're used to seeing an `$event` object coming from a DOM event.
|
|
||||||
The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter`
|
|
||||||
property that returns the input box value when it fires—which is precisely what
|
|
||||||
we should assign to the model's `name` property.
|
|
||||||
|
|
||||||
Nice to know but is it practical? `[(ngModel)]` is usually what we want, but
|
|
||||||
we might split the binding when the event handling has to do something special
|
|
||||||
such as debounce or throttle the keystrokes.
|
|
||||||
|
|
||||||
<!-- TODO: Add the following once template-syntax.html exists.
|
|
||||||
Learn more about `NgModel` and other template syntax in the
|
|
||||||
[Template Syntax](./template-syntax.html) chapter.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Track change-state and validity with **ngControl**
|
|
||||||
|
|
||||||
A form isn't just about data binding. We'd also like to know the state of the controls on our form.
|
|
||||||
The `NgControl` directive keeps track of control state for us.
|
|
||||||
|
|
||||||
.callout.is-helpful
|
|
||||||
header NgControl requires Form
|
|
||||||
:marked
|
|
||||||
The `NgControl` is one of a family of `NgForm` directives that can only be applied to
|
|
||||||
a control within a `<form`> tag.
|
|
||||||
:marked
|
|
||||||
Our application can ask an `NgControl` instance whether
|
|
||||||
the user touched the control, the value changed, or the value is valid.
|
|
||||||
|
|
||||||
`NgControl` doesn't just track state; it updates the control with special
|
|
||||||
Angular CSS classes, such as `ng-valid` or `ng-invalid`.
|
|
||||||
We can use those class names to change the appearance of the
|
|
||||||
control and make messages appear or disappear.
|
|
||||||
|
|
||||||
We'll explore those effects soon. Right now
|
|
||||||
let's **add `ngControl`to all three form controls**,
|
|
||||||
starting with the Name input box.
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_ngcontrol.html', 'ngControl-1', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
:marked
|
|
||||||
Be sure to assign a unique name to each `ngControl` directive.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Angular registers controls under their `ngControl` names
|
|
||||||
with the `NgForm` directive.
|
|
||||||
We didn't add the `NgForm` directive explicitly but it's here;
|
|
||||||
we'll talk about it [later in this chapter](#ng-form).
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
h2 Add custom CSS for visual feedback
|
|
||||||
|
|
||||||
p.
|
|
||||||
<code>NgControl</code> doesn't just track state. It updates the control to
|
|
||||||
have three classes that describe the control's state.
|
|
||||||
|
|
||||||
table
|
|
||||||
tr
|
|
||||||
th State
|
|
||||||
th Class if true
|
|
||||||
th Class if false
|
|
||||||
tr
|
|
||||||
td Control has been visited
|
|
||||||
td <code>ng-touched</code>
|
|
||||||
td <code>ng-untouched</code>
|
|
||||||
tr
|
|
||||||
td Control's value has changed
|
|
||||||
td <code>ng-dirty</code>
|
|
||||||
td <code>ng-pristine</code>
|
|
||||||
tr
|
|
||||||
td Control's value is valid
|
|
||||||
td <code>ng-valid</code>
|
|
||||||
td <code>ng-invalid</code>
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy**
|
|
||||||
to the "Name" `<input>` tag and use the spy to display those classes.
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component_spy.html', 'ngControl-2', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Now run the app, and look at the Name input box.
|
|
||||||
Follow the next four steps *precisely*:
|
|
||||||
|
|
||||||
1. Look but don't touch.
|
|
||||||
1. Click inside the name box, then click outside it.
|
|
||||||
1. Add slashes to the end of the name.
|
|
||||||
1. Erase the name.
|
|
||||||
|
|
||||||
The classes are displayed as follows:
|
|
||||||
|
|
||||||
1. `form-control ng-untouched ng-valid ng-pristine` (initial state)
|
|
||||||
1. `form-control ng-valid ng-pristine ng-touched` (after clicking)
|
|
||||||
1. `form-control ng-valid ng-touched ng-dirty` (after changing)
|
|
||||||
1. `form-control ng-touched ng-dirty ng-invalid` (after erasing)
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The (`ng-valid` | `ng-invalid`) pair are the most interesting to us, because we want to send a
|
|
||||||
strong visual signal when the values are bad. We also want to mark required fields.
|
|
||||||
|
|
||||||
We can do both at the same time with a colored bar on the left of the input box:
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We achieve this effect by adding two styles to a new `forms.css` file
|
|
||||||
(under `web/`).
|
|
||||||
|
|
||||||
+makeExample('forms/dart/web/forms.css', null, 'web/forms.css')
|
|
||||||
:marked
|
|
||||||
These styles select for the two Angular validity classes and the HTML 5 "required" attribute.
|
|
||||||
|
|
||||||
To add the styles to the app,
|
|
||||||
update the `<head>` of `index.html` to link to `forms.css`.
|
|
||||||
- var stylePattern = { otl: /(.*forms.css.*$)/gm };
|
|
||||||
+makeExample('forms/dart/web/index.html', 'styles', 'web/index.html (excerpt)', stylePattern)(format=".")
|
|
||||||
|
|
||||||
|
|
||||||
:marked
|
|
||||||
## Show and hide validation error messages
|
|
||||||
|
|
||||||
We can do better.
|
|
||||||
|
|
||||||
The Name input box is required. Clearing it turns the bar red. That says *something* is wrong but we
|
|
||||||
don't know *what* is wrong or what to do about it.
|
|
||||||
We can leverage the `ng-invalid` class to reveal a helpful message.
|
|
||||||
|
|
||||||
Here's the way it should look when the user deletes the name:
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
To achieve this effect we extend the `<input>` tag with
|
|
||||||
1. a [template reference variable](./template-syntax.html#ref-vars)
|
|
||||||
1. the "*is required*" message in a nearby `<div>` which we'll display only if the control is invalid.
|
|
||||||
|
|
||||||
Here's an example of adding an error message to the "name" input box:
|
|
||||||
- var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm };
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component.html', 'name-with-error-msg', 'lib/hero_form_component.html (excerpt)', stylePattern)(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We need a template reference variable to access the input box's Angular control from within the template.
|
|
||||||
Here we created a variable called `name` and gave it the value "ngForm".
|
|
||||||
|
|
||||||
Angular recognizes that syntax and sets the `name` variable
|
|
||||||
to the `Control` object identified by the `ngControl` directive that,
|
|
||||||
not coincidentally, we called "name".
|
|
||||||
|
|
||||||
We bind the `Control` object's `valid` property to the element's `hidden` property.
|
|
||||||
While the control is valid, the message is hidden;
|
|
||||||
if it becomes invalid, the message is revealed.
|
|
||||||
<a id="ng-form"></a>
|
|
||||||
.l-sub-section
|
|
||||||
h3 The NgForm directive
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Recall from the previous section that `ngControl` registered this input box with the
|
|
||||||
`NgForm` directive as "name".
|
|
||||||
|
|
||||||
We didn't add the [NgForm](../api/angular2.common/NgForm-class.html) directive
|
|
||||||
explicitly. Angular added it surreptitiously, wrapping it around the `<form>` element.
|
|
||||||
|
|
||||||
The `NgForm` directive supplements the `<form>` element with additional features.
|
|
||||||
It collects controls (elements identified by an `ngControl` directive)
|
|
||||||
and monitors their properties including their validity.
|
|
||||||
It has its own `valid` property, which is true only if every contained
|
|
||||||
control is valid.
|
|
||||||
|
|
||||||
In this example, we are pulling the "name" control out of its `controls` collection
|
|
||||||
and assigning it to a template reference variable so that we can
|
|
||||||
access the control's properties—such as the control's own `valid` property.
|
|
||||||
:marked
|
|
||||||
The Alter Ego is optional so we can leave that be.
|
|
||||||
|
|
||||||
Hero Power selection is required.
|
|
||||||
We can add the same kind of error handling to the `<select>` if we want,
|
|
||||||
but it's not imperative because the selection box already constrains the
|
|
||||||
power to valid values.
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Submit the form with **ngSubmit**
|
|
||||||
The user should be able to submit this form after filling it in.
|
|
||||||
The Submit button at the bottom of the form
|
|
||||||
does nothing on its own, but it will
|
|
||||||
trigger a form submit because of its type (`type="submit"`).
|
|
||||||
|
|
||||||
A "form submit" is meaningless at the moment. To make it meaningful,
|
|
||||||
we'll update the `<form>` tag with another Angular directive, `NgSubmit`,
|
|
||||||
and bind it to the `HeroFormComponent.onSubmit()` method:
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component.html', 'ngSubmit', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We slipped in something extra there at the end! We defined a
|
|
||||||
template reference variable, **`#heroForm`**, and initialized it with the value "ngForm".
|
|
||||||
|
|
||||||
The variable `heroForm` is now a handle to the `NgForm` as we [discussed earlier](#ng-form)
|
|
||||||
with respect to `ngControl`, although this time we have a reference to the form
|
|
||||||
rather than a control.
|
|
||||||
|
|
||||||
We'll bind the form's overall validity via
|
|
||||||
the `heroForm` variable to the button's `disabled` property
|
|
||||||
using an event binding. Here's the code:
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component.html', 'submit-button', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
:marked
|
|
||||||
If we run the application now, we find that the button is enabled.
|
|
||||||
It doesn't do anything useful yet but it's alive.
|
|
||||||
|
|
||||||
Now if we delete the Name, we violate the "required" rule, which
|
|
||||||
is duly noted in the error message.
|
|
||||||
The Submit button is also disabled.
|
|
||||||
|
|
||||||
Not impressed? Think about it for a moment. What would we have to do to
|
|
||||||
wire the button's enable/disabled state to the form's validity without Angular's help?
|
|
||||||
|
|
||||||
For us, it was as simple as:
|
|
||||||
1. Define a template reference variable on the (enhanced) form element.
|
|
||||||
2. Refer to that variable in a button many lines away.
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Toggle two form regions (extra credit)
|
|
||||||
Submitting the form isn't terribly dramatic at the moment.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
An unsurprising observation for a demo. To be honest,
|
|
||||||
jazzing it up won't teach us anything new about forms.
|
|
||||||
But this is an opportunity to exercise some of our newly won
|
|
||||||
binding skills.
|
|
||||||
If you aren't interested, go ahead and skip to this chapter's conclusion.
|
|
||||||
:marked
|
|
||||||
Let's do something more strikingly visual.
|
|
||||||
Let's hide the data entry area and display something else.
|
|
||||||
|
|
||||||
Start by wrapping the form in a `<div>` and binding
|
|
||||||
its `hidden` property to the `HeroFormComponent.submitted` property.
|
|
||||||
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component.html', 'edit-div', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The main form is visible from the start because the
|
|
||||||
`submitted` property is false until we submit the form,
|
|
||||||
as the following code from `hero_form_component.dart` shows:
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component.dart', 'submitted')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
When we click the Submit button, the `submitted` flag becomes true and the form disappears
|
|
||||||
as planned.
|
|
||||||
|
|
||||||
Now the app needs to show something else while the form is in the submitted state.
|
|
||||||
Add the following block of HTML below the `<div>` wrapper we just wrote:
|
|
||||||
+makeExample('forms/dart/lib/hero_form_component.html', 'submitted', 'lib/hero_form_component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
There's our hero again, displayed read-only with interpolation bindings.
|
|
||||||
This slug of HTML appears only while the component is in the submitted state.
|
|
||||||
|
|
||||||
The HTML includes an Edit button whose click event is bound to an expression
|
|
||||||
that clears the `submitted` flag.
|
|
||||||
|
|
||||||
When we click the Edit button, this block disappears and the editable form reappears.
|
|
||||||
|
|
||||||
That's as much drama as we can muster for now.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The Angular form discussed in this chapter takes advantage of the following framework features to provide support for data modification, validation, and more:
|
|
||||||
|
|
||||||
- An Angular HTML form template.
|
|
||||||
- A form component class with a `Component` decorator.
|
|
||||||
- The `ngSubmit` directive for handling the form submission.
|
|
||||||
- Template reference variables such as `#heroForm`, `#name`, `#p`, and `#spy`.
|
|
||||||
- The `ngModel` directive for two-way data binding.
|
|
||||||
- The `ngControl` directive for validation and form element change tracking.
|
|
||||||
- The reference variable’s `valid` property on input controls to check if a control is valid and show/hide error messages.
|
|
||||||
- Property binding to disable the submit button when the form is invalid.
|
|
||||||
- Custom CSS classes that provide visual feedback to users about required invalid controls.
|
|
||||||
|
|
||||||
Here’s the code for the final version of the application:
|
|
||||||
|
|
||||||
+makeTabs(
|
|
||||||
`forms/dart/lib/hero_form_component.dart,
|
|
||||||
forms/dart/lib/hero_form_component.html,
|
|
||||||
forms/dart/lib/hero.dart,
|
|
||||||
forms/dart/pubspec.yaml,
|
|
||||||
forms/dart/web/index.html,
|
|
||||||
forms/dart/web/main.dart,
|
|
||||||
forms/dart/web/forms.css`,
|
|
||||||
'no-todo,,all,,,,',
|
|
||||||
`lib/hero_form_component.dart,
|
|
||||||
lib/hero_form_component.html,
|
|
||||||
lib/hero.dart,
|
|
||||||
pubspec.yaml,
|
|
||||||
web/index.html,
|
|
||||||
web/main.dart,
|
|
||||||
web/forms.css`)
|
|
|
@ -1,4 +0,0 @@
|
||||||
extends ../glossary
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
|
@ -1,4 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/hierarchical-dependency-injection.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -1,5 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/index.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _angular_io = 'website';
|
|
|
@ -1,4 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/learning-angular.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
|
@ -1,18 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/lifecycle-hooks.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
block other-angular-subsystems
|
|
||||||
:marked
|
|
||||||
The router, for instance, also has its own [router lifecycle
|
|
||||||
hooks](router.html#router-lifecycle-hooks) that allow us to tap into
|
|
||||||
specific moments in route navigation.
|
|
||||||
A parallel can be drawn between `ngOnInit` and `routerOnActivate`. Both are
|
|
||||||
prefixed so as to avoid collision, and both run right when a component is
|
|
||||||
being initialized.
|
|
||||||
|
|
||||||
block tick-methods
|
|
||||||
:marked
|
|
||||||
The `LoggerService.tick()` postpones the log update
|
|
||||||
for one turn of the browser's update cycle ... and that's just long enough.
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1,8 +0,0 @@
|
||||||
:marked
|
|
||||||
## NPM and NPM packages
|
|
||||||
|
|
||||||
TypeScript and JavaScript developers rely on the Node Package Manager (*npm*) to install
|
|
||||||
angular and other libraries.
|
|
||||||
|
|
||||||
Dart applications do not use *npm* to load 3rd party modules so
|
|
||||||
the chapter describing that process is irrelevant to Dart developers.
|
|
|
@ -1,11 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/pipes.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
block pure-change
|
|
||||||
:marked
|
|
||||||
Angular executes a *pure pipe* only when it detects a *pure change* to the input value.
|
|
||||||
In Angular Dart, a *pure change* results only from a change in object reference
|
|
||||||
(given that [everything is an object in Dart](https://www.dartlang.org/docs/dart-up-and-running/ch02.html#important-concepts)).
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
This advanced guide hasn't yet been written.
|
|
||||||
To learn the basics of routing, read the Tour of Heroes tutorial:
|
|
||||||
[Routing Around the App](../tutorial/toh-pt5.html).
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1,82 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/server-communication.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _Http = 'BrowserClient';
|
|
||||||
- var _Angular_Http = 'Dart <code>BrowserClient</code>'
|
|
||||||
- var _httpUrl = 'https://pub.dartlang.org/packages/http'
|
|
||||||
- var _Angular_http_library = 'Dart <a href="' + _httpUrl + '"><b>http</b></a> library'
|
|
||||||
|
|
||||||
block demos-list
|
|
||||||
:marked
|
|
||||||
- [HTTP client: Tour of Heroes](#http-client)
|
|
||||||
- [JSONP client: Wikipedia to fetch data from a service that does not support CORS (**under development**)](#cors)
|
|
||||||
|
|
||||||
block http-providers
|
|
||||||
:marked
|
|
||||||
Actually, it is unnecessary to include `BrowserClient` in the list of providers.
|
|
||||||
***But*** as is mentioned in the *Angular Dart Transformer* [wiki page][ng2dtri],
|
|
||||||
the template compiler _generates_ dependency injection code, hence all the
|
|
||||||
identifiers used in DI have to be collected by the Angular transformer
|
|
||||||
so that the libraries containing these identifiers can be transformed.
|
|
||||||
|
|
||||||
Unless special steps are taken, Dart libraries like `http`
|
|
||||||
are not transformed. To ensure that the `BrowserClient` identifier is available
|
|
||||||
for DI, we must add a `resolved_identifiers` parameter to the `angular2`
|
|
||||||
transformer in `pubspec.yaml`:
|
|
||||||
|
|
||||||
[ng2dtri]: https://github.com/dart-lang/angular2/wiki/Transformer#resolved_identifiers
|
|
||||||
|
|
||||||
- var stylePattern = { pnk: /(resolved_identifiers:|Browser.*)/gm, otl: /(- angular2:)|(transformers:)/g };
|
|
||||||
+makeExcerpt('pubspec.yaml', 'transformers', null, stylePattern)
|
|
||||||
|
|
||||||
block getheroes-and-addhero
|
|
||||||
:marked
|
|
||||||
The hero service `getHeroes()` and `addHero()` asynchronous methods return the
|
|
||||||
[`Future`](https://api.dartlang.org/stable/dart-async/Future-class.html)
|
|
||||||
values of the current hero list and the newly added hero,
|
|
||||||
respectively. The hero list component methods of the same name specifying
|
|
||||||
the actions to be taken when the asynchronous method calls succeed or fail.
|
|
||||||
|
|
||||||
For more information about `Future`s, consult any one
|
|
||||||
of the [articles](https://www.dartlang.org/articles/) on asynchronous
|
|
||||||
programming in Dart, or the tutorial on
|
|
||||||
[_Asynchronous Programming: Futures_](https://www.dartlang.org/docs/tutorials/futures/).
|
|
||||||
|
|
||||||
block parse-json
|
|
||||||
:marked
|
|
||||||
The response data are in JSON string form.
|
|
||||||
We must parse that string into Objects which we do by calling
|
|
||||||
the `JSON.decode()` method from the `dart:convert` library.
|
|
||||||
|
|
||||||
block error-handling
|
|
||||||
//- TODO: describe `_handleError`?
|
|
||||||
|
|
||||||
block hlc-error-handling
|
|
||||||
:marked
|
|
||||||
Back in the `HeroListComponent`, we wrapped our call to
|
|
||||||
`!{_priv}heroService.getHeroes()` in a `try` clause. When an exception is
|
|
||||||
caught, the `errorMessage` variable — which we've bound conditionally in the
|
|
||||||
template — gets assigned to.
|
|
||||||
|
|
||||||
block hero-list-comp-add-hero
|
|
||||||
:marked
|
|
||||||
Back in the `HeroListComponent`, we see that *its* `addHero()`
|
|
||||||
awaits for the *service's* asynchronous `addHero()` to return, and when it does,
|
|
||||||
the new hero is added to the `heroes` list for presentation to the user.
|
|
||||||
|
|
||||||
block wikipedia-jsonp+
|
|
||||||
:marked
|
|
||||||
Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API.
|
|
||||||
.alert.is-important
|
|
||||||
:marked
|
|
||||||
The remaining content of this section is coming soon.
|
|
||||||
In the meantime, consult the
|
|
||||||
[example sources](https://github.com/angular-examples/server-communication)
|
|
||||||
to see how to access Wikipedia via its `JSONP` API.
|
|
||||||
|
|
||||||
block redirect-to-web-api
|
|
||||||
:marked
|
|
||||||
To achieve this, we have Angular inject an in-memory web API server
|
|
||||||
instance as a provider for the `BrowserClient`. This is possible because
|
|
||||||
the in-memory web API server class extends `BrowserClient`.
|
|
|
@ -1,161 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
a#develop-locally
|
|
||||||
:marked
|
|
||||||
Setting up a new Angular project is quick and easy,
|
|
||||||
using everyday Dart tools.
|
|
||||||
|
|
||||||
.l-main-section#sdk
|
|
||||||
:marked
|
|
||||||
## Prerequisites: Dart SDK & Dartium
|
|
||||||
|
|
||||||
If you don't already have the **Dart SDK** and **Dartium**, get them.
|
|
||||||
We recommend using the WebStorm IDE, as well.
|
|
||||||
|
|
||||||
[Get Started](https://webdev.dartlang.org/guides/get-started) tells you how to get the tools.
|
|
||||||
|
|
||||||
|
|
||||||
a#webstorm
|
|
||||||
:marked
|
|
||||||
## Creating a project in WebStorm
|
|
||||||
|
|
||||||
Using WebStorm to create an app is simple, once you complete some one-time setup.
|
|
||||||
|
|
||||||
1. Launch WebStorm.
|
|
||||||
1. If you haven't already done so,
|
|
||||||
[configure Dart support in WebStorm](https://webdev.dartlang.org/tools/webstorm#configuring-dart-support).
|
|
||||||
1. Choose **Create New Project** from the welcome screen,
|
|
||||||
or **File > New > Project...** from the menu. A dialog appears.
|
|
||||||
1. Select **Dart** from the list on the left.
|
|
||||||
1. Set the location and template:
|
|
||||||
1. In the **Location** input field, check that
|
|
||||||
the project folder is where you want it.
|
|
||||||
1. Also in the **Location** field,
|
|
||||||
change the name of the project from `untitled` to whatever you choose, such as `angular_quickstart`.
|
|
||||||
1. Make sure that **Generate sample content** is checked.
|
|
||||||
1. Select **Angular QuickStart Example** from the list.
|
|
||||||
<br>
|
|
||||||
The form should look similar to the following:
|
|
||||||
<br>
|
|
||||||
<img src="images/create-ng2-project.png" alt="A screenshot of the New Project dialog, with the specified selections" style="border:1px solid">
|
|
||||||
1. Click **Create**.
|
|
||||||
|
|
||||||
WebStorm takes several seconds to analyze the sources and do other housekeeping.
|
|
||||||
This only happens once. After that, you'll be able to use WebStorm for the usual IDE tasks,
|
|
||||||
including running the app.
|
|
||||||
|
|
||||||
For more information on using WebStorm, see
|
|
||||||
[Installing and Using WebStorm](https://webdev.dartlang.org/tools/webstorm).
|
|
||||||
|
|
||||||
|
|
||||||
a#cli
|
|
||||||
:marked
|
|
||||||
## Using a template from the command line
|
|
||||||
|
|
||||||
[Stagehand](http://stagehand.pub/) gives you
|
|
||||||
command-line access to the same templates that WebStorm uses.
|
|
||||||
Assuming the Dart SDK and
|
|
||||||
[pub cache `bin` directory](https://www.dartlang.org/tools/pub/cmd/pub-global#running-a-script-from-your-path)
|
|
||||||
are in your path, here's how you use Stagehand to create an Angular project.
|
|
||||||
|
|
||||||
1. Install or update stagehand:
|
|
||||||
<br>
|
|
||||||
`pub global activate stagehand`
|
|
||||||
1. Create a directory for your project:
|
|
||||||
<br>
|
|
||||||
`mkdir angular_quickstart; cd angular_quickstart`
|
|
||||||
1. Use Stagehand with the appropriate template to create a skeleton app:
|
|
||||||
<br>
|
|
||||||
`stagehand web-angular-quickstart`
|
|
||||||
<br>
|
|
||||||
**Note:**
|
|
||||||
Examples in this guide and tutorial are based on the QuickStart example,
|
|
||||||
which is in the `web-angular-quickstart` template.
|
|
||||||
A more general-purpose template is `web-angular`.
|
|
||||||
1. Get the app's dependencies: <br>
|
|
||||||
`pub get`
|
|
||||||
1. Run the app: <br>
|
|
||||||
`pub serve`
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section#seed
|
|
||||||
:marked
|
|
||||||
## What's in the QuickStart example?
|
|
||||||
|
|
||||||
The <live-example name="quickstart">QuickStart example</live-example>
|
|
||||||
contains the following core files:
|
|
||||||
|
|
||||||
+makeTabs(`
|
|
||||||
quickstart/ts/app/app.component.ts,
|
|
||||||
quickstart/ts/app/main.ts,
|
|
||||||
quickstart/ts/index.html,
|
|
||||||
quickstart/ts/styles.css,
|
|
||||||
quickstart/dart/pubspec.yaml`,
|
|
||||||
',,,quickstart,',
|
|
||||||
`app/app.component.ts,
|
|
||||||
app/main.ts,
|
|
||||||
index.html,
|
|
||||||
styles.css (excerpt),
|
|
||||||
pubspec.yaml`)
|
|
||||||
|
|
||||||
:marked
|
|
||||||
These files are organized as follows:
|
|
||||||
|
|
||||||
.filetree
|
|
||||||
.file angular_quickstart
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file main.dart
|
|
||||||
.file index.html
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
|
|
||||||
:marked
|
|
||||||
All guides and cookbooks have _at least these core files_. Each file has a distinct purpose and evolves independently as the application grows.
|
|
||||||
|
|
||||||
style td, th {vertical-align: top}
|
|
||||||
table(width="100%")
|
|
||||||
col(width="20%")
|
|
||||||
col(width="80%")
|
|
||||||
tr
|
|
||||||
th File
|
|
||||||
th Purpose
|
|
||||||
tr
|
|
||||||
td <code>lib/app_component.dart</code>
|
|
||||||
td
|
|
||||||
:marked
|
|
||||||
Defines `<my-app>`, the **root** component of what will become a tree of nested components
|
|
||||||
as the application evolves.
|
|
||||||
tr
|
|
||||||
td <code>web/main.dart</code>
|
|
||||||
td
|
|
||||||
:marked
|
|
||||||
Bootstraps the application to run in the browser.
|
|
||||||
tr
|
|
||||||
td <code>web/index.html</code>
|
|
||||||
td
|
|
||||||
:marked
|
|
||||||
Contains the `<my-app>` tag in its `<body>`.
|
|
||||||
This is where the app lives!
|
|
||||||
tr
|
|
||||||
td <code>web/styles.css</code>
|
|
||||||
td
|
|
||||||
:marked
|
|
||||||
A set of styles used throughout the app.
|
|
||||||
tr
|
|
||||||
td <code>pubspec.yaml</code>
|
|
||||||
td
|
|
||||||
:marked
|
|
||||||
The file that describes this Dart package (the app) and its dependencies.
|
|
||||||
For example, it specifies the **angular2** and **browser** packages as dependencies,
|
|
||||||
as well as the **angular2** transformer.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
### Next step
|
|
||||||
|
|
||||||
If you're new to Angular, we recommend staying on the [learning path](learning-angular.html).
|
|
|
@ -1,9 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/structural-directives.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
block unless-intro
|
|
||||||
:marked
|
|
||||||
Creating a directive is similar to creating a component.
|
|
||||||
Here is how we begin:
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1,118 +0,0 @@
|
||||||
extends ../../../ts/_cache/guide/template-syntax.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _JavaScript = 'Dart';
|
|
||||||
- var __chaining_op = '<code>;</code>';
|
|
||||||
- var __new_op = '<code>new</code> or <code>const</code>';
|
|
||||||
- var mapApiRef = 'https://api.dartlang.org/stable/dart-core/Map-class.html';
|
|
||||||
- var __objectAsMap = '<b><a href="' + mapApiRef + '">Map</a></b>'
|
|
||||||
|
|
||||||
block notable-differences
|
|
||||||
:marked
|
|
||||||
* no support for Dart string interpolation; for example,
|
|
||||||
instead of `"'The title is $title'"`, you must use
|
|
||||||
`"'The title is ' + title"`
|
|
||||||
* no support for the bitwise operators `|` and `&`
|
|
||||||
* new [template expression operators](#expression-operators), such as `|`
|
|
||||||
|
|
||||||
block template-expressions-cannot
|
|
||||||
:marked
|
|
||||||
Perhaps more surprising, template expressions can’t refer to static
|
|
||||||
properties, nor to top-level variables or functions, such as `window` or
|
|
||||||
`document` from `dart:html`. They can’t directly call `print` or functions
|
|
||||||
imported from `dart:math`. They are restricted to referencing members of
|
|
||||||
the expression context.
|
|
||||||
|
|
||||||
block statement-context
|
|
||||||
:marked
|
|
||||||
Template statements can’t refer to static properties on the class, nor to
|
|
||||||
top-level variables or functions, such as `window` or `document` from
|
|
||||||
`dart:html`. They can’t directly call `print` or functions imported from
|
|
||||||
`dart:math`.
|
|
||||||
|
|
||||||
block dart-type-exceptions
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Type exceptions
|
|
||||||
:marked
|
|
||||||
In checked mode, if the template expression result type and the target
|
|
||||||
property type are not assignment compatible, then a type exception will
|
|
||||||
be thrown.
|
|
||||||
For information on checked mode, see [Important concepts](https://www.dartlang.org/docs/dart-up-and-running/ch02.html#important-concepts)
|
|
||||||
in the Dart language tour.
|
|
||||||
|
|
||||||
block dart-type-exception-example
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Type exception example
|
|
||||||
:marked
|
|
||||||
In checked mode, the code above will result in a type exception:
|
|
||||||
`String` isn't a subtype of `Hero`.
|
|
||||||
|
|
||||||
block style-property-name-dart-diff
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: Style property names
|
|
||||||
:marked
|
|
||||||
While [camelCase](glossary.html#camelcase) and
|
|
||||||
[dash-case](glossary.html#dash-case) style property naming schemes are
|
|
||||||
equivalent in Angular Dart, only dash-case names are recognized by the
|
|
||||||
`dart:html` [CssStyleDeclaration][CssSD] methods `getPropertyValue()`
|
|
||||||
and `setProperty()`. Hence, we recommend only using dash-case for style
|
|
||||||
property names.
|
|
||||||
|
|
||||||
[CssSD]: https://api.dartlang.org/stable/dart-html/CssStyleDeclaration-class.html
|
|
||||||
|
|
||||||
|
|
||||||
block dart-no-truthy-falsey
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: No truthy/falsey values
|
|
||||||
:marked
|
|
||||||
In checked mode, Dart expects Boolean values
|
|
||||||
(those with type `bool`) to be either `true` or `false`.
|
|
||||||
Even in production mode, the only value Dart treats as `true` is
|
|
||||||
the value `true`; all other values are `false`.
|
|
||||||
TypeScript and JavaScript, on the other hand, treat
|
|
||||||
many values (including non-null objects) as true.
|
|
||||||
A TypeScript Angular program, for example, often has code like
|
|
||||||
`*ngIf="currentHero"` where a Dart program has code like
|
|
||||||
`*ngIf="currentHero != null"`.
|
|
||||||
|
|
||||||
When converting TypeScript code to Dart code, watch out for
|
|
||||||
true/false problems. For example, forgetting the `!= null`
|
|
||||||
can lead to exceptions in checked mode, such as
|
|
||||||
"EXCEPTION: type 'Hero' is not a subtype of type 'bool' of 'boolean expression'".
|
|
||||||
For more information, see
|
|
||||||
[Booleans](https://www.dartlang.org/docs/dart-up-and-running/ch02.html#booleans)
|
|
||||||
in the [Dart language tour](https://www.dartlang.org/docs/dart-up-and-running/ch02.html).
|
|
||||||
|
|
||||||
block remember-the-brackets
|
|
||||||
//- Changed from RED to ORANGE, since this isn't so dire a situation in Dart.
|
|
||||||
.callout.is-important
|
|
||||||
header Remember the brackets!
|
|
||||||
:marked
|
|
||||||
Don’t make the mistake of writing `ngIf="currentHero"`!
|
|
||||||
That syntax assigns the *string* value `"currentHero"` to `ngIf`,
|
|
||||||
which won't work because `ngIf` expects a `bool`.
|
|
||||||
|
|
||||||
block dart-safe-nav-op
|
|
||||||
.callout.is-helpful
|
|
||||||
header Dart difference: ?. is a Dart operator
|
|
||||||
:marked
|
|
||||||
The safe navigation operator (`?.`) is part of the Dart language.
|
|
||||||
It's considered a template expression operator because
|
|
||||||
Angular supports `?.` even in TypeScript and JavaScript apps.
|
|
||||||
|
|
||||||
block json-pipe
|
|
||||||
//- TODO: explain alternative in Dart
|
|
||||||
//- {{ e | json }} --> {{ e }}
|
|
||||||
//- which causes the object's toString() method to be invoked.
|
|
||||||
//- Of course the `json` pipe can be used if the instance supports
|
|
||||||
//- JSON encoding.
|
|
||||||
|
|
||||||
block null-deref-example
|
|
||||||
:marked
|
|
||||||
Dart throws an exception, and so does Angular:
|
|
||||||
code-example(format="nocode").
|
|
||||||
EXCEPTION: The null object does not have a getter 'firstName'.
|
|
||||||
|
|
||||||
block safe-op-alt
|
|
||||||
//- N/A
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1,254 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
When the user clicks a link, pushes a button, or enters text
|
|
||||||
we want to know about it. These user actions all raise DOM events.
|
|
||||||
In this chapter we learn to bind to those events using the Angular
|
|
||||||
event binding syntax.
|
|
||||||
|
|
||||||
Run the <live-example></live-example>.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
## Binding to user input events
|
|
||||||
|
|
||||||
We can use [Angular event bindings](./template-syntax.html#event-binding)
|
|
||||||
to respond to [any DOM event](https://developer.mozilla.org/en-US/docs/Web/Events).
|
|
||||||
|
|
||||||
The syntax is simple. We surround the DOM event name with
|
|
||||||
parentheses and assign a quoted template statement to it.
|
|
||||||
As an example, here's an event binding that implements a click handler:
|
|
||||||
+makeExample('user-input/dart/lib/click_me_component.dart', 'click-me-button')(format=".", language="html")
|
|
||||||
|
|
||||||
<a id="click"></a>
|
|
||||||
:marked
|
|
||||||
The `(click)` to the left of the equal sign identifies the button's click event as the **target of the binding**.
|
|
||||||
The text within quotes on the right is the **template statement** in which we
|
|
||||||
respond to the click event by calling the component's `onClickMe` method. A [template statement](./template-syntax.html#template-statements) is a subset
|
|
||||||
of Dart with restrictions and a few added tricks.
|
|
||||||
|
|
||||||
When writing a binding we must be aware of a template statement's **execution context**.
|
|
||||||
The identifiers appearing within a statement belong to a specific context object.
|
|
||||||
That object is usually the Angular component that controls the template ... which it definitely is
|
|
||||||
in this case because that snippet of HTML belongs to the following component:
|
|
||||||
|
|
||||||
+makeExample('user-input/dart/lib/click_me_component.dart', 'click-me-component', 'web/click_me_component.dart')(format=".")
|
|
||||||
:marked
|
|
||||||
When the user clicks the button, Angular calls the component's `onClickMe` method.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Get user input from the $event object
|
|
||||||
We can bind to all kinds of events. Let's bind to the keyup event of an input box and replay
|
|
||||||
what the user types back onto the screen.
|
|
||||||
|
|
||||||
This time we'll (1) listen to an event and (2) grab the user's input.
|
|
||||||
+makeExample('user-input/dart/lib/keyup_components.dart', 'key-up-component-1-template', 'web/keyup_components.dart (template v.1)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Angular makes an event object available in the **`$event`** variable,
|
|
||||||
which we pass to the component's `onKey()` method.
|
|
||||||
The user data we want is in that variable somewhere.
|
|
||||||
|
|
||||||
.callout.is-important
|
|
||||||
header $event vs. \$event
|
|
||||||
:marked
|
|
||||||
Templates in Dart files need a `\` in front of the `$`.
|
|
||||||
If the template is in an HTML file, use `$event` instead of `\$event`.
|
|
||||||
|
|
||||||
+makeExample('user-input/dart/lib/keyup_components.dart', 'key-up-component-1-class-no-type', 'web/keyup_components.dart (class v.1)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The shape of the `$event` object is determined by whatever raises the event.
|
|
||||||
The `keyup` event comes from the DOM, so `$event` must be a [standard DOM event object](https://developer.mozilla.org/en-US/docs/Web/API/Event).
|
|
||||||
The `$event.target` gives us an
|
|
||||||
[`HTMLInputElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement), which
|
|
||||||
has a `value` property that contains our user input data.
|
|
||||||
|
|
||||||
The `onKey()` component method is where we extract the user's input
|
|
||||||
from the event object, adding that input to the list of user data that we're accumulating in the component's `values` property.
|
|
||||||
We then use [interpolation](./template-syntax.html#interpolation)
|
|
||||||
to display the accumulating `values` property back on screen.
|
|
||||||
|
|
||||||
Enter the letters "abc", and then backspace to remove them.
|
|
||||||
Here's what the UI displays:
|
|
||||||
code-example().
|
|
||||||
a | ab | abc | ab | a | |
|
|
||||||
figure.image-display
|
|
||||||
img(src='/resources/images/devguide/user-input/keyup1-anim.gif' alt="key up 1")
|
|
||||||
|
|
||||||
<a id="keyup1"></a>
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We cast the `$event` as an `any` type, which means we've abandoned strong typing
|
|
||||||
to simplify our code. We generally prefer the strong typing that Dart affords.
|
|
||||||
We can rewrite the method, casting to HTML DOM objects like this.
|
|
||||||
+makeExample('user-input/dart/lib/keyup_components.dart', 'key-up-component-1-class', 'web/keyup_components.dart (class v.1 - strongly typed )')(format=".")
|
|
||||||
:marked
|
|
||||||
<br>Strong typing reveals a serious problem with passing a DOM event into the method:
|
|
||||||
too much awareness of template details, too little separation of concerns.
|
|
||||||
|
|
||||||
We'll address this problem in our next try at processing user keystrokes.
|
|
||||||
:marked
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Get user input from a template reference variable
|
|
||||||
There's another way to get the user data without the `$event` variable.
|
|
||||||
|
|
||||||
Angular has a syntax feature called [**template reference variables**](./template-syntax.html#ref-vars).
|
|
||||||
These variables grant us direct access to an element.
|
|
||||||
We declare a template reference variable by preceding an identifier with a hash/pound character (#).
|
|
||||||
|
|
||||||
Here's an example of using a template reference variable
|
|
||||||
to implement a clever keystroke loopback in an ultra-simple template.
|
|
||||||
+makeExample('user-input/dart/lib/loop_back_component.dart', 'loop-back-component', 'web/loop_back_component.dart')(format=".")
|
|
||||||
:marked
|
|
||||||
We've declared a template reference variable named `box` on the `<input>` element.
|
|
||||||
The `box` variable is a reference to the `<input>` element itself, which means we can
|
|
||||||
grab the input element's `value` and display it
|
|
||||||
with interpolation between `<p>` tags.
|
|
||||||
|
|
||||||
The template is completely self contained. It doesn't bind to the component,
|
|
||||||
and the component does nothing.
|
|
||||||
|
|
||||||
Type in the input box, and watch the display update with each keystroke. *Voila!*
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src='/resources/images/devguide/user-input/keyup-loop-back-anim.gif' alt="loop back")
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
**This won't work at all unless we bind to an event**.
|
|
||||||
|
|
||||||
Angular only updates the bindings (and therefore the screen)
|
|
||||||
if we do something in response to asynchronous events such as keystrokes.
|
|
||||||
|
|
||||||
That's why we bind the `keyup` event to a statement that does ... well, nothing.
|
|
||||||
We're binding to the number 0, the shortest statement we can think of.
|
|
||||||
That is all it takes to keep Angular happy. We said it would be clever!
|
|
||||||
:marked
|
|
||||||
That template reference variable is intriguing. It's clearly easier to get to the textbox with that
|
|
||||||
variable than to go through the `$event` object. Maybe we can rewrite our previous
|
|
||||||
keyup example so that it uses the variable to get the user's input. Let's give it a try.
|
|
||||||
+makeExample('user-input/dart/lib/keyup_components.dart', 'key-up-component-2' ,'web/keyup_components.dart (v2)')(format=".")
|
|
||||||
:marked
|
|
||||||
That sure seems easier.
|
|
||||||
An especially nice aspect of this approach is that our component code gets clean data values from the view.
|
|
||||||
It no longer requires knowledge of the `$event` and its structure.
|
|
||||||
|
|
||||||
<a id="key-event"></a>
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Key event filtering (with `key.enter`)
|
|
||||||
Perhaps we don't care about every keystroke.
|
|
||||||
Maybe we're only interested in the input box value when the user presses Enter, and we'd like to ignore all other keys.
|
|
||||||
When we bind to the `(keyup)` event, our event handling statement hears *every keystroke*.
|
|
||||||
We could filter the keys first, examining every `$event.keyCode`, and update the `values` property only if the key is Enter.
|
|
||||||
|
|
||||||
Angular can filter the key events for us. Angular has a special syntax for keyboard events.
|
|
||||||
We can listen for just the Enter key by binding to Angular's `keyup.enter` pseudo-event.
|
|
||||||
|
|
||||||
Only then do we update the component's `values` property. (In this example,
|
|
||||||
the update happens inside the event binding statement. A better practice
|
|
||||||
would be to put the update code in the component.)
|
|
||||||
+makeExample('user-input/dart/lib/keyup_components.dart', 'key-up-component-3' ,'web/keyup_components.dart (v3)')(format=".")
|
|
||||||
:marked
|
|
||||||
Here's how it works.
|
|
||||||
figure.image-display
|
|
||||||
img(src='/resources/images/devguide/user-input/keyup3-anim.gif' alt="key up 3")
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## On blur
|
|
||||||
|
|
||||||
Our previous example won't transfer the current state of the input box if the user mouses away and clicks
|
|
||||||
elsewhere on the page. We update the component's `values` property only when the user presses Enter
|
|
||||||
while the focus is inside the input box.
|
|
||||||
|
|
||||||
Let's fix that by listening to the input box's blur event as well.
|
|
||||||
|
|
||||||
+makeExample('user-input/dart/lib/keyup_components.dart', 'key-up-component-4' ,'web/keyup_components.dart (v4)')(format=".")
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Put it all together
|
|
||||||
We learned how to [display data](./displaying-data.html) in the previous chapter.
|
|
||||||
We've acquired a small arsenal of event binding techniques in this chapter.
|
|
||||||
|
|
||||||
Let's put it all together in a micro-app
|
|
||||||
that can display a list of heroes and add new heroes to that list.
|
|
||||||
The user can add a hero by first typing in the input box and then
|
|
||||||
pressing Enter, clicking the Add button, or clicking elsewhere on the page.
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src='/resources/images/devguide/user-input/little-tour-anim.gif' alt="Little Tour of Heroes")
|
|
||||||
:marked
|
|
||||||
Below is the "Little Tour of Heroes" component.
|
|
||||||
We'll call out the highlights after we bask briefly in its minimalist glory.
|
|
||||||
|
|
||||||
+makeExample('user-input/dart/lib/little_tour_component.dart', 'little-tour', 'web/little_tour_component.dart')(format=".")
|
|
||||||
:marked
|
|
||||||
We've seen almost everything here before. A few things are new or bear repeating.
|
|
||||||
|
|
||||||
### Use template variables to refer to elements
|
|
||||||
|
|
||||||
The `newHero` template variable refers to the `<input>` element.
|
|
||||||
We can use `newHero` from any sibling or child of the `<input>` element.
|
|
||||||
|
|
||||||
Getting the element from a template variable makes the button click handler
|
|
||||||
simpler. Without the variable, we'd have to use a fancy CSS selector
|
|
||||||
to find the input element.
|
|
||||||
|
|
||||||
### Pass values, not elements
|
|
||||||
|
|
||||||
We could have passed the `newHero` into the component's `addHero` method.
|
|
||||||
|
|
||||||
But that would require `addHero` to pick its way through the `<input>` DOM element,
|
|
||||||
something we learned to dislike in our first try at a [keyup component](#keyup1).
|
|
||||||
|
|
||||||
Instead, we grab the input box *value* and pass *that* to `addHero`.
|
|
||||||
The component knows nothing about HTML or the DOM, which is the way we like it.
|
|
||||||
|
|
||||||
### Keep template statements simple
|
|
||||||
We bound `(blur)` to *two* Dart statements.
|
|
||||||
|
|
||||||
We like the first one, which calls `addHero`.
|
|
||||||
We do not like the second one, which assigns an empty string to the input box value.
|
|
||||||
|
|
||||||
The second statement exists for a good reason. We have to clear the input box after adding the new hero to the list.
|
|
||||||
The component has no way to do that itself because it has no access to the
|
|
||||||
input box (our design choice).
|
|
||||||
|
|
||||||
Although the example *works*, we are rightly wary of Dart in HTML.
|
|
||||||
Template statements are powerful. We're supposed to use them responsibly.
|
|
||||||
Complex Dart in HTML is irresponsible.
|
|
||||||
|
|
||||||
Should we reconsider our reluctance to pass the input box into the component?
|
|
||||||
|
|
||||||
There should be a better third way. And there is, as we'll see when we learn about `NgModel` in the [Forms](forms.html) chapter.
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Source code
|
|
||||||
|
|
||||||
Here is all the code we talked about in this chapter.
|
|
||||||
+makeTabs(`
|
|
||||||
user-input/dart/lib/click_me_component.dart,
|
|
||||||
user-input/dart/lib/keyup_components.dart,
|
|
||||||
user-input/dart/lib/loop_back_component.dart,
|
|
||||||
user-input/dart/lib/little_tour_component.dart
|
|
||||||
`,'',
|
|
||||||
`click_me_component.dart,
|
|
||||||
keyup_components.dart,
|
|
||||||
loop_back_component.dart,
|
|
||||||
little_tour_component.dart`)
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
We've mastered the basic primitives for responding to user input and gestures.
|
|
||||||
As powerful as these primitives are, they are a bit clumsy for handling
|
|
||||||
large amounts of user input. We're operating down at the low level of events when
|
|
||||||
we should be writing two-way bindings between data entry fields and model properties.
|
|
||||||
|
|
||||||
Angular has a two-way binding called `NgModel`, which we'll learn about
|
|
||||||
in the `Forms` chapter.
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../../_includes/_ts-temp
|
|
|
@ -1,43 +0,0 @@
|
||||||
.l-main-section
|
|
||||||
h2 Get Help Using Angular
|
|
||||||
|
|
||||||
p.
|
|
||||||
We have an incredible community of developers who are passionate about solving problems.
|
|
||||||
We recommend some of the following methods to get help with Angular.
|
|
||||||
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
h3 Angular Google Group
|
|
||||||
|
|
||||||
ol
|
|
||||||
li.
|
|
||||||
Search the archive first.
|
|
||||||
It's likely that your question has already been answered.
|
|
||||||
li.
|
|
||||||
To avoid the spam moderation queue,
|
|
||||||
don't include code directly in your email. (See #3)
|
|
||||||
li.
|
|
||||||
Link to a live code example that demonstrates your problem or question,
|
|
||||||
so you'll get an answer faster.
|
|
||||||
<a href="http://plnkr.co/edit/jISF8yxbVGmarpBbqsYW?p=preview">Use this template</a>.
|
|
||||||
li.
|
|
||||||
If you get help, help others. Good karma rulez!
|
|
||||||
|
|
||||||
a(href="https://groups.google.com/forum/#!forum/angular" class="button button-primary" md-button) View the Google Group
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
h3 Angular Chat Room
|
|
||||||
|
|
||||||
p Talk in real time with other Angular developers on Slack.
|
|
||||||
|
|
||||||
a(href="http://dartlang-slack.herokuapp.com/" class="button button-primary" md-button) Join Dart on Slack
|
|
||||||
|
|
||||||
a(href="https://dartlang.slack.com/messages/angular2/" class="button button-primary" md-button) Go to the Angular channel
|
|
||||||
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
h3 Report an Issue
|
|
||||||
|
|
||||||
p If you run into an issue or have a feature request, you can create a new issue on our GitHub repository.
|
|
||||||
|
|
||||||
a(href="https://github.com/angular/angular/issues" class="button button-primary" md-button) Report an Issue
|
|
|
@ -1,7 +0,0 @@
|
||||||
include ../../ts/latest/index
|
|
||||||
|
|
||||||
div.c12.l-space-top-3
|
|
||||||
.alert.is-helpful.
|
|
||||||
Not using Angular 2 yet? Perhaps you need the
|
|
||||||
<a href="https://www.dartdocs.org/documentation/angular/latest" target="_blank">API docs for the original AngularDart</a>.
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
extends ../../ts/_cache/quickstart.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include _util-fns
|
|
||||||
- var _on_Plunkr = '';
|
|
|
@ -1,40 +0,0 @@
|
||||||
// TODO: don't duplicate text from /docs/ts/latest/resources.jade
|
|
||||||
.l-main-section
|
|
||||||
h2 Victor Savkin's Blog Posts
|
|
||||||
ul
|
|
||||||
li: a(href="http://victorsavkin.com/post/137821436516/managing-state-in-angular-2-applications") Managing State in Angular 2 Applications
|
|
||||||
li <a href="http://victorsavkin.com/post/114168430846/two-phases-of-angular-2-applications">Two Phases of Angular 2 Applications</a>
|
|
||||||
li <a href="http://angularjs.blogspot.com/2015/03/forms-in-angular-2.html">Forms in Angular 2</a>
|
|
||||||
li <a href="http://victorsavkin.com/post/110170125256/change-detection-in-angular-2">Change detection</a>
|
|
||||||
li <a href="http://victorsavkin.com/post/108837493941/better-support-for-functional-programming-in">Functional programming </a>
|
|
||||||
li <a href="http://victorsavkin.com/post/102965317996/angular-2-bits-unified-dependency-injection">Dependency injection</a>
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
h2 <span class="icon-play-circle-outline"></span> Videos
|
|
||||||
|
|
||||||
h4 Intro Videos
|
|
||||||
ul
|
|
||||||
li <a href="https://www.youtube.com/watch?v=uD6Okha_Yj0">Building a Todo App</a> by David East
|
|
||||||
li <a href="https://www.youtube.com/watch?v=4C4bmDOV5hk">Angular 2 Forms</a> by David East
|
|
||||||
|
|
||||||
h4 ng-conf
|
|
||||||
ul
|
|
||||||
li Playlist <a href="https://www.youtube.com/watch?v=QHulaj5ZxbI&index=1&list=PLOETEcp3DkCoNnlhE-7fovYvqwVPrRiY7">of ng-conf 2015 videos</a>.
|
|
||||||
li <a href="https://www.youtube.com/watch?v=QHulaj5ZxbI&list=PLOETEcp3DkCoNnlhE-7fovYvqwVPrRiY7">Day 1 Keynote</a>: a broad overview of Angular 2, migration, and where we are headed.
|
|
||||||
li <a href="https://www.youtube.com/watch?v=-dMBcqwvYA0&index=21&list=PLOETEcp3DkCoNnlhE-7fovYvqwVPrRiY7">Day 2 Keynote</a>: Misko and Rado do a deep-dive on Angular 2 details.
|
|
||||||
li <a href="https://www.youtube.com/watch?v=AbunztfV5vU&index=6&list=PLOETEcp3DkCoNnlhE-7fovYvqwVPrRiY7">Creating Container Components with Web Components in Angular</a>: Kara Erickson & Rachael L Moore.
|
|
||||||
li <a href="https://www.youtube.com/watch?v=jvKGQSFQf10&index=31&list=PLOETEcp3DkCoNnlhE-7fovYvqwVPrRiY7">Change Detection Reinvented</a>: Why Angular 2 change detection is fast out of the box and options for developers to make it even faster.
|
|
||||||
|
|
||||||
h4 ng-europe
|
|
||||||
ul
|
|
||||||
li Oct 2014 <a href="https://www.youtube.com/watch?v=lGdnh8QSPPk&list=PLhc_bKwZngxW_ZlY0NkaGkvKpiA_pzcZ-">playlist of ng-europe videos on Angular 2</a> and the future of Angular.
|
|
||||||
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
h2 <span class="icon-content-copy"></span> API Design Docs & Notes
|
|
||||||
ul
|
|
||||||
li <a href="https://drive.google.com/open?id=0B7GYXx6a6d8QR3lTT1J3MEpRSlE&authuser=0">Best Practices</a>
|
|
||||||
li <a href="https://drive.google.com/open?id=0BxgtL8yFJbacUnUxc3l5aTZrbVk&authuser=0">API Design Docs</a>
|
|
||||||
li <a href="https://drive.google.com/open?id=0BxgtL8yFJbacMEZDc2NtWS1VZ1k&authuser=0">Meeting Notes</a>
|
|
||||||
li <a href="https://drive.google.com/open?id=0BxgtL8yFJbaceGc2dlhGQnMzYXc&authuser=0">Presentations</a>
|
|
||||||
li <a href="http://goo.gl/sj0Nk1">More...</a>
|
|
|
@ -1 +0,0 @@
|
||||||
include ../../_includes/styleguide/_styleguide
|
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"index": {
|
|
||||||
"title": "Tutorial: Tour of Heroes",
|
|
||||||
"navTitle": "Introduction",
|
|
||||||
"intro": "The Tour of Heroes tutorial takes us through the steps of creating an Angular application in Dart.",
|
|
||||||
"nextable": true
|
|
||||||
},
|
|
||||||
"toh-pt1": {
|
|
||||||
"title": "The Hero Editor",
|
|
||||||
"intro": "We build a simple hero editor",
|
|
||||||
"nextable": true
|
|
||||||
},
|
|
||||||
"toh-pt2": {
|
|
||||||
"title": "Master/Detail",
|
|
||||||
"intro": "We build a master/detail page with a list of heroes",
|
|
||||||
"nextable": true
|
|
||||||
},
|
|
||||||
"toh-pt3": {
|
|
||||||
"title": "Multiple Components",
|
|
||||||
"intro": "We refactor the master/detail view into separate components",
|
|
||||||
"nextable": true
|
|
||||||
},
|
|
||||||
"toh-pt4": {
|
|
||||||
"title": "Services",
|
|
||||||
"intro": "We create a reusable service to manage our hero data calls",
|
|
||||||
"nextable": true
|
|
||||||
},
|
|
||||||
"toh-pt5": {
|
|
||||||
"title": "Routing",
|
|
||||||
"intro": "We add the Angular Component Router and learn to navigate among the views",
|
|
||||||
"nextable": true
|
|
||||||
},
|
|
||||||
"toh-pt6": {
|
|
||||||
"title": "HTTP",
|
|
||||||
"intro": "We convert our service and components to use Angular's HTTP service",
|
|
||||||
"nextable": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
extends ../../../ts/_cache/tutorial/index.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
|
|
|
@ -1,185 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Every story starts somewhere. Our story starts where the [QuickStart](../quickstart.html) ends.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Follow the "QuickStart" steps. They provide the prerequisites, the folder structure,
|
|
||||||
and the core files for our Tour of Heroes.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
TODO: Recommend stagehand?
|
|
||||||
-->
|
|
||||||
|
|
||||||
Copy the "QuickStart" code to a new folder and rename the folder `angular_tour_of_heroes`.
|
|
||||||
We should have the following structure:
|
|
||||||
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
|
|
||||||
.p
|
|
||||||
|
|
||||||
.callout.is-helpful
|
|
||||||
header Source code
|
|
||||||
:marked
|
|
||||||
Run the <live-example></live-example> for this part.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
## Keep the app compiling and running
|
|
||||||
We want to start the Dart compiler, have it watch for changes, and start our server. We'll do this by typing
|
|
||||||
|
|
||||||
code-example(language="sh" class="code-shell").
|
|
||||||
pub serve
|
|
||||||
|
|
||||||
:marked
|
|
||||||
This command runs the compiler in watch mode, starts the server,
|
|
||||||
and keeps the app running while we continue to build the Tour of Heroes.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Show our Hero
|
|
||||||
We want to display Hero data in our app.
|
|
||||||
|
|
||||||
Let's add two properties to our `AppComponent`, a `title` property for the application name and a `hero` property
|
|
||||||
for a hero named "Windstorm".
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart-snippets/app_component_snippets_pt1.dart', 'app-component-1', 'app_component.dart (AppComponent class)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Now we update the template in the `@Component` annotation with data bindings to these new properties.
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart-snippets/app_component_snippets_pt1.dart', 'show-hero')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The browser should refresh and display our title and hero.
|
|
||||||
|
|
||||||
The double curly braces tell our app to read the `title` and `hero` properties from the component and render them.
|
|
||||||
This is the "interpolation" form of one-way data binding.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about interpolation in the [Displaying Data chapter](../guide/displaying-data.html).
|
|
||||||
:marked
|
|
||||||
### Hero object
|
|
||||||
|
|
||||||
At the moment, our hero is just a name. Our hero needs more properties.
|
|
||||||
Let's convert the `hero` from a literal string to a class.
|
|
||||||
|
|
||||||
Create a `Hero` class with `id` and `name` properties.
|
|
||||||
Keep this near the top of the `app_component.dart` file for now.
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart/lib/app_component.dart', 'hero-class-1', 'app_component.dart (Hero class)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Now that we have a `Hero` class, let’s refactor our component’s `hero` property to be of type `Hero`.
|
|
||||||
Then initialize it with an id of `1` and the name, "Windstorm".
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart/lib/app_component.dart', 'hero-property-1', 'app_component.dart (hero property)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Because we changed the hero from a string to an object,
|
|
||||||
we update the binding in the template to refer to the hero’s `name` property.
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart-snippets/app_component_snippets_pt1.dart', 'show-hero-2')
|
|
||||||
:marked
|
|
||||||
The browser refreshes and continues to display our hero’s name.
|
|
||||||
|
|
||||||
### Adding more HTML
|
|
||||||
Displaying a name is good, but we want to see all of our hero’s properties.
|
|
||||||
We’ll add a `<div>` for our hero’s `id` property and another `<div>` for our hero’s `name`.
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart-snippets/app_component_snippets_pt1.dart', 'show-hero-properties')
|
|
||||||
:marked
|
|
||||||
Uh oh, our template string is getting long. We better take care of that to avoid the risk of making a typo in the template.
|
|
||||||
|
|
||||||
### Multi-line template strings
|
|
||||||
|
|
||||||
We could make a more readable template with string concatenation
|
|
||||||
but that gets ugly fast, it is harder to read, and
|
|
||||||
it is easy to make a spelling error. Instead,
|
|
||||||
let’s take advantage of the template strings feature
|
|
||||||
in Dart to maintain our sanity.
|
|
||||||
|
|
||||||
Change the quotes around the template to triple quotes and
|
|
||||||
put the `<h1>`, `<h2>` and `<div>` elements on their own lines.
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart-snippets/app_component_snippets_pt1.dart', 'multi-line-strings', 'app_component.dart (AppComponent\'s template)')(format=".")
|
|
||||||
|
|
||||||
// omit back-tick warning
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Editing Our Hero
|
|
||||||
|
|
||||||
We want to be able to edit the hero name in a textbox.
|
|
||||||
|
|
||||||
Refactor the hero name `<label>` with `<label>` and `<input>` elements as shown below:
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart-snippets/app_component_snippets_pt1.dart', 'editing-Hero', 'app_component.dart (input element)')(format=".")
|
|
||||||
:marked
|
|
||||||
We see in the browser that the hero’s name does appear in the `<input>` textbox.
|
|
||||||
But something doesn’t feel right.
|
|
||||||
When we change the name, we notice that our change
|
|
||||||
is not reflected in the `<h2>`. We won't get the desired behavior
|
|
||||||
with a one-way binding to `<input>`.
|
|
||||||
|
|
||||||
### Two-Way Binding
|
|
||||||
|
|
||||||
We intend to display the name of the hero in the `<input>`, change it,
|
|
||||||
and see those changes wherever we bind to the hero’s name.
|
|
||||||
In short, we want two-way data binding.
|
|
||||||
|
|
||||||
Let’s update the template to use the **`ngModel`** built-in directive for two-way binding.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about `ngModel` in the
|
|
||||||
[Forms](../guide/forms.html#ngModel) and
|
|
||||||
[Template Syntax](../guide/template-syntax.html#ngModel) chapters.
|
|
||||||
:marked
|
|
||||||
Replace the `<input>` with the following HTML
|
|
||||||
|
|
||||||
code-example(language="html").
|
|
||||||
<input [(ngModel)]="hero.name" placeholder="name">
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The browser refreshes. We see our hero again. We can edit the hero’s name and
|
|
||||||
see the changes reflected immediately in the `<h2>`.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## The Road We’ve Travelled
|
|
||||||
Let’s take stock of what we’ve built.
|
|
||||||
|
|
||||||
* Our Tour of Heroes uses the double curly braces of interpolation (a form of one-way data binding)
|
|
||||||
to display the application title and properties of a `Hero` object.
|
|
||||||
* We wrote a multi-line template using Dart's template strings to make our template readable.
|
|
||||||
* We can both display and change the hero’s name after adding a two-way data binding to the `<input>` element
|
|
||||||
using the built-in `ngModel` directive.
|
|
||||||
* The `ngModel` directive also propagates changes to every other binding of the `hero.name`.
|
|
||||||
|
|
||||||
<!-- TODO:
|
|
||||||
add [Run the live example for part 1](https://tour-of-heroes.firebaseapp.com/toh1/)
|
|
||||||
-->
|
|
||||||
|
|
||||||
Here's the complete `app_component.dart` as it stands now:
|
|
||||||
|
|
||||||
+makeExample('toh-1/dart/lib/app_component.dart', 'pt1', 'app_component.dart')
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## The Road Ahead
|
|
||||||
Our Tour of Heroes only displays one hero and we really want to display a list of heroes.
|
|
||||||
We also want to allow the user to select a hero and display their details.
|
|
||||||
We’ll learn more about how to retrieve lists, bind them to the
|
|
||||||
template, and allow a user to select it in the
|
|
||||||
[next tutorial chapter](./toh-pt2.html).
|
|
|
@ -1,302 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Our story needs more heroes.
|
|
||||||
We’ll expand our Tour of Heroes app to display a list of heroes,
|
|
||||||
allow the user to select a hero, and display the hero’s details.
|
|
||||||
|
|
||||||
Run the <live-example></live-example> for this part.
|
|
||||||
|
|
||||||
Let’s take stock of what we’ll need to display a list of heroes.
|
|
||||||
First, we need a list of heroes. We want to display those heroes in the view’s template,
|
|
||||||
so we’ll need a way to do that.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Where We Left Off
|
|
||||||
Before we continue with Part 2 of the Tour of Heroes,
|
|
||||||
let’s verify we have the following structure after [Part 1](./toh-pt1.html).
|
|
||||||
If not, we’ll need to go back to Part 1 and figure out what we missed.
|
|
||||||
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
:marked
|
|
||||||
### Keep the app compiling and running
|
|
||||||
We want to start the Dart compiler, have it watch for changes, and start our server. We'll do this by typing
|
|
||||||
|
|
||||||
code-example(language="sh" class="code-shell").
|
|
||||||
pub serve
|
|
||||||
|
|
||||||
:marked
|
|
||||||
This will keep the application running while we continue to build the Tour of Heroes.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Displaying Our Heroes
|
|
||||||
### Creating heroes
|
|
||||||
Let’s create a list of ten heroes at the bottom of `app_component.dart`.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart/lib/app_component.dart', 'hero-array', 'app_component.dart (hero list)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The `mockHeroes` list is of type `Hero`, the class defined in part one,
|
|
||||||
to create a list of heroes.
|
|
||||||
We aspire to fetch this list of heroes from a web service, but let’s take small steps
|
|
||||||
first and display mock heroes.
|
|
||||||
|
|
||||||
### Exposing heroes
|
|
||||||
Let’s create a public property in `AppComponent` that exposes the heroes for binding.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'hero-array-1', 'app_component.dart (hero list property)')
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We could have defined the heroes list here in this component class.
|
|
||||||
But we know that ultimately we’ll get the heroes from a data service.
|
|
||||||
Because we know where we are heading, it makes sense to separate the hero data
|
|
||||||
from the class implementation from the start.
|
|
||||||
:marked
|
|
||||||
### Displaying heroes in a template
|
|
||||||
Our component has `heroes`. Let’s create an unordered list in our template to display them.
|
|
||||||
We’ll insert the following chunk of HTML below the title and above the hero details.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'heroes-template-1', 'app_component.dart (heroes template)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Now we have a template that we can fill with our heroes.
|
|
||||||
|
|
||||||
### Listing heroes with ngFor
|
|
||||||
|
|
||||||
We want to bind the list of `heroes` in our component to our template, iterate over them,
|
|
||||||
and display them individually.
|
|
||||||
We’ll need some help from Angular to do this. Let’s do this step by step.
|
|
||||||
|
|
||||||
First modify the `<li>` tag by adding the built-in directive `*ngFor`.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'heroes-ngfor-1', 'app_component.dart (ngFor)')
|
|
||||||
|
|
||||||
.alert.is-critical
|
|
||||||
:marked
|
|
||||||
The leading asterisk (`*`) in front of `ngFor` is a critical part of this syntax.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
The (`*`) prefix to `ngFor` indicates that the `<li>` element and its children
|
|
||||||
constitute a master template.
|
|
||||||
|
|
||||||
The `ngFor` directive iterates over the `heroes` list returned by the `AppComponent.heroes` property
|
|
||||||
and stamps out instances of this template.
|
|
||||||
|
|
||||||
The quoted text assigned to `ngFor` means
|
|
||||||
“*take each hero in the `heroes` list, store it in the local `hero` variable,
|
|
||||||
and make it available to the corresponding template instance*”.
|
|
||||||
|
|
||||||
The `let` keyword before "hero" identifies `hero` as a template input variable.
|
|
||||||
We can reference this variable within the template to access a hero’s properties.
|
|
||||||
|
|
||||||
Learn more about `ngFor` and template input variables in the
|
|
||||||
[Displaying Data](../guide/displaying-data.html#ngFor) and
|
|
||||||
[Template Syntax](../guide/template-syntax.html#ngFor) chapters.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Now we insert some content between the `<li>` tags
|
|
||||||
that uses the `hero` template variable to display the hero’s properties.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'ng-for', 'app_component.dart (ngFor template)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
When the browser refreshes, we see a list of heroes!
|
|
||||||
|
|
||||||
### Styling our heroes
|
|
||||||
Our list of heroes looks pretty bland.
|
|
||||||
We want to make it visually obvious to a user which hero we are hovering over and which hero is selected.
|
|
||||||
|
|
||||||
Let’s add some styles to our component by setting the `styles` argument of the `@Component` annotation
|
|
||||||
to the following CSS classes:
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart/lib/app_component.dart', 'styles', 'app_component.dart (styles)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Notice that we again use the triple-quote notation for multi-line strings.
|
|
||||||
|
|
||||||
That's a lot of styles! We can put them inline as shown here, or we can move them out to their own file which will make it easier to code our component.
|
|
||||||
We'll do this in a later chapter. For now let's keep rolling.
|
|
||||||
|
|
||||||
When we assign styles to a component they are scoped to that specific component.
|
|
||||||
Our styles will only apply to our `AppComponent` and won't "leak" to the outer HTML.
|
|
||||||
|
|
||||||
Our template for displaying the heroes should now look like this:
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'heroes-styled', 'app_component.dart (styled heroes)')
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Selecting a Hero
|
|
||||||
We have a list of heroes and we have a single hero displayed in our app.
|
|
||||||
The list and the single hero are not connected in any way.
|
|
||||||
We want the user to select a hero from our list, and have the selected hero appear in the details view.
|
|
||||||
This UI pattern is widely known as "master-detail".
|
|
||||||
In our case, the master is the heroes list and the detail is the selected hero.
|
|
||||||
|
|
||||||
Let’s connect the master to the detail through a `selectedHero` component property bound to a click event.
|
|
||||||
|
|
||||||
### Click event
|
|
||||||
We modify the `<li>` by inserting an Angular event binding to its click event.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'selectedHero-click', 'app_component.dart (template excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Focus on the event binding
|
|
||||||
code-example.
|
|
||||||
(click)="onSelect(hero)"
|
|
||||||
:marked
|
|
||||||
The parentheses identify the `<li>` element’s `click` event as the target.
|
|
||||||
The expression to the right of the equal sign calls the `AppComponent` method, `onSelect()`,
|
|
||||||
passing the template input variable `hero` as an argument.
|
|
||||||
That’s the same `hero` variable we defined previously in the `ngFor`.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about Event Binding in the
|
|
||||||
[User Input](../guide/user-input.html) and
|
|
||||||
[Templating Syntax](../guide/template-syntax.html#event-binding) chapters.
|
|
||||||
:marked
|
|
||||||
### Add the click handler
|
|
||||||
Our event binding refers to an `onSelect` method that doesn’t exist yet.
|
|
||||||
We’ll add that method to our component now.
|
|
||||||
|
|
||||||
What should that method do? It should set the component’s selected hero to the hero that the user clicked.
|
|
||||||
|
|
||||||
Our component doesn’t have a “selected hero” yet either. We’ll start there.
|
|
||||||
|
|
||||||
### Expose the selected hero
|
|
||||||
We no longer need the static `hero` property of the `AppComponent`.
|
|
||||||
**Replace** it with this simple `selectedHero` property:
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart/lib/app_component.dart', 'selected-hero', 'app_component.dart (selectedHero)')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We’ve decided that none of the heroes should be selected before the user picks a hero so
|
|
||||||
we won’t initialize the `selectedHero` as we were doing with `hero`.
|
|
||||||
|
|
||||||
Now **add an `onSelect` method** that sets the `selectedHero` property to the `hero` the user clicked.
|
|
||||||
+makeExample('toh-2/dart/lib/app_component.dart', 'on-select', 'app_component.dart (onSelect)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We will be showing the selected hero's details in our template.
|
|
||||||
At the moment, it is still referring to the old `hero` property.
|
|
||||||
Let’s fix the template to bind to the new `selectedHero` property.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'selectedHero-details', 'app_component.dart (template excerpt)')(format=".")
|
|
||||||
:marked
|
|
||||||
### Hide the empty detail with ngIf
|
|
||||||
|
|
||||||
When our app loads we see a list of heroes, but a hero is not selected.
|
|
||||||
The `selectedHero` is `undefined`.
|
|
||||||
That’s why we'll see the following error in the browser’s console:
|
|
||||||
|
|
||||||
code-example(format="nocode").
|
|
||||||
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Remember that we are displaying `selectedHero.name` in the template.
|
|
||||||
This name property does not exist because `selectedHero` itself is undefined.
|
|
||||||
|
|
||||||
We'll address this problem by keeping the hero detail out of the DOM until there is a selected hero.
|
|
||||||
|
|
||||||
We wrap the HTML hero detail content of our template with a `<div>`.
|
|
||||||
Then we add the `ngIf` built-in directive and set it to the `selectedHero` property of our component.
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'ng-if', 'app_component.dart (ngIf)')(format=".")
|
|
||||||
|
|
||||||
.alert.is-critical
|
|
||||||
:marked
|
|
||||||
Remember that the leading asterisk (`*`) in front of `ngIf` is
|
|
||||||
a critical part of this syntax.
|
|
||||||
:marked
|
|
||||||
When there is no `selectedHero`, the `ngIf` directive removes the hero detail HTML from the DOM.
|
|
||||||
There will be no hero detail elements and no bindings to worry about.
|
|
||||||
|
|
||||||
When the user picks a hero, `selectedHero` isn't `null` anymore and
|
|
||||||
`ngIf` puts the hero detail content into the DOM and evaluates the nested bindings.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
`ngIf` and `ngFor` are called “structural directives” because they can change the
|
|
||||||
structure of portions of the DOM.
|
|
||||||
In other words, they give structure to the way Angular displays content in the DOM.
|
|
||||||
|
|
||||||
Learn more about `ngIf`, `ngFor` and other structural directives in the
|
|
||||||
[Structural Directives](../guide/structural-directives.html) and
|
|
||||||
[Template Syntax](../guide/template-syntax.html#directives) chapters.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The browser refreshes and we see the list of heroes but not the selected hero detail.
|
|
||||||
The `ngIf` keeps it out of the DOM as long as the `selectedHero` is undefined.
|
|
||||||
When we click on a hero in the list, the selected hero displays in the hero details.
|
|
||||||
Everything is working as we expect.
|
|
||||||
|
|
||||||
### Styling the selection
|
|
||||||
|
|
||||||
We see the selected hero in the details area below but we can’t quickly locate that hero in the list above.
|
|
||||||
We can fix that by applying the `selected` CSS class to the appropriate `<li>` in the master list.
|
|
||||||
For example, when we select Magneta from the heroes list,
|
|
||||||
we can make it pop out visually by giving it a subtle background color as shown here.
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src='/resources/images/devguide/toh/heroes-list-selected.png' alt="Selected hero")
|
|
||||||
:marked
|
|
||||||
We’ll add a property binding on `class` for the `selected` class to the template. We'll set this to an expression that compares the current `selectedHero` to the `hero`.
|
|
||||||
|
|
||||||
The key is the name of the CSS class (`selected`). The value is `true` if the two heroes match and `false` otherwise.
|
|
||||||
We’re saying “*apply the `selected` class if the heroes match, remove it if they don’t*”.
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'class-selected-1', 'app_component.dart (setting the CSS class)')(format=".")
|
|
||||||
:marked
|
|
||||||
Notice in the template that the `class.selected` is surrounded in square brackets (`[]`).
|
|
||||||
This is the syntax for a **property binding**, a binding in which data flows one way
|
|
||||||
from the data source (the expression `hero == selectedHero`) to a property of `class`.
|
|
||||||
+makeExample('toh-2/dart-snippets/app_component_snippets_pt2.dart', 'class-selected-2', 'app_component.dart (styling each hero)')(format=".")
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about [property bindings](../guide/template-syntax.html#property-binding)
|
|
||||||
in the Template Syntax chapter.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The browser reloads our app.
|
|
||||||
We select the hero Magneta and the selection is clearly identified by the background color.
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src='/resources/images/devguide/toh/heroes-list-1.png' alt="Output of heroes list app")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We select a different hero and the tell-tale color switches to that hero.
|
|
||||||
|
|
||||||
Here's the complete `app_component.dart` as it stands now:
|
|
||||||
|
|
||||||
+makeExample('toh-2/dart/lib/app_component.dart', '', 'app_component.dart')
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## The Road We’ve Travelled
|
|
||||||
Here’s what we achieved in this chapter:
|
|
||||||
|
|
||||||
* Our Tour of Heroes now displays a list of selectable heroes
|
|
||||||
* We added the ability to select a hero and show the hero’s details
|
|
||||||
* We learned how to use the built-in directives `ngIf` and `ngFor` in a component’s template
|
|
||||||
|
|
||||||
Run the <live-example></live-example> for this part.
|
|
||||||
|
|
||||||
### The Road Ahead
|
|
||||||
Our Tour of Heroes has grown, but it’s far from complete.
|
|
||||||
We can't put the entire app into a single component.
|
|
||||||
We need to break it up into sub-components and teach them to work together
|
|
||||||
as we learn in the [next chapter](toh-pt3.html).
|
|
|
@ -1,255 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Our app is growing.
|
|
||||||
Use cases are flowing in for reusing components, passing data to components, and creating more reusable assets. Let's separate the heroes list from the hero details and make the details component reusable.
|
|
||||||
|
|
||||||
Run the <live-example></live-example> for this part.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Where We Left Off
|
|
||||||
Before we continue with our Tour of Heroes, let’s verify we have the following structure. If not, we’ll need to go back and follow the previous chapters.
|
|
||||||
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
:marked
|
|
||||||
### Keep the app compiling and running
|
|
||||||
We want to start the Dart compiler, have it watch for changes, and start our server. We'll do this by typing
|
|
||||||
|
|
||||||
code-example(language="sh" class="code-shell").
|
|
||||||
pub serve
|
|
||||||
|
|
||||||
:marked
|
|
||||||
This will keep the application running while we continue to build the Tour of Heroes.
|
|
||||||
|
|
||||||
## Making a Hero Detail Component
|
|
||||||
Our heroes list and our hero details are in the same component in the same file.
|
|
||||||
They're small now but each could grow.
|
|
||||||
We are sure to receive new requirements for one and not the other.
|
|
||||||
Yet every change puts both components at risk and doubles the testing burden without benefit.
|
|
||||||
If we had to reuse the hero details elsewhere in our app,
|
|
||||||
the heroes list would tag along for the ride.
|
|
||||||
|
|
||||||
Our current component violates the
|
|
||||||
[Single Responsibility Principle](https://blog.8thlight.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html).
|
|
||||||
It's only a tutorial but we can still do things right —
|
|
||||||
especially if doing them right is easy and we learn how to build Angular apps in the process.
|
|
||||||
|
|
||||||
Let’s break the hero details out into its own component.
|
|
||||||
|
|
||||||
### Separating the Hero Detail Component
|
|
||||||
Add a new file named `hero_detail_component.dart` to the `lib` folder and create `HeroDetailComponent` as follows.
|
|
||||||
|
|
||||||
+makeExample('toh-3/dart/lib/hero_detail_component.dart', 'v1', 'lib/hero_detail_component.dart (initial version)')(format=".")
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
### Naming conventions
|
|
||||||
We like to identify at a glance which classes are components and which files contain components.
|
|
||||||
|
|
||||||
Notice that we have an `AppComponent` in a file named `app_component.dart` and our new
|
|
||||||
`HeroDetailComponent` is in a file named `hero_detail_component.dart`.
|
|
||||||
|
|
||||||
All of our component names end in "Component". All of our component file names end in "_component".
|
|
||||||
|
|
||||||
We spell our filenames in lower **underscore case**
|
|
||||||
(AKA **[snake_case](../guide/glossary.html#snake_case)**) so we don't worry about
|
|
||||||
case sensitivity on the server or in source control.
|
|
||||||
|
|
||||||
<!-- TODO
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about naming conventions in the chapter [Naming Conventions]
|
|
||||||
:marked
|
|
||||||
-->
|
|
||||||
:marked
|
|
||||||
We begin by importing the Angular `core.dart` file,
|
|
||||||
so that we can use common types like `@Component` when we create
|
|
||||||
our component.
|
|
||||||
|
|
||||||
We create metadata with the `@Component` annotation where we
|
|
||||||
specify the selector name that identifies this component's element.
|
|
||||||
|
|
||||||
When we finish here, we'll import it into `AppComponent` and create a corresponding `<my-hero-detail>` element.
|
|
||||||
:marked
|
|
||||||
#### Hero Detail Template
|
|
||||||
At the moment, the *Heroes* and *Hero Detail* views are combined in one template in `AppComponent`.
|
|
||||||
Let’s **cut** the *Hero Detail* content from `AppComponent` and **paste** it into the new template property of `HeroDetailComponent`.
|
|
||||||
|
|
||||||
We previously bound to the `selectedHero.name` property of the `AppComponent`.
|
|
||||||
Our `HeroDetailComponent` will have a `hero` property, not a `selectedHero` property.
|
|
||||||
So we replace `selectedHero` with `hero` everywhere in our new template. That's our only change.
|
|
||||||
The result looks like this:
|
|
||||||
|
|
||||||
+makeExample('toh-3/dart/lib/hero_detail_component.dart', 'template', 'lib/hero_detail_component.dart (template)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Now our hero detail layout exists only in the `HeroDetailComponent`.
|
|
||||||
|
|
||||||
#### Add the *hero* property
|
|
||||||
Let’s add that `hero` property we were talking about to the component class.
|
|
||||||
+makeExample('toh-3/dart/lib/hero_detail_component.dart', 'hero')
|
|
||||||
:marked
|
|
||||||
Uh oh. We declared the `hero` property as type `Hero` but our `Hero` class is over in the `app_component.dart` file.
|
|
||||||
We have two components, each in their own file, that need to reference the `Hero` class.
|
|
||||||
|
|
||||||
We solve the problem by relocating the `Hero` class from `app_component.dart` to its own `hero.dart` file.
|
|
||||||
|
|
||||||
+makeExample('toh-3/dart/lib/hero.dart', '', 'lib/hero.dart')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Add the following import statement near the top of **both `app_component.dart` and `hero_detail_component.dart`**.
|
|
||||||
|
|
||||||
+makeExample('toh-3/dart/lib/hero_detail_component.dart', 'hero-import')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
#### The *hero* property is an ***input***
|
|
||||||
|
|
||||||
The `HeroDetailComponent` must be told what hero to display. Who will tell it? The parent `AppComponent`!
|
|
||||||
|
|
||||||
The `AppComponent` knows which hero to show: the hero that the user selected from the list.
|
|
||||||
The user's selection is in its `selectedHero` property.
|
|
||||||
|
|
||||||
We will soon update the `AppComponent` template so that it binds its `selectedHero` property
|
|
||||||
to the `hero` property of our `HeroDetailComponent`. The binding *might* look like this:
|
|
||||||
code-example(language="html").
|
|
||||||
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
|
|
||||||
:marked
|
|
||||||
Notice that the `hero` property is the ***target*** of a property binding — it's in square brackets to the left of the (=).
|
|
||||||
|
|
||||||
Angular insists that we declare a ***target*** property to be an ***input*** property.
|
|
||||||
If we don't, Angular rejects the binding and throws an error.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We explain input properties in more detail [here](../guide/attribute-directives.html#why-input)
|
|
||||||
where we also explain why *target* properties require this special treatment and
|
|
||||||
*source* properties do not.
|
|
||||||
:marked
|
|
||||||
There are a couple of ways we can declare that `hero` is an *input*.
|
|
||||||
We'll do it the way we *prefer*, by annotating the `hero` property with `@Input()`.
|
|
||||||
+makeExample('toh-3/dart/lib/hero_detail_component.dart', 'inputs')(format=".")
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about `@Input()` in the
|
|
||||||
[Attribute Directives](../guide/attribute-directives.html#input) chapter.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Refresh the AppComponent
|
|
||||||
We return to the `AppComponent` and teach it to use the `HeroDetailComponent`.
|
|
||||||
|
|
||||||
We begin by importing the `HeroDetailComponent` so we can refer to it.
|
|
||||||
|
|
||||||
+makeExample('toh-3/dart/lib/app_component.dart', 'hero-detail-import')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Find the location in the template where we removed the *Hero Detail* content
|
|
||||||
and add an element tag that represents the `HeroDetailComponent`.
|
|
||||||
code-example(language="html").
|
|
||||||
<my-hero-detail></my-hero-detail>
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
*my-hero-detail* is the name we set as the `selector` in the `HeroDetailComponent` metadata.
|
|
||||||
:marked
|
|
||||||
The two components won't coordinate until we bind the `selectedHero` property of the `AppComponent`
|
|
||||||
to the `HeroDetailComponent` element's `hero` property like this:
|
|
||||||
code-example(language="html").
|
|
||||||
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
|
|
||||||
:marked
|
|
||||||
The `AppComponent`’s template should now look like this
|
|
||||||
|
|
||||||
+makeExample('toh-3/dart/lib/app_component.dart', 'hero-detail-template', 'app_component.dart (template)')(format=".")
|
|
||||||
:marked
|
|
||||||
Thanks to the binding, the `HeroDetailComponent` should receive the hero from the `AppComponent` and display that hero's detail beneath the list.
|
|
||||||
The detail should update every time the user picks a new hero.
|
|
||||||
|
|
||||||
It's not happening yet!
|
|
||||||
|
|
||||||
We click among the heroes. No details. We look for an error in the console of the browser development tools. No error.
|
|
||||||
|
|
||||||
It is as if Angular were ignoring the new tag. That's because *it is ignoring the new tag*.
|
|
||||||
|
|
||||||
### The *directives* list
|
|
||||||
A browser ignores HTML tags and attributes that it doesn't recognize. So does Angular.
|
|
||||||
|
|
||||||
We've imported `HeroDetailComponent`, we've used it in the template, but we haven't told Angular about it.
|
|
||||||
|
|
||||||
We tell Angular about it by listing it in the metadata `directives` list. Let's add that list property to the bottom of the
|
|
||||||
`@Component` configuration object, immediately after the `template` and `styles` properties.
|
|
||||||
+makeExample('toh-3/dart/lib/app_component.dart', 'directives')(format=".")
|
|
||||||
|
|
||||||
|
|
||||||
:marked
|
|
||||||
### It works!
|
|
||||||
When we view our app in the browser we see the list of heroes.
|
|
||||||
When we select a hero we can see the selected hero’s details.
|
|
||||||
|
|
||||||
What's fundamentally new is that we can use this `HeroDetailComponent`
|
|
||||||
to show hero details anywhere in the app.
|
|
||||||
|
|
||||||
We’ve created our first reusable component!
|
|
||||||
|
|
||||||
### Reviewing the App Structure
|
|
||||||
Let’s verify that we have the following structure after all of our good refactoring in this chapter:
|
|
||||||
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file hero.dart
|
|
||||||
.file hero_detail_component.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
:marked
|
|
||||||
Here are the code files we discussed in this chapter.
|
|
||||||
|
|
||||||
+makeTabs(`
|
|
||||||
toh-3/dart/lib/hero_detail_component.dart,
|
|
||||||
toh-3/dart/lib/app_component.dart,
|
|
||||||
toh-3/dart/lib/hero.dart
|
|
||||||
`,'',`
|
|
||||||
lib/hero_detail_component.dart,
|
|
||||||
lib/app_component.dart,
|
|
||||||
lib/hero.dart
|
|
||||||
`)
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## The Road We’ve Travelled
|
|
||||||
Let’s take stock of what we’ve built.
|
|
||||||
|
|
||||||
* We created a reusable component
|
|
||||||
* We learned how to make a component accept input
|
|
||||||
* We learned to bind a parent component to a child component.
|
|
||||||
* We learned to declare the application directives we need in a `directives` list.
|
|
||||||
|
|
||||||
Run the <live-example></live-example> for this part.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## The Road Ahead
|
|
||||||
Our Tour of Heroes has become more reusable with shared components.
|
|
||||||
|
|
||||||
We're still getting our (mock) data within the `AppComponent`.
|
|
||||||
That's not sustainable.
|
|
||||||
We should refactor data access to a separate service
|
|
||||||
and share it among the components that need data.
|
|
||||||
|
|
||||||
We’ll learn to create services in the [next tutorial](toh-pt4.html) chapter.
|
|
|
@ -1,400 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The Tour of Heroes is evolving and we anticipate adding more components in the near future.
|
|
||||||
|
|
||||||
Multiple components will need access to hero data and we don't want to copy and
|
|
||||||
paste the same code over and over.
|
|
||||||
Instead, we'll create a single reusable data service and learn to
|
|
||||||
inject it in the components that need it.
|
|
||||||
|
|
||||||
Refactoring data access to a separate service keeps the component lean and focused on supporting the view.
|
|
||||||
It also makes it easier to unit test the component with a mock service.
|
|
||||||
|
|
||||||
Because data services are invariably asynchronous,
|
|
||||||
we'll finish the chapter with a **!{_Promise}**-based version of the data service.
|
|
||||||
|
|
||||||
Run the <live-example></live-example> for this part.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Where We Left Off
|
|
||||||
Before we continue with our Tour of Heroes, let’s verify we have the following structure.
|
|
||||||
If not, we’ll need to go back and follow the previous chapters.
|
|
||||||
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file hero.dart
|
|
||||||
.file hero_detail_component.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
:marked
|
|
||||||
### Keep the app compiling and running
|
|
||||||
Open a terminal/console window.
|
|
||||||
Start the Dart compiler, watch for changes, and start our server by entering the command:
|
|
||||||
|
|
||||||
code-example(language="sh" class="code-shell").
|
|
||||||
pub serve
|
|
||||||
|
|
||||||
:marked
|
|
||||||
The application runs and updates automatically as we continue to build the Tour of Heroes.
|
|
||||||
|
|
||||||
## Creating a Hero Service
|
|
||||||
Our stakeholders have shared their larger vision for our app.
|
|
||||||
They tell us they want to show the heroes in various ways on different pages.
|
|
||||||
We already can select a hero from a list.
|
|
||||||
Soon we'll add a dashboard with the top performing heroes and create a separate view for editing hero details.
|
|
||||||
All three views need hero data.
|
|
||||||
|
|
||||||
At the moment the `AppComponent` defines mock heroes for display.
|
|
||||||
We have at least two objections.
|
|
||||||
First, defining heroes is not the component's job.
|
|
||||||
Second, we can't easily share that list of heroes with other components and views.
|
|
||||||
|
|
||||||
We can refactor this hero data acquisition business to a single service that provides heroes, and
|
|
||||||
share that service with all components that need heroes.
|
|
||||||
|
|
||||||
### Create the HeroService
|
|
||||||
Create a file in the `lib` folder called `hero_service.dart`.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We've adopted a convention in which we spell the name of a service in lowercase followed by `_service`.
|
|
||||||
If the service name were multi-word, we'd spell the base filename in lower underscore case (also called [snake_case](../guide/glossary.html#snake_case)).
|
|
||||||
The `SpecialSuperHeroService` would be defined in the `special_super_hero_service.dart` file.
|
|
||||||
:marked
|
|
||||||
We name the class `HeroService`.
|
|
||||||
|
|
||||||
+makeExample('toh-4/dart/lib/hero_service_1.dart', 'empty-class', 'lib/hero_service.dart (starting point)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
### Injectable Services
|
|
||||||
Notice that we used an `@Injectable()` annotation.
|
|
||||||
.callout.is-helpful
|
|
||||||
:marked
|
|
||||||
**Don't forget the parentheses!** Neglecting them leads to an error that's difficult to diagnose.
|
|
||||||
:marked
|
|
||||||
Dart sees the `@Injectable()` annotation and emits metadata about our service,
|
|
||||||
metadata that Angular may need to inject other dependencies into this service.
|
|
||||||
|
|
||||||
The `HeroService` doesn't have any dependencies *at the moment*. Add the annotation anyway.
|
|
||||||
It is a "best practice" to apply the `@Injectable()` annotation *from the start*
|
|
||||||
both for consistency and for future-proofing.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
### Getting Heroes
|
|
||||||
Add a `getHeroes` method stub.
|
|
||||||
+makeExample('toh-4/dart/lib/hero_service_1.dart', 'getHeroes-stub', 'lib/hero_service.dart (getHeroes stub)')(format=".")
|
|
||||||
:marked
|
|
||||||
We're holding back on the implementation for a moment to make an important point.
|
|
||||||
|
|
||||||
The consumer of our service doesn't know how the service gets the data.
|
|
||||||
Our `HeroService` could get `Hero` data from anywhere.
|
|
||||||
It could get the data from a web service or local storage
|
|
||||||
or from a mock data source.
|
|
||||||
|
|
||||||
That's the beauty of removing data access from the component.
|
|
||||||
We can change our minds about the implementation as often as we like,
|
|
||||||
for whatever reason, without touching any of the components that need heroes.
|
|
||||||
|
|
||||||
|
|
||||||
### Mock Heroes
|
|
||||||
We already have mock `Hero` data sitting in the `AppComponent`. It doesn't belong there. It doesn't belong *here* either.
|
|
||||||
We'll move the mock data to its own file.
|
|
||||||
|
|
||||||
Cut the `mockHeroes` list from `app_component.dart` and paste it to a new file in the `lib` folder named `mock_heroes.dart`.
|
|
||||||
We copy the `import 'hero.dart'` statement as well because the heroes list uses the `Hero` class.
|
|
||||||
|
|
||||||
+makeExample('toh-4/dart/lib/mock_heroes.dart', null, 'lib/mock_heroes.dart')
|
|
||||||
:marked
|
|
||||||
Meanwhile, back in `app_component.dart` where we cut away the `mockHeroes` list,
|
|
||||||
we leave behind an uninitialized `heroes` property:
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'heroes-prop', 'lib/app_component.dart (heroes property)')(format=".")
|
|
||||||
:marked
|
|
||||||
### Return Mocked Heroes
|
|
||||||
Back in the `HeroService` we import the mock `mockHeroes` and return it from the `getHeroes` method.
|
|
||||||
Our `HeroService` looks like this:
|
|
||||||
+makeExample('toh-4/dart/lib/hero_service_1.dart', 'final', 'lib/hero_service.dart')(format=".")
|
|
||||||
:marked
|
|
||||||
### Use the Hero Service
|
|
||||||
We're ready to use the `HeroService` in other components starting with our `AppComponent`.
|
|
||||||
|
|
||||||
We begin, as usual, by importing the thing we want to use, the `HeroService`.
|
|
||||||
+makeExcerpt('toh-4/dart/lib/app_component.dart', 'hero-service-import')
|
|
||||||
:marked
|
|
||||||
Importing the service allows us to *reference* it in our code.
|
|
||||||
How should the `AppComponent` acquire a runtime concrete `HeroService` instance?
|
|
||||||
|
|
||||||
### Do we *new* the *HeroService*? No way!
|
|
||||||
We could create a new instance of the `HeroService` with `new` like this:
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'new-service')(format=".")
|
|
||||||
:marked
|
|
||||||
That's a bad idea for several reasons including
|
|
||||||
|
|
||||||
* Our component has to know how to create a `HeroService`.
|
|
||||||
If we ever change the `HeroService` constructor,
|
|
||||||
we'll have to find every place we create the service and fix it.
|
|
||||||
Running around patching code is error prone and adds to the test burden.
|
|
||||||
|
|
||||||
* We create a new service each time we use `new`.
|
|
||||||
What if the service should cache heroes and share that cache with others?
|
|
||||||
We couldn't do that.
|
|
||||||
|
|
||||||
* We're locking the `AppComponent` into a specific implementation of the `HeroService`.
|
|
||||||
It will be hard to switch implementations for different scenarios.
|
|
||||||
Can we operate offline?
|
|
||||||
Will we need different mocked versions under test?
|
|
||||||
Not easy.
|
|
||||||
|
|
||||||
*What if ... what if ... Hey, we've got work to do!*
|
|
||||||
|
|
||||||
We get it. Really we do.
|
|
||||||
But it is so ridiculously easy to avoid these problems that there is no excuse for doing it wrong.
|
|
||||||
|
|
||||||
### Inject the *HeroService*
|
|
||||||
|
|
||||||
Three lines replace the one line of *new*:
|
|
||||||
1. We add a property.
|
|
||||||
1. We add a constructor that sets the property.
|
|
||||||
1. We add to the component's `providers` metadata.
|
|
||||||
|
|
||||||
Here are the property and the constructor:
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'ctor', 'lib/app_component.dart (constructor)')(format='.')
|
|
||||||
:marked
|
|
||||||
The constructor does nothing except set the `_heroService`
|
|
||||||
property. The `HeroService` type of `_heroService`
|
|
||||||
identifies the constructor's parameter as
|
|
||||||
a `HeroService` injection site.
|
|
||||||
|
|
||||||
Now Angular will know to supply an instance of the `HeroService` when it creates a new `AppComponent`.
|
|
||||||
|
|
||||||
Angular has to get that instance from somewhere. That's the role of the Angular *Dependency Injector*.
|
|
||||||
The **Injector** has a **container** of previously created services.
|
|
||||||
Either it finds and returns a pre-existing `HeroService` from its container or it creates a new instance, adds
|
|
||||||
it to the container, and returns it to Angular.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about Dependency Injection in the [Dependency Injection](../guide/dependency-injection.html) chapter.
|
|
||||||
:marked
|
|
||||||
The *injector* does not know yet how to create a `HeroService`.
|
|
||||||
If we ran our code now, Angular would fail with an error:
|
|
||||||
code-example(format="nocode").
|
|
||||||
EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
|
|
||||||
:marked
|
|
||||||
We have to teach the *injector* how to make a `HeroService` by registering a `HeroService` **provider**.
|
|
||||||
Do that by adding the following `providers` parameter to the bottom of the component metadata
|
|
||||||
in the `@Component` annotation.
|
|
||||||
|
|
||||||
+makeExcerpt('toh-4/dart/lib/app_component_1.dart', 'providers')
|
|
||||||
:marked
|
|
||||||
The `providers` parameter tells Angular to create a fresh instance of the `HeroService` when it creates a new `AppComponent`.
|
|
||||||
The `AppComponent` can use that service to get heroes and so can every child component of its component tree.
|
|
||||||
<a id="child-component"></a>
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
### Services and the component tree
|
|
||||||
|
|
||||||
Recall that the `AppComponent` creates an instance of `HeroDetail` by virtue of the
|
|
||||||
`<my-hero-detail>` tag at the bottom of its template. That `HeroDetail` is a child of the `AppComponent`.
|
|
||||||
|
|
||||||
If the `HeroDetailComponent` needed its parent component's `HeroService`,
|
|
||||||
it would ask Angular to inject the service into its constructor which would look just like the one for `AppComponent`:
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'ctor', 'lib/hero_detail_component.dart (constructor)')(format=".")
|
|
||||||
:marked
|
|
||||||
The `HeroDetailComponent` must *not* repeat its parent's `providers` list! Guess [why](#shadow-provider).
|
|
||||||
|
|
||||||
The `AppComponent` is the top level component of our application.
|
|
||||||
There should be only one instance of that component and only one instance of the `HeroService` in our entire app.
|
|
||||||
:marked
|
|
||||||
### *getHeroes* in the *AppComponent*
|
|
||||||
We've got the service in a `_heroService` private variable. Let's use it.
|
|
||||||
|
|
||||||
We pause to think. We can call the service and get the data in one line.
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'get-heroes')(format=".")
|
|
||||||
:marked
|
|
||||||
We don't really need a dedicated method to wrap one line. We write it anyway:
|
|
||||||
+makeExcerpt('toh-4/dart/lib/app_component_1.dart', 'getHeroes')
|
|
||||||
|
|
||||||
<a id="oninit"></a>
|
|
||||||
:marked
|
|
||||||
### The *ngOnInit* Lifecycle Hook
|
|
||||||
`AppComponent` should fetch and display heroes without a fuss.
|
|
||||||
Where do we call the `getHeroes` method? In a constructor? We do *not*!
|
|
||||||
|
|
||||||
Years of experience and bitter tears have taught us to keep complex logic out of the constructor,
|
|
||||||
especially anything that might call a server as a data access method is sure to do.
|
|
||||||
|
|
||||||
The constructor is for simple initializations like wiring constructor parameters to properties.
|
|
||||||
It's not for heavy lifting. We should be able to create a component in a test and not worry that it
|
|
||||||
might do real work — like calling a server! — before we tell it to do so.
|
|
||||||
|
|
||||||
If not the constructor, something has to call `getHeroes`.
|
|
||||||
|
|
||||||
Angular will call it if we implement the Angular **ngOnInit** *Lifecycle Hook*.
|
|
||||||
Angular offers a number of interfaces for tapping into critical moments in the component lifecycle:
|
|
||||||
at creation, after each change, and at its eventual destruction.
|
|
||||||
|
|
||||||
Each interface has a single method. When the component implements that method, Angular calls it at the appropriate time.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn more about lifecycle hooks in the [Lifecycle Hooks](../guide/lifecycle-hooks.html) chapter.
|
|
||||||
:marked
|
|
||||||
Here's the essential outline for the `OnInit` interface:
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'on-init', 'lib/app_component.dart (ngOnInit stub)')(format=".")
|
|
||||||
:marked
|
|
||||||
We write an `ngOnInit` method with our initialization logic inside and leave it to Angular to call it
|
|
||||||
at the right time. In our case, we initialize by calling `getHeroes`.
|
|
||||||
+makeExcerpt('toh-4/dart/lib/app_component_1.dart', 'ng-on-init')
|
|
||||||
:marked
|
|
||||||
Our application should be running as expected, showing a list of heroes and a hero detail view
|
|
||||||
when we click on a hero name.
|
|
||||||
|
|
||||||
We're getting closer. But something isn't quite right.
|
|
||||||
|
|
||||||
<a id="async"></a>
|
|
||||||
## Async Services and !{_Promise}
|
|
||||||
Our `HeroService` returns a list of mock heroes immediately.
|
|
||||||
Its `getHeroes` signature is synchronous
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'get-heroes')(format=".")
|
|
||||||
:marked
|
|
||||||
Ask for heroes and they are there in the returned result.
|
|
||||||
|
|
||||||
Someday we're going to get heroes from a remote server. We don’t call http yet, but we aspire to in later chapters.
|
|
||||||
|
|
||||||
When we do, we'll have to wait for the server to respond and we won't be able to block the UI while we wait,
|
|
||||||
even if we want to (which we shouldn't) because the browser won't block.
|
|
||||||
|
|
||||||
We'll have to use some kind of asynchronous technique and that will change the signature of our `getHeroes` method.
|
|
||||||
|
|
||||||
We'll use *!{_Promise}s*.
|
|
||||||
|
|
||||||
### The Hero Service returns a !{_Promise}
|
|
||||||
|
|
||||||
We ask an asynchronous service to do some work and give us the result in the !{_Promise}.
|
|
||||||
The service does that work (somewhere) and eventually it updates the !{_Promise} with the results of the work or an error.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
We are simplifying. Learn about !{_Promise}s in the tutorial
|
|
||||||
[Asynchronous Programming: Futures](https://www.dartlang.org/docs/tutorials/futures/).
|
|
||||||
:marked
|
|
||||||
Update the `HeroService` with this !{_Promise}-returning `getHeroes` method:
|
|
||||||
+makeExample('toh-4/dart/lib/hero_service.dart', 'get-heroes', 'lib/hero_service.dart (excerpt)')(format=".")
|
|
||||||
:marked
|
|
||||||
We're still mocking the data. We're simulating the behavior of an ultra-fast, zero-latency server,
|
|
||||||
by returning a !{_Promise} that will quickly resolve with our mock heroes as the result.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Marking the method's body with `async` makes the method immediately return a `Future` object.
|
|
||||||
That !{_Promise} later completes with the method's return value.
|
|
||||||
For more information on async functions, see
|
|
||||||
[Declaring async functions](https://www.dartlang.org/docs/dart-up-and-running/ch02.html#async) in the Dart language tour.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
### Act on the !{_Promise}
|
|
||||||
Returning to the `AppComponent` and its `getHeroes` method, we see that it still looks like this:
|
|
||||||
+makeExample('toh-4/dart/lib/app_component_1.dart', 'getHeroes', 'lib/app_component.dart (getHeroes - old)')(format=".")
|
|
||||||
:marked
|
|
||||||
As a result of our change to `HeroService`, we're now setting `heroes` to a !{_Promise} rather than a list of heroes.
|
|
||||||
|
|
||||||
We have to change our implementation to *act on the !{_Promise} when it resolves*.
|
|
||||||
We can *await* for the !{_Promise} to resolve, and then display the heroes:
|
|
||||||
+makeExample('toh-4/dart/lib/app_component.dart', 'get-heroes', 'lib/app_component.dart (getHeroes - revised)')(format=".")
|
|
||||||
:marked
|
|
||||||
Our code waits until the !{_Promise} completes, and then
|
|
||||||
sets the component's `heroes` property to the list of heroes returned by the service. That's all there is to it!
|
|
||||||
|
|
||||||
Our app should still be running, still showing a list of heroes, and still
|
|
||||||
responding to a name selection with a detail view.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Check out the "[Take it slow](#slow)" appendix to see what the app might be like with a poor connection.
|
|
||||||
:marked
|
|
||||||
### Review the App Structure
|
|
||||||
Let’s verify that we have the following structure after all of our good refactoring in this chapter:
|
|
||||||
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file hero.dart
|
|
||||||
.file hero_detail_component.dart
|
|
||||||
.file hero_service.dart
|
|
||||||
.file mock_heroes.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
:marked
|
|
||||||
Here are the code files we discussed in this chapter.
|
|
||||||
|
|
||||||
+makeTabs(`
|
|
||||||
toh-4/dart/lib/hero_service.dart,
|
|
||||||
toh-4/dart/lib/app_component.dart,
|
|
||||||
toh-4/dart/lib/mock_heroes.dart
|
|
||||||
`,'',`
|
|
||||||
lib/hero_service.dart,
|
|
||||||
lib/app_component.dart,
|
|
||||||
lib/mock_heroes.dart
|
|
||||||
`)
|
|
||||||
:marked
|
|
||||||
## The Road We’ve Travelled
|
|
||||||
Let’s take stock of what we’ve built.
|
|
||||||
|
|
||||||
* We created a service class that can be shared by many components.
|
|
||||||
* We used the `ngOnInit` Lifecycle Hook to get our heroes when our `AppComponent` activates.
|
|
||||||
* We defined our `HeroService` as a provider for our `AppComponent`.
|
|
||||||
* We created mock hero data and imported them into our service.
|
|
||||||
* We designed our service to return a !{_Promise} and our component to get our data from the !{_Promise}.
|
|
||||||
|
|
||||||
|
|
||||||
### The Road Ahead
|
|
||||||
Our Tour of Heroes has become more reusable using shared components and services.
|
|
||||||
We want to create a dashboard, add menu links that route between the views, and format data in a template.
|
|
||||||
As our app evolves, we’ll learn how to design it to make it easier to grow and maintain.
|
|
||||||
|
|
||||||
We learn about Angular Component Router and navigation among the views in the [next tutorial](toh-pt5.html) chapter.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
<a id="slow"></a>
|
|
||||||
:marked
|
|
||||||
### Appendix: Take it slow
|
|
||||||
|
|
||||||
We can simulate a slow connection.
|
|
||||||
|
|
||||||
Add the following `getHeroesSlowly` method to the `HeroService`:
|
|
||||||
+makeExample('toh-4/dart/lib/hero_service.dart', 'get-heroes-slowly', 'lib/hero_service.dart (getHeroesSlowly)')(format=".")
|
|
||||||
:marked
|
|
||||||
Like `getHeroes`, it also returns a !{_Promise}.
|
|
||||||
But this !{_Promise} waits 2 seconds before resolving the !{_Promise} with mock heroes.
|
|
||||||
|
|
||||||
Back in the `AppComponent`, replace
|
|
||||||
`_heroService.getHeroes` with `_heroService.getHeroesSlowly`
|
|
||||||
and see how the app behaves.
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
<a id="shadow-provider"></a>
|
|
||||||
:marked
|
|
||||||
### Appendix: Shadowing the parent's service
|
|
||||||
|
|
||||||
We stated [earlier](#child-component) that if we injected the parent `AppComponent` `HeroService`
|
|
||||||
into the `HeroDetailComponent`, *we must not add a providers list* to the `HeroDetailComponent` metadata.
|
|
||||||
|
|
||||||
Why? Because that tells Angular to create a new instance of the `HeroService` at the `HeroDetailComponent` level.
|
|
||||||
The `HeroDetailComponent` doesn't want its *own* service instance; it wants its *parent's* service instance.
|
|
||||||
Adding the `providers` list creates a new service instance that shadows the parent instance.
|
|
||||||
|
|
||||||
Think carefully about where and when to register a provider.
|
|
||||||
Understand the scope of that registration. Be careful not to create a new service instance at the wrong level.
|
|
|
@ -1,168 +0,0 @@
|
||||||
extends ../../../ts/_cache/tutorial/toh-pt5
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _appRoutingTsVsAppComp = 'AppComponent'
|
|
||||||
- var _RoutesVsAtRouteConfig = '@RouteConfig'
|
|
||||||
- var _RouterModuleVsRouterDirectives = 'ROUTER_DIRECTIVES'
|
|
||||||
- var _redirectTo = 'useAsDefault'
|
|
||||||
|
|
||||||
block intro-file-tree
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.dart
|
|
||||||
.file hero.dart
|
|
||||||
.file hero_detail_component.dart
|
|
||||||
.file hero_service.dart
|
|
||||||
.file mock_heroes.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
|
|
||||||
block keep-app-running
|
|
||||||
:marked
|
|
||||||
### Keep the app compiling and running
|
|
||||||
|
|
||||||
Open a terminal/console window.
|
|
||||||
Start the Dart compiler, watch for changes, and start our server by entering the command:
|
|
||||||
|
|
||||||
code-example(language="sh" class="code-shell").
|
|
||||||
pub serve
|
|
||||||
|
|
||||||
block app-comp-v1
|
|
||||||
+makeExcerpt('lib/app_component_1.dart (v1)', '')
|
|
||||||
|
|
||||||
block angular-router
|
|
||||||
:marked
|
|
||||||
The Angular router is a combination of multiple services
|
|
||||||
(`ROUTER_PROVIDERS`), multiple directives (`ROUTER_DIRECTIVES`), and a
|
|
||||||
configuration annotation (`RouteConfig`). You get them all by importing
|
|
||||||
the router library:
|
|
||||||
|
|
||||||
+makeExcerpt('app/app.component.ts (router imports)', 'import-router')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
### Make the router available
|
|
||||||
|
|
||||||
Not all apps need routing, which is why the Angular *Component Router* is
|
|
||||||
in a separate, optional library module.
|
|
||||||
|
|
||||||
Like for any service, you make router services available to the application
|
|
||||||
by adding them to the `providers` list. Update the `directives` and
|
|
||||||
`providers` lists to include the router assets:
|
|
||||||
|
|
||||||
+makeExcerpt('app/app.component.ts (excerpt)', 'directives-and-providers')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
`AppComponent` no longer shows heroes, that will be the router's job,
|
|
||||||
so you can remove the `HeroesComponent` from the `directives` list.
|
|
||||||
You'll soon remove `<my-heroes>` from the template too.
|
|
||||||
|
|
||||||
block router-config-intro
|
|
||||||
:marked
|
|
||||||
### Configure routes and add the router
|
|
||||||
|
|
||||||
The `AppComponent` doesn't have a router yet. You'll use the `@RouteConfig`
|
|
||||||
annotation to simultaneously:
|
|
||||||
|
|
||||||
- Assign a router to the component
|
|
||||||
- Configure that router with *routes*
|
|
||||||
|
|
||||||
block routerLink
|
|
||||||
:marked
|
|
||||||
Notice the `[routerLink]` binding in the anchor tag.
|
|
||||||
You bind the `RouterLink` directive (another of the `ROUTER_DIRECTIVES`) to a list
|
|
||||||
that tells the router where to navigate when the user clicks the link.
|
|
||||||
|
|
||||||
You define a *routing instruction* with a *link parameters list*.
|
|
||||||
The list only has one element in our little sample, the quoted ***name* of the route** to follow.
|
|
||||||
Looking back at the route configuration, confirm that `'Heroes'` is the name of the route to the `HeroesComponent`.
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Learn about the *link parameters list*
|
|
||||||
in the [Routing](../guide/router.html#link-parameters-array) chapter.
|
|
||||||
|
|
||||||
block redirect-vs-use-as-default
|
|
||||||
:marked
|
|
||||||
You don't need a route definition for that. Instead,
|
|
||||||
add `useAsDefault: true` to the dashboard *route definition* and the
|
|
||||||
router will display the dashboard when the browser URL doesn't match an existing route.
|
|
||||||
|
|
||||||
block templateUrl-path-resolution
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
The value of `templateUrl` can be an [asset][] in this package or another
|
|
||||||
package. To use an asset in another package, use a full package reference,
|
|
||||||
such as `'package:some_other_package/dashboard_component.html'`.
|
|
||||||
|
|
||||||
[asset]: https://www.dartlang.org/tools/pub/glossary#asset
|
|
||||||
|
|
||||||
block route-params
|
|
||||||
:marked
|
|
||||||
You will no longer receive the hero in a parent component property binding.
|
|
||||||
The new `HeroDetailComponent` should take the `id` parameter from the router's
|
|
||||||
`RouteParams` service and use the `HeroService` to fetch the hero with that `id`.
|
|
||||||
|
|
||||||
block ngOnInit
|
|
||||||
:marked
|
|
||||||
Inside the `ngOnInit` lifecycle hook, extract the `id` parameter value from the `RouteParams` service
|
|
||||||
and use the `HeroService` to fetch the hero with that `id`.
|
|
||||||
|
|
||||||
block extract-id
|
|
||||||
:marked
|
|
||||||
Notice how you can extract the `id` by calling the `RouteParams.get` method.
|
|
||||||
|
|
||||||
block heroes-component-cleanup
|
|
||||||
:marked
|
|
||||||
Because the template for `HeroesComponent` no longer uses `HeroDetailComponent`
|
|
||||||
directly — instead using the router to _navigate_ to it — you can
|
|
||||||
drop the `directives` argument from `@Component` and remove the unused hero detail
|
|
||||||
import. The revised `@Component` looks like this:
|
|
||||||
|
|
||||||
block css-files
|
|
||||||
+makeTabs(
|
|
||||||
`toh-5/dart/lib/hero_detail_component.css,
|
|
||||||
toh-5/dart/lib/dashboard_component.css`,
|
|
||||||
null,
|
|
||||||
`lib/hero_detail_component.css,
|
|
||||||
lib/dashboard_component.css`)
|
|
||||||
|
|
||||||
block router-link-active
|
|
||||||
:marked
|
|
||||||
**The *router-link-active* class**
|
|
||||||
|
|
||||||
The Angular Router adds the `router-link-active` class to the HTML navigation element
|
|
||||||
whose route matches the active route. All you have to do is define the style for it. Sweet!
|
|
||||||
|
|
||||||
block file-tree-end
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.css
|
|
||||||
.file app_component.dart
|
|
||||||
.file dashboard_component.css
|
|
||||||
.file dashboard_component.dart
|
|
||||||
.file dashboard_component.html
|
|
||||||
.file hero.dart
|
|
||||||
.file hero_detail_component.css
|
|
||||||
.file hero_detail_component.dart
|
|
||||||
.file hero_detail_component.html
|
|
||||||
.file hero_service.dart
|
|
||||||
.file heroes_component.css
|
|
||||||
.file heroes_component.dart
|
|
||||||
.file heroes_component.html
|
|
||||||
.file mock_heroes.dart
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file index.html
|
|
||||||
.file main.dart
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
|
@ -1,191 +0,0 @@
|
||||||
extends ../../../ts/_cache/tutorial/toh-pt6.jade
|
|
||||||
|
|
||||||
block includes
|
|
||||||
include ../_util-fns
|
|
||||||
- var _Http = 'BrowserClient';
|
|
||||||
- var _Angular_Http = 'Dart <code>BrowserClient</code>'
|
|
||||||
- var _httpUrl = 'https://pub.dartlang.org/packages/http'
|
|
||||||
- var _Angular_http_library = 'Dart <a href="' + _httpUrl + '"><b>http</b></a> package'
|
|
||||||
- var _HttpModule = 'BrowserClient'
|
|
||||||
- var _JSON_stringify = 'JSON.encode'
|
|
||||||
|
|
||||||
block start-server-and-watch
|
|
||||||
:marked
|
|
||||||
### Keep the app compiling and running
|
|
||||||
|
|
||||||
Open a terminal/console window.
|
|
||||||
Start the Dart compiler, watch for changes, and start our server by entering the command:
|
|
||||||
|
|
||||||
code-example(language="sh" class="code-shell").
|
|
||||||
pub serve
|
|
||||||
|
|
||||||
block http-library
|
|
||||||
:marked
|
|
||||||
We'll be using the !{_Angular_http_library}'s
|
|
||||||
`BrowserClient` class to communicate with a server.
|
|
||||||
|
|
||||||
### Pubspec updates
|
|
||||||
|
|
||||||
Update package dependencies by adding the
|
|
||||||
`stream_transformers` and !{_Angular_http_library}s.
|
|
||||||
|
|
||||||
We also need to add a `resolved_identifiers` entry, to inform the [angular2
|
|
||||||
transformer][ng2x] that we'll be using `BrowserClient`. (For an explanation of why
|
|
||||||
this extra configuration is needed, see the [HTTP client chapter][guide-http].) We'll
|
|
||||||
also need to use `Client` from http, so let's add that now as well.
|
|
||||||
|
|
||||||
Update `pubspec.yaml` to look like this (additions are highlighted):
|
|
||||||
|
|
||||||
[guide-http]: ../guide/server-communication.html#http-providers
|
|
||||||
[ng2x]: https://github.com/angular/angular/wiki/Angular-2-Dart-Transformer
|
|
||||||
|
|
||||||
- var stylePattern = { pnk: /(http.*|stream.*|resolved_identifiers:|Browser.*|Client.*)/gm };
|
|
||||||
+makeExcerpt('pubspec.yaml', 'additions', null, stylePattern)
|
|
||||||
|
|
||||||
block http-providers
|
|
||||||
:marked
|
|
||||||
Before our app can use `#{_Http}`, we have to register it as a service provider.
|
|
||||||
|
|
||||||
We should be able to access `!{_Http}` services from anywhere in the application.
|
|
||||||
So we register it in the `bootstrap` call where we
|
|
||||||
launch the application and its root `AppComponent`.
|
|
||||||
|
|
||||||
+makeExcerpt('app/main.ts','v1')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Notice that we supply `!{_HttpModule}` in a list, as the second parameter to
|
|
||||||
the `bootstrap` method. This has the same effect as the `providers` list in
|
|
||||||
`@Component` annotation.
|
|
||||||
|
|
||||||
block backend
|
|
||||||
:marked
|
|
||||||
We want to replace `BrowserClient`, the service that talks to the remote server,
|
|
||||||
with the in-memory web API service.
|
|
||||||
Our in-memory web API service, shown below, is implemented using the
|
|
||||||
`http` library `MockClient` class.
|
|
||||||
All `http` client implementations share a common `Client` interface, so
|
|
||||||
we'll have our app use the `Client` type so that we can freely switch between
|
|
||||||
implementations.
|
|
||||||
|
|
||||||
block dont-be-distracted-by-backend-subst
|
|
||||||
//- No backend substitution but we do need to comment on the changes to Hero.
|
|
||||||
:marked
|
|
||||||
As is common for web API services, our mock in-memory service will be
|
|
||||||
encoding and decoding heroes in JSON format, so we enhance the `Hero`
|
|
||||||
class with these capabilities:
|
|
||||||
|
|
||||||
+makeExample('lib/hero.dart')
|
|
||||||
|
|
||||||
block get-heroes-details
|
|
||||||
:marked
|
|
||||||
To get the list of heroes, we first make an asynchronous call to
|
|
||||||
`http.get()`. Then we use the `_extractData` helper method to decode the
|
|
||||||
response body.
|
|
||||||
|
|
||||||
block observables-section-intro
|
|
||||||
:marked
|
|
||||||
Recall that `HeroService.getHeroes()` awaits for an `http.get()`
|
|
||||||
response and yields a _Future_ `List<Hero>`, which is fine when we are only
|
|
||||||
interested in a single result.
|
|
||||||
|
|
||||||
block search-criteria-intro
|
|
||||||
:marked
|
|
||||||
A [StreamController][], as its name implies, is a controller for a [Stream][] that allows us to
|
|
||||||
manipulate the underlying stream by adding data to it, for example.
|
|
||||||
|
|
||||||
In our sample, the underlying stream of strings (`_searchTerms.stream`) represents the hero
|
|
||||||
name search patterns entered by the user. Each call to `search` puts a new string into
|
|
||||||
the stream by calling `add` over the controller.
|
|
||||||
|
|
||||||
[Stream]: https://api.dartlang.org/stable/dart-async/Stream-class.html
|
|
||||||
[StreamController]: https://api.dartlang.org/stable/dart-async/StreamController-class.html
|
|
||||||
|
|
||||||
block observable-transformers
|
|
||||||
:marked
|
|
||||||
Fortunately, there are stream transformers that will help us reduce the request flow.
|
|
||||||
We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how:
|
|
||||||
|
|
||||||
* `transform(new Debounce(... 300)))` waits until the flow of search terms pauses for 300
|
|
||||||
milliseconds before passing along the latest string. We'll never make requests more frequently
|
|
||||||
than 300ms.
|
|
||||||
|
|
||||||
* `distinct()` ensures that we only send a request if a search term has changed.
|
|
||||||
There's no point in repeating a request for the same search term.
|
|
||||||
|
|
||||||
* `transform(new FlatMapLatest(...))` applies a map-like transformer that (1) calls our search
|
|
||||||
service for each search term that makes it through the debounce and distinct gauntlet and (2)
|
|
||||||
returns only the most recent search service result, discarding any previous results.
|
|
||||||
|
|
||||||
* `handleError()` handles errors. Our simple example prints the error to the console; a real
|
|
||||||
life application should do better.
|
|
||||||
|
|
||||||
block filetree
|
|
||||||
.filetree
|
|
||||||
.file angular_tour_of_heroes
|
|
||||||
.children
|
|
||||||
.file lib
|
|
||||||
.children
|
|
||||||
.file app_component.css
|
|
||||||
.file app_component.dart
|
|
||||||
.file dashboard_component.css
|
|
||||||
.file dashboard_component.dart
|
|
||||||
.file dashboard_component.html
|
|
||||||
.file hero.dart
|
|
||||||
.file hero_detail_component.css
|
|
||||||
.file hero_detail_component.dart
|
|
||||||
.file hero_detail_component.html
|
|
||||||
.file hero_search_component.css (new)
|
|
||||||
.file hero_search_component.dart (new)
|
|
||||||
.file hero_search_component.html (new)
|
|
||||||
.file hero_search_service.dart (new)
|
|
||||||
.file hero_service.dart
|
|
||||||
.file heroes_component.css
|
|
||||||
.file heroes_component.dart
|
|
||||||
.file heroes_component.html
|
|
||||||
.file in_memory_data_service.dart (new)
|
|
||||||
.file web
|
|
||||||
.children
|
|
||||||
.file main.dart
|
|
||||||
.file index.html
|
|
||||||
.file styles.css
|
|
||||||
.file pubspec.yaml
|
|
||||||
|
|
||||||
block file-summary
|
|
||||||
+makeTabs(
|
|
||||||
`toh-6/dart/lib/dashboard_component.dart,
|
|
||||||
toh-6/dart/lib/dashboard_component.html,
|
|
||||||
toh-6/dart/lib/hero.dart,
|
|
||||||
toh-6/dart/lib/hero_detail_component.dart,
|
|
||||||
toh-6/dart/lib/hero_detail_component.html,
|
|
||||||
toh-6/dart/lib/hero_service.dart,
|
|
||||||
toh-6/dart/lib/heroes_component.css,
|
|
||||||
toh-6/dart/lib/heroes_component.dart,
|
|
||||||
toh-6/dart/lib/in_memory_data_service.dart`,
|
|
||||||
',,,,,,,,',
|
|
||||||
`lib/dashboard_component.dart,
|
|
||||||
lib/dashboard_component.html,
|
|
||||||
lib/hero.dart,
|
|
||||||
lib/hero_detail_component.dart,
|
|
||||||
lib/hero_detail_component.html,
|
|
||||||
lib/hero_service.dart,
|
|
||||||
lib/heroes_component.css,
|
|
||||||
lib/heroes_component.dart,
|
|
||||||
lib/in_memory_data_service.dart`)
|
|
||||||
|
|
||||||
+makeTabs(
|
|
||||||
`toh-6/dart/lib/hero_search_component.css,
|
|
||||||
toh-6/dart/lib/hero_search_component.dart,
|
|
||||||
toh-6/dart/lib/hero_search_component.html,
|
|
||||||
toh-6/dart/lib/hero_search_service.dart`,
|
|
||||||
null,
|
|
||||||
`lib/hero_search_component.css,
|
|
||||||
lib/hero_search_component.dart,
|
|
||||||
lib/hero_search_component.html,
|
|
||||||
lib/hero_search_service.dart`)
|
|
||||||
|
|
||||||
+makeTabs(
|
|
||||||
`toh-6/dart/pubspec.yaml,
|
|
||||||
toh-6/dart/web/main.dart`,
|
|
||||||
null,
|
|
||||||
`pubspec.yaml,
|
|
||||||
web/main.dart`)
|
|
Loading…
Reference in New Issue