Reusing Markup with a Generic Text Field Component

You’ve already seen how to reuse markup when we created the AddressComponent in Chapter 11, Asynchronously Load Data from Many Sources. Our TextFieldComponent is even more generic, so we will need to have a few more properties that the user has to set. To make this generic, we need to get a few bits of information from the user:

Let’s see how those values would be used in our markup. A completely generic version of our text field would now look like this:

12_angular-forms/10-text-component/shine/app/javascript/TextFieldComponent/template.html
 <div bind-class=​"( model.invalid && model.dirty ) ?
  'form-group has-warning' :
  'form-group'"​>
  <label class=​"sr-only"​ attr​.​for=​"{{field_name}}"​>
  {{label}}
  </label>
  <div bind-class=​"addon ? 'input-group' : ''"​>
  <div ​*​ngIf=​"addon"​ class=​"input-group-addon"​>{{addon}}</div>
  <input type=​"text"​ class=​"form-control"​ attr​.​name=​"{{field_name}}"
  required
  bindon-ngModel=​"object[field_name]"
  ref-model=​"ngModel"
  >
  </div>
  <aside ​*​ngIf=​"model.invalid && model.dirty"
  class=​"alert alert-danger"​>
  <small>This is required</small>
  </aside>
 </div>

Note that we’ve put this in app/javascript/TextFieldComponent, and not in app/javascript/CustomerDetailsComponent, because this component is intended to be totally generic. There are a few notable differences between this and the markup we saw in the previous section. First, we had to handle the addon. This means that if addon is true, our markup works properly and renders like so:

 <div class=​"input-group"​>
  <div class=​"input-group-addon"​>@</div>
 
 <!-- form control markup -->
 
 </div>

Without it, the class input-group on the outermost div is omitted, leaving us with an empty div (turns out, leaving it there causes odd rendering issues when we aren’t using an addon):

 <div>
 
 <!-- form control markup -->
 
 </div>

Second, we’re using code like attr.for="{{field_name}}". You might think we should be doing for="{{field_name}}", but this won’t work. You’ll get an error like “Can’t bind to ‘for’ since it isn’t a known property of label.” This is because Angular is trying to set the property for on its internal representation of a label, and it has no for property. We really want to set the underlying label element’s for attribute. It’s a subtle difference.

To do that, prefix the property with attr., which tells Angular that we want to set the for attribute of the underlying HTML element.

The final odd bit is that we’re using bindon-ngModel="object[field_name]". Because the other parts of the markup just use field_name directly (for example, attr.name="field_name"), this sticks out as strange.

If you just bind to the field we’re managing using bindon-ngModel="field_name", the changes won’t be seen on the customer object containing the value. It’s not clear to me why this is, but our overall feature definitely won’t work if you bind directly to the field, So, bind to object[field_name], which would be like binding to customer.first_name.

With this markup created, our component’s JavaScript is fairly bare-bones:

12_angular-forms/10-text-component/shine/app/javascript/TextFieldComponent/index.ts
 import​ { Component } ​from​ ​"@angular/core"​;
 import​ template ​from​ ​"./template.html"​;
 
 var​ TextFieldComponent = Component({
  selector: ​"shine-text-field"​,
  template: template,
  inputs: [
 "object"​,
 "field_name"​,
 "label"​,
 "addon"
  ]
 }).Class({
  constructor: [
 function​() {
 this​.object = ​null​;
 this​.field_name = ​null​;
 this​.label = ​null​;
 this​.addon = ​null​;
  }
  ]
 });
 
 export​ { TextFieldComponent };

Finally, we need to tell our NgModule in app/javascript/packs/customers.js about our new component:

12_angular-forms/10-text-component/shine/app/javascript/packs/customers.js
 import​ ​"polyfills"​;
 
 import​ { Component, NgModule } from ​"@angular/core"​;
 import​ { BrowserModule } from ​"@angular/platform-browser"​;
 import​ { FormsModule } from ​"@angular/forms"​;
 import​ { platformBrowserDynamic } from ​"@angular/platform-browser-dynamic"​;
 import​ { HttpModule } from ​"@angular/http"​;
 import​ { RouterModule } from ​"@angular/router"​;
 
 import​ { CustomerSearchComponent } from ​"CustomerSearchComponent"​;
 import​ { CustomerDetailsComponent } from ​"CustomerDetailsComponent"​;
 import​ { CustomerInfoComponent } from
 "CustomerDetailsComponent/CustomerInfoComponent"​;
 import​ { AddressComponent } from
 "CustomerDetailsComponent/AddressComponent"​;
 import​ { CreditCardComponent } from
 "CustomerDetailsComponent/CreditCardComponent"​;
»import​ { TextFieldComponent } from ​"TextFieldComponent"​;
 
 // code as before...
 
 var​ CustomerAppModule = NgModule({
  imports: [
  BrowserModule,
  FormsModule,
  HttpModule,
  routing
  ],
  declarations: [
  CustomerSearchComponent,
  CustomerDetailsComponent,
  CustomerInfoComponent,
  AddressComponent,
  CreditCardComponent,
» TextFieldComponent,
  AppComponent
  ],
  bootstrap: [ AppComponent ]
 })
 .Class({
  constructor: ​function​() {}
 });
 
 platformBrowserDynamic().bootstrapModule(CustomerAppModule);

This allows you to replace the markup in our components with a reference to shine-text-field, like so:

 <shine-text-field
  bind-object=​"customer"
  field_name=​"first_name"
  label=​"First Name"​>
 </shine-text-field>

You can now replace all references to text fields with markup like this. Although it’s overall less repetition, it’s still repetitive, so we’ll omit the entire listing here to save space.

Once you do this replacement, you should see that all of our fields now require values. While you still need to get to actually saving the data back to the server, you’ll notice a few problems with the address components. If you omit the state and set the zip code to an invalid value like “abcd,” the problems become apparent:

images/12_angular-forms/10-text-component/address-validation-screwy.png

The state field is too small for the error message, and we didn’t add validation that the zip code field is a valid U.S. zip code. We really should fix this before we get to saving the data.