diff --git a/public/docs/_examples/forms/js/.gitignore b/public/docs/_examples/forms/js/.gitignore new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/public/docs/_examples/forms/js/.gitignore @@ -0,0 +1 @@ + diff --git a/public/docs/_examples/forms/js/app/app.component.js b/public/docs/_examples/forms/js/app/app.component.js new file mode 100644 index 0000000000..bb6b789938 --- /dev/null +++ b/public/docs/_examples/forms/js/app/app.component.js @@ -0,0 +1,12 @@ +// #docregion +(function(app) { + app.AppComponent = ng.core + .Component({ + selector: 'my-app', + template: '', + directives: [app.HeroFormComponent] + }) + .Class({ + constructor: function() {} + }); +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms/js/app/boot.js b/public/docs/_examples/forms/js/app/boot.js new file mode 100644 index 0000000000..56de1a0fdb --- /dev/null +++ b/public/docs/_examples/forms/js/app/boot.js @@ -0,0 +1,6 @@ +// #docregion +(function(app) { + document.addEventListener('DOMContentLoaded', function() { + ng.platform.browser.bootstrap(app.AppComponent); + }); +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms/js/app/hero-form.component.html b/public/docs/_examples/forms/js/app/hero-form.component.html new file mode 100644 index 0000000000..2e20f638b7 --- /dev/null +++ b/public/docs/_examples/forms/js/app/hero-form.component.html @@ -0,0 +1,195 @@ + + +
+ +
+

Hero Form

+ +
+ + +
+ + + +
+ Name is required +
+ +
+ +
+ + +
+ +
+ + +
+ Power is required +
+
+ + + + +
+
+ + +
+

You submitted the following:

+
+
Name
+
{{ model.name }}
+
+
+
Alter Ego
+
{{ model.alterEgo }}
+
+
+
Power
+
{{ model.power }}
+
+
+ +
+ +
+ + + +
+
+ + + + +
+
+ + + +
+ +
+ +
+

Hero Form

+
+
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + + +
+
+ + + + +
+ +
+

Hero Form

+
+ + {{diagnostic()}} +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ + + +
+ + + TODO: remove this: {{model.name}} + +
+ + + TODO: remove this: {{model.name}} + +
+
+ + + +
+ + +
TODO: remove this: {{spy.className}} + +
+ +
+
+ Name via form.controls = {{showFormControls(heroForm)}} +
+ +
diff --git a/public/docs/_examples/forms/js/app/hero-form.component.js b/public/docs/_examples/forms/js/app/hero-form.component.js new file mode 100644 index 0000000000..8988231189 --- /dev/null +++ b/public/docs/_examples/forms/js/app/hero-form.component.js @@ -0,0 +1,52 @@ +// #docplaster +// #docregion +// #docregion first, final +(function(app) { + app.HeroFormComponent = ng.core + .Component({ + selector: 'hero-form', + templateUrl: 'app/hero-form.component.html' + }) + .Class({ + // #docregion submitted + constructor: function() { + // #enddocregion submitted + this.powers = ['Really Smart', 'Super Flexible', + 'Super Hot', 'Weather Changer' + ]; + + this.model = new app.Hero(18, 'Dr IQ', this.powers[0], + 'Chuck Overstreet'); + + // #docregion submitted + this.submitted = false; + }, + onSubmit: function() { + this.submitted = true; + }, + // #enddocregion submitted + + // #enddocregion final + // TODO: Remove this when we're done + diagnostic: function() { + return JSON.stringify(this.model); + }, + // #enddocregion first + + + //////// DO NOT SHOW IN DOCS //////// + + // Reveal in html: + // AlterEgo via form.controls = {{showFormControls(hf)}} + showFormControls: function(form) { + return form.controls['alterEgo'] && + // #docregion form-controls + form.controls['name'].value; // Dr. IQ + // #enddocregion form-controls + }, + ///////////////////////////// + + // #docregion first, final + }); + // #enddocregion first, final +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms/js/app/hero.js b/public/docs/_examples/forms/js/app/hero.js new file mode 100644 index 0000000000..9c2449c922 --- /dev/null +++ b/public/docs/_examples/forms/js/app/hero.js @@ -0,0 +1,11 @@ +// #docregion +(function(app) { + app.Hero = Hero; + + function Hero(id, name, power, alterEgo) { + this.id = id; + this.name = name; + this.power = power; + this.alterEgo = alterEgo; + } +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms/js/example-config.json b/public/docs/_examples/forms/js/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/forms/js/index.html b/public/docs/_examples/forms/js/index.html new file mode 100644 index 0000000000..d5947f552f --- /dev/null +++ b/public/docs/_examples/forms/js/index.html @@ -0,0 +1,32 @@ + + + + + + + Hero Form + + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/forms/js/package.json b/public/docs/_examples/forms/js/package.json new file mode 100644 index 0000000000..8b6dd7e376 --- /dev/null +++ b/public/docs/_examples/forms/js/package.json @@ -0,0 +1,42 @@ +{ + "name": "angular2-examples-master", + "version": "1.0.0", + "description": "Master package.json, the superset of all dependencies for all of the _example package.json files.", + "main": "index.js", + "scripts": { + "tsc": "tsc", + "tsc:w": "tsc -w", + "lite": "lite-server", + "live": "live-server", + "start": "npm run lite", + "both": "concurrent \"npm run tsc:w\" \"npm run start\" ", + "test": "karma start karma.conf.js", + "build-and-test": "npm run tsc && npm run test" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "angular2": "2.0.0-alpha.52", + "systemjs": "0.19.6", + "es6-promise": "^3.0.2", + "es6-shim": "^0.33.3", + "reflect-metadata": "0.1.2", + "rxjs": "5.0.0-alpha.14", + "zone.js": "0.5.8", + "bootstrap": "^3.3.6" + }, + "devDependencies": { + "concurrently": "^1.0.0", + "lite-server": "^1.3.1", + "live-server": "^0.8.2", + "typescript": "^1.7.3", + "jasmine-core":"~2.1.0", + "karma": "^0.12.23", + "karma-chrome-launcher": "^0.1.4", + "karma-cli": "^0.0.4", + "karma-jasmine": "^0.3.6", + "rimraf": "^2.4.3" + } + +} diff --git a/public/docs/_examples/forms/js/plnkr.json b/public/docs/_examples/forms/js/plnkr.json new file mode 100644 index 0000000000..5617d94f9a --- /dev/null +++ b/public/docs/_examples/forms/js/plnkr.json @@ -0,0 +1,4 @@ +{ + "description": "Forms", + "files": ["**/*.js"] +} diff --git a/public/docs/_examples/forms/js/styles.css b/public/docs/_examples/forms/js/styles.css new file mode 100644 index 0000000000..d7e11405b1 --- /dev/null +++ b/public/docs/_examples/forms/js/styles.css @@ -0,0 +1,9 @@ +/* #docregion */ +.ng-valid[required] { + border-left: 5px solid #42A948; /* green */ +} + +.ng-invalid { + border-left: 5px solid #a94442; /* red */ +} +/* #enddocregion */ \ No newline at end of file diff --git a/public/docs/_examples/forms/ts/app/hero-form.component.ts b/public/docs/_examples/forms/ts/app/hero-form.component.ts index e68d17124e..09bcbeb7b5 100644 --- a/public/docs/_examples/forms/ts/app/hero-form.component.ts +++ b/public/docs/_examples/forms/ts/app/hero-form.component.ts @@ -42,4 +42,4 @@ export class HeroFormComponent { // #docregion first, final } -// #enddocregion first, final \ No newline at end of file +// #enddocregion first, final diff --git a/public/docs/_examples/forms/ts/app/hero.ts b/public/docs/_examples/forms/ts/app/hero.ts index 6ce9e8af1a..c128626452 100644 --- a/public/docs/_examples/forms/ts/app/hero.ts +++ b/public/docs/_examples/forms/ts/app/hero.ts @@ -8,4 +8,4 @@ export class Hero { public alterEgo?: string ) { } -} \ No newline at end of file +} diff --git a/public/docs/dart/latest/guide/forms.jade b/public/docs/dart/latest/guide/forms.jade index 9fda51294c..2190c2e4cd 100644 --- a/public/docs/dart/latest/guide/forms.jade +++ b/public/docs/dart/latest/guide/forms.jade @@ -53,7 +53,7 @@ include ../../../../_includes/_util-fns We'll discuss and learn to build a template-driven form that looks like this: figure.image-display - img(src="/resources/images/devguide/forms/hf-1.png" width="400px" alt="Clean Form") + 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 @@ -64,7 +64,7 @@ figure.image-display 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/hf-2.png" width="400px" alt="Invalid, Name Required") + 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. @@ -259,7 +259,7 @@ figure.image-display Running the app right now would be disappointing. figure.image-display - img(src="/resources/images/devguide/forms/hf-3.png" width="400px" alt="Early form with no binding") + 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. diff --git a/public/docs/js/latest/guide/_data.json b/public/docs/js/latest/guide/_data.json index 2d50d8fd90..3c5f5dbead 100644 --- a/public/docs/js/latest/guide/_data.json +++ b/public/docs/js/latest/guide/_data.json @@ -17,5 +17,10 @@ "user-input": { "title": "User Input", "intro": "DOM events drive user input in Angular. You can use the native events like click, mouseover, and keyup. Angular uses a special syntax to register events to DOM elements. This section covers all the ins and outs of using the event syntax." + }, + + "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." } } diff --git a/public/docs/js/latest/guide/forms.jade b/public/docs/js/latest/guide/forms.jade new file mode 100644 index 0000000000..14d0a09026 --- /dev/null +++ b/public/docs/js/latest/guide/forms.jade @@ -0,0 +1,640 @@ +include ../../../../_includes/_util-fns + +:marked + We’ve all used a form to login, 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 we can 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 local variables + + [Live Example](/resources/live-examples/forms/js/plnkr.html) +.l-main-section +:marked + ## Template-Driven Forms + + Many of us will build forms by writing templates in the Angular [template syntax](./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 $mdash; login forms, contact forms ... pretty much any business forms. + 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, boiler plate tasks we'd + otherwise wrestle with ourselves. + + We'll discuss and learn to build the following template-driven form: + +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 + p We'll' customize the colors and location of the "required" bar with standard CSS. + +:marked + We will build this form in the following sequence of 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. Disable the form’s submit button until the form is valid + +:marked + ## Setup + Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html). + + ## Create the Hero Model Class + + As users enter form data, we capture their changes and update an instance of a model. + We can't layout 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`). + + Create a new file in the app folder called `hero.js` and give it the following constructor: + ++makeExample('forms/js/app/hero.js', null, 'app/hero.js') + +:marked + It's an anemic model with few requirements and no behavior. Perfect for our demo. + + The `alterEgo` is optional and the constructor lets us omit it by being the last argument. + + We can create a new hero like this: +code-example(format=""). + var myHero = new Hero(42, 'SkyDog', + 'Fetch any object at any distance', 'Leslie Rollover'); + console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog" +:marked + We update the `` of the `index.html` to include this javascript file. + ++makeExample('forms/js/index.html', 'scripts-hero', 'index.html (excerpt)')(format=".") + +.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. + + Create a new file called `hero-form.component.js` and give it the following definition: + ++makeExample('forms/js/app/hero-form.component.js', 'first', 'app/hero-form.component.js') + +: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 2 concepts we’ve learned in previous chapters + + 1. We use the `ng.core` object from the Angular library as we usually do. + + 1. The `Component()` selector value of "hero-form" means we can drop this form in a parent template with a `` tag. + + 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](./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` method at the end to return a JSON representation of 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 don't we write the template inline in the component file as we often do + elsewhere in the Developer Guide? + + There is no “right” answer for all occasions. We like inline templates when they are short. + Most form templates won't be short. TypeScript and JavaScript 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. + We also like short files with a clear and obvious purpose like this one. + + We made a good choice to put the HTML template elsewhere. + We'll write that template in a moment. Before we do, we'll take a step back + and revise the `app.component.js` to make use of our new `HeroFormComponent`. + +:marked + Again we update the `` of the `index.html` to include the new javascript file. + ++makeExample('forms/js/index.html', 'scripts-hero-form', 'index.html (excerpt)')(format=".") + +.l-main-section +:marked + ## Revise the *app.component.js* + + `app.component.js` is the application's root component. It will host our new `HeroFormComponent`. + + Replace the contents of the "QuickStart" version with the following: ++makeExample('forms/js/app/app.component.js', null, 'app/app.component.js') + +:marked +.l-sub-section + :marked + There are only two changes: + + 1. The `template` is simply the new element tag identified by the component's `select` property. + + 1. The `directives` array tells Angular that our template depends upon the `HeroFormComponent` + which is itself a Directive (as are all Components). + +.l-main-section +:marked + ## Create an initial HTML Form Template + + Create a new template file called `hero-form.component.html` and give it the following definition: + ++makeExample('forms/js/app/hero-form.component.html', 'start', 'app/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* `` control has the HTML5 `required` attribute; + the *Alter Ego* `` 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 + come from [Twitter Boostrap](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. + +ol + li Open a terminal window in the application root folder and enter the command: + code-example(language="html" escape="html"). + npm install bootstrap --save + li Open index.html and add the following link to the <head>. + +makeExample('forms/js/index.html', 'bootstrap')(format=".") +:marked +.l-main-section +:marked + ## Add Powers with ***ngFor** + Our hero may 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 we might have seen before in the [Displaying Data](./displaying-data.html) chapter. + + Add the following HTML *immediately below* the *Alter Ego* group. ++makeExample('forms/js/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".") + +:marked + We are repeating the `` tag for each power in the list of Powers. + The `#p` local template variable is a different power in each iteration; + we display its name using the interpolation syntax with the double-curly-braces. + +.l-main-section +: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 those techniques again in our form. + Instead we'll introduce something new, the `NgModel` directive, that + makes binding our form to the model super-easy. + + Find the `` tag for the "Name" and update it like this + ++makeExample('forms/js/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".") + +.l-sub-section + :marked + We appended 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 we really are flowing values 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` method. + Then we can confirm that two-way data binding works *for the entire Hero model*. + + After revision the core of our form should have three `[(ngModel)]` bindings that + look much like this: + ++makeExample('forms/js/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)') + +:marked + If we ran the app right now and changed every Hero model property, the form might display like this: +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action") +:marked + The diagnostic near the top of the form + confirms that all of our changes are reflected in the model. + + **Delete** the `{{diagnostic()}}` binding at the top as it has served its purpose. + +.l-sub-section + :marked + ### Inside [(ngModel)] + *This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!* + + The punctuation in the binding syntax, [()], is a good clue to what's going on. + + In a Property Binding, a value flows from the model to a target property on screen. + We identify that target property by surrounding its name in brackets, []. + This is a one-way data binding **from the model to the view**. + + In an Event Binding, we flow the value from the target property on screen to the model. + We identify that target property by surrounding its name in parentheses, (). + This is a one-way data binding in the opposite direction **from the view to the model**. + + No wonder Angular chose to combine the punctuation as [()] + 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 we do in this re-write of the "Name" `` binding: + +makeExample('forms/js/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".") + + :marked +
The Property Binding should feel familiar. The Event Binding might seem strange. + + The `ngModelChange` is not an `` element event. + It is actually an event property of the `NgModel` directive. + When Angular sees a binding target in the form [(abc)], + it expects the `abc` directive to have an `abc` input property and an `abc-change` 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? We almost always prefer `[(ngModel)]`. + We might split the binding if we had to do something special in + the event handling such as debounce or throttle the key strokes. + + 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 ` tag. +:marked + Our application can ask an `NgControl` if the user touched the control, + if the value changed, or if the value became invalid. + + `NgControl` doesn't just track state; it updates the control with special + Angular CSS classes from the set we listed above. + We can leverage those class names to change the appearance of the + control and make messages appear or disappear. + + We'll explore those effects soon. Right now + we should **add `ngControl`to all three form controls**, + starting with the *Name* input box ++makeExample('forms/js/app/hero-form.component.html', 'ngControl-1', 'app/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`. + We didn't add the `NgForm` directive explicitly but it's here + and we'll talk about it [later in this chapter](#ngForm). + +.l-main-section +:marked + ## Add Custom CSS for Visual Feedback + + `NgControl` doesn't just track state. + It updates the control with three classes that reflect the state. + +table + tr + th State + th Class if true + th Class if false + tr + td Control has been visited + td ng-touched + td ng-untouched + tr + td Control's value has changed + td ng-dirty + td ng-pristine + tr + td Control's value is valid + td ng-valid + td ng-invalid +:marked + Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy** + to the "Name" `` tag and use the spy to display those classes. + ++makeExample('forms/js/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") + +:marked + Now run the app and focus on the *Name* input box. + Follow the next four steps *precisely* + + 1. Look but don't touched + 1. Click in the input box, then click outside the text input box + 1. Add slashes to the end of the name + 1. Erase the name + + The actions and effects are as follows: +figure.image-display + img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition") +:marked + We should be able to see the following four sets of class names and their transitions: +figure.image-display + img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions") + +:marked + The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a + strong visual signal when the data are invalid and we want to mark required fields. + + We realize 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 `styles.css` file + that we add to our project as a sibling to `index.html`. + ++makeExample('forms/js/styles.css',null,'styles.css')(format=".") +:marked + These styles select for the two Angular validity classes and the HTML 5 "required" attribute. + + We update the `` of the `index.html` to include this style sheet. ++makeExample('forms/js/index.html', 'styles', 'index.html (excerpt)')(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 `` tag with + 1. a [local template variable](./template-syntax.html#local-vars) + 1. the "*is required*" message in a nearby `
` which we'll display only if the control is invalid. + + Here's how we do it for the *name* input box: +-var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm }; ++makeExample('forms/js/app/hero-form.component.html', + 'name-with-error-msg', + 'app/hero-form.component.html (excerpt)', + stylePattern) +:marked + When we added the `ngControl` directive, we bound it to the the model's `name` property. + + Here we initialize a template local variable (`name`) with the value "ngForm" (`#name="ngForm"`). + Angular recognizes that syntax and re-sets the `name` local template variable to the + `ngControl` directive instance. + In other words, the `name` local template variable becomes a handle on the `ngControl` object + for this input box. + + Now we can control visibility of the "name" error message by binding the message `
` element's `hidden` property + to the `ngControl` object's `valid` property. The message is hidden while the control is valid; + the message is revealed when the control becomes invalid. + +.l-sub-section + :marked + ### The NgForm directive + We just set a template local variable with the value of an `NgForm` directive. + Why did that work? We didn't add the **[`NgForm`](../api/core/NgForm-class.html) directive** explicitly. + + Angular added it surreptiously, wrapping it around the `
` 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 also has its own `valid` property which is true only if every contained + control is valid. +:marked + The Hero *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 `