Saturday, September 24, 2016

Learning Angular 2: Populating Properties With the Constructor And Using Promise.all

Version 0.0.5 of my GuildRunner sandbox Angular 2 application was focused on updating the model object graph of the application (mainly to provide more opportunities for exploring forms), which included the following changes:

  • Removed the Address domain class and replaced it with a Location object containing traditional, basic address properties. 
  • Created a Person class containing properties such as first name and last name as well as a "residence" property that is an instance of Location.
  • Added the concept of "Chapter", where each Guild has a number of geographically-based Chapters.  Each Chapter domain class is associated with a Guild and has a "location" property that is an instance of ChapterLocation, another new domain class that extends Location and contains properties like "floors" and "entryPoints".
  • Removed the Member domain class and replaced it with ChapterMember, which extends the Person class.
  • Simplified the Guild domain class.
  • Created new master list view for the Chapters and ChapterMembers and added them to the navigation bar.
During the process of refactoring the domain classes, I refined my approach to setting the domain class properties via the constructor.  Previously, I simply declared my properties (with a data type where appropriate) and used ternary operations to set the individual property values like so:

// domain/guild.ts
export class Guild {
  id: number;
  name: string;
  email: string;
  incorporationYear: number;

  constructor( guildData?: any  ) {
    if( guildData ) {
      this.id = guildData.id ? guildData.id : null ;
      this.name = guildData.name ? guildData.name : null ;
      this.email = guildData.email ? guildData.email : null;
      this.incorporationYear = guildData.incorporationYear ? guildData.incorporationYear : null;
    }
  }

It worked, but it would get tedious and hard to read with larger property sets. And having default values set by conditional logic isn't very "default-like" behavior. Compare that to the constructor method I wrote for the new Chapter domain class:


// domain/chapter.ts
import { ChapterLocation } from './chapter-location';

export class Chapter {
  id: number = null;
  guildId: number = null;
  name: string = null;
  location: ChapterLocation = new ChapterLocation();
  headChapter: boolean = false;
  founded: Date = null;
  defenses: Number[] = [];

  constructor( chapterData?: any ) {
    if( chapterData ) {
      let props = Object.keys( this );
      for( let p in props ) {
        if( chapterData[ props[p] ] ) {
          if( props[p] == 'location' ) {
            this.location = new ChapterLocation( chapterData.location )
          } else {
            this[ props[p] ] = chapterData[ props[p] ];
          }
        }
      }
    }
  }

}

It's worth noting that the technique of looping through the properties via Object.keys() only works if we have default values set for each property: properties without values are considered undefined and aren't retrieved by Object.keys().

I also realized that my new master lists of guild chapters and chapter members would look more realistic if they included related data, so I could for example denote the guild and chapter each member belonged to.

Under real-world conditions, a REST request for chapter members would probably include the related guild and chapter data in the returned data. But simulating that kind of all-inclusive data set with the in-memory web API is a bit problematic, because any changes I made to the Guild or Chapter data via the API wouldn't be reflected in the ChapterMember data set. So to avoid that problem, I needed to retrieve the full list of guilds and chapters along with the full list of members.

In Angular 1x, when you needed to collect data returned by multiple promises before proceeding, you could use $q.all() to combine all of the promise results into an array that would become available only after all of the promises returned successfully.  I was able to do the same thing with Promise.all():


// members-master/members-master-component.ts
export class MembersMasterComponent implements OnInit {

  members: ChapterMember[] = [];
  chapters: any = {};
  guilds: any = {};

  constructor(
    private memberService: MemberService,
    private chapterService: ChapterService,
    private guildService: GuildService
  ) { }

  ngOnInit() {

    Promise.all( [ //the array of service calls that return Promises
      this.memberService.getMembers(),
      this.chapterService.getChapters(),
      this.guildService.getGuilds()
    ]).then( (results:Promise[]) => {
      results[0]['data'].forEach( memberData => {
        this.members.push( new ChapterMember( memberData ) )
      });
      results[1]['data'].forEach( chapterData => {
        this.chapters[ chapterData.id ] = chapterData
      });
      results[2]['data'].forEach( guildData => {
        this.guilds[ guildData.id ] = guildData
      });
    });

  }

}

Each service method call returns an instance of my HttpResponse class where the "data" property is populated with the array of member, chapter, and guild data returned by the in-memory web API, and the code loops over each array. Note that the code doesn't access the "data" property via dot-notation: when I tried using dot-notation ("results[0].data") I got a compiler error stating that "data" was not a property of the object. Not sure why: I probably have it coded in such a fashion that TypeScript doesn't recognize the results item as an HttpResponse despite the data typing.

Note that only the member data gets translated into instantiated domain class objects (ChapterMember objects): for the chapters and the guilds, I simply need to capture them such that they can be referenced in the component view:


<-- members-master/members-master.component.html -->
<h3>Chapter Members</h3>
    <table class="table table-bordered table-striped">
      <thead>
      <tr>
        <th>ID</th>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Guild</th>
        <th>Chapter</th>
        <th>Active?</th>
      </tr>
      </thead>
      <tbody>
      <tr *ngFor="let member of members">
        <td>{{member.id}}</td>
        <td>{{member.firstName}}</td>
        <td>{{member.lastName}}</td>
        <td>{{guilds[chapters[member.chapterId].guildId].name}}</td>
        <td>{{chapters[member.chapterId].name}}</td>
        <td>{{member.isActive ? 'Yes' : 'No'}}</td>
      </tr>
      </tbody>
    </table>

Again, this is not the ideal way of gathering related data for a master display of records, but in this case it gets the job done.

Monday, September 5, 2016

Learning Angular 2: Experiment in Validating a Template-Driven Forms

Version 0.0.4 of my GuildRunner sandbox Angular 2 application is now available.

In an earlier post, I shared my thoughts after working through the examples on the official documentation page for template-driven forms.  I came away underwhelmed with the features designed to help with validating the form input:

  • The classes Angular adds to form controls to indicate the control state are based on interactions with the DOM and don't reflect the state of the model data attached to the control (for example, once a form control is marked as "dirty", changing the control value back to its original value does not re-mark it as "clean").

  • Some basic form control attributes (like "required") could be used to set validation constraints that Angular would use to toggle the valid/invalid class on the control, but not the validation attributes introduced in HTML5, and there was no documentation about what worked and what didn't.

  • Even when Angular could toggle the valid/invalid class correctly, that only indicated that the form control should be considered invalid, not why it was invalid.

(The change log for the recently released RC6 version of Angular 2 hints that some of these issues may have been addressed.)

So when I decided that the next feature of GuildRunner would be a detail component for adding or editing a guild, I knew figuring out my strategy for validating the form would be a big part of the work involved.  What I came up for this release is a rough, first draft of an approach where the form takes its validation cues from validation logic and state data stored in the component.  There is a lot of room for improvement should I decide that this approach is viable and a reasonable alternative to the other method of generating and managing forms in Angular 2 (dynamic forms).

I started by making a few changes to my guild data and my Guild domain class.  I added two new properties:  "email" and "incorporationYear" (the year the guild was incorporated).  I also refactored the Guild constructor to make the object literal argument and all of properties optional:

//domain/guild.ts
...
constructor( guildData?: any  ) {
    if( guildData ) {
      this.id = guildData.id ? guildData.id : null ;
      this.name = guildData.name ? guildData.name : null ;
      this.email = guildData.email ? guildData.email : null;
      this.incorporationYear = guildData.incorporationYear ? guildData.incorporationYear : null;
...
I then created the initial GuildsDetailComponent and defined two routes to reach it from the GuildsMasterComponent, one route for editing and one for adding:

//app.routing.ts
{ path: 'guilds/:id', component: GuildsDetailComponent}, //Edit route
{ path: 'guild', component: GuildsDetailComponent },  //Add route
...I thought about just having the one route with the parameter value, where a parameter value of 0 would be used to trigger the add behavior, but I wanted to avoid that if I could. Having a route that didn't provide a parameter meant I had to handle that in the ngOnInit method of GuildsDetailComponent:

//guilds-detail.component.ts
...
export class GuildsDetailComponent implements OnInit {

  guild: Guild;
  ...
  ngOnInit() {
      this.route.params.forEach( (params: Params ) => {
        let id = params['id'] ? +params['id'] : null ;  //converts param string to number
        if( id ) {
          //Ask the GuildService for a Guild object instantiated with data for that record
        } else {
          this.guild = new Guild();  //Instantiate a "blank slate" Guild object
        }
...
I created a bare-bone GuildService method to provide GuildsDetailComponent with a populated Guild object for the selected guild, then started working on the form and the form logic. I tried a few different approaches before settling on the format in the current release.

The component HTML starts off with a header block:

<!-- guilds-detail.component.html -->
<div class="row">
  <div class="col-md-12">
    <h6 *ngIf="!guild && !serviceErrors">Loading...</h6>
    <h3 *ngIf="guild">{{(guild.id ? 'Edit' : 'Add' )}} {{(guild.name ? guild.name : 'Guild')}}</h3>
  </div>
</div>
...
The <h3> interpolation logic makes sure we end up with an appropriate title based on the situation, and the ngIf directives make sure we display the appropriate content under the appropriate conditions.

The form is styled with Bootstrap and contains text inputs for the name, incorporationYear, and email address for the guild.  Except for the attributes that are specific to the guild data bound to each control, the HTML is essentially the same:

<!-- guilds-detail.component.html -->
...
<div class="form-group">
    <label for="name" class="col-md-2 control-label">Name:</label>
    <div class="col-md-6">
      <input id="name" type="text" class="form-control" [(ngModel)]="guild.name" (change)="checkValidity( 'name' )" (keyup)="checkFix( 'name' )" name="name" #name="ngModel" />
      <div *ngIf="status.name.errors.length" class="alert alert-danger">
        <ul>
          <li *ngFor="let error of status.name.errors">
            {{error}}
          </li>
        </ul>
      </div>
    </div>
</div>
So [(ngModel)] binds this control to the name property of the Guild object of the component, and checkValidity() and checkFix() are executed in response to the change and keyup events emitted by the control. Any errors regarding this input are managed via an array of errors attached to the "name" property of a simple "status" object literal defined in the component.

All of the validation work occurs within the checkValidity() method of the component:

//guilds-detail.component.ts
...
checkValidity( propertyName: string ) {

      let yearRegEx = /[1-2]\d{3}$/;
      let emailRegEx = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$/;

      switch( propertyName ) {
        case 'name':
          this.status.name.errors = [];
          if( !this.guild[ propertyName ] ) {
            this.status.name.errors.push( 'The name field is required.' );
            break;
          }
          if( this.guild[ propertyName ].length < 5 ) {
            this.status.name.errors.push( 'The guild name is too short.' );
          }
          break;
        case 'incorporationYear':
          this.status.incorporationYear.errors = [];
          if( !this.guild[ propertyName ] ) {
            this.status.incorporationYear.errors.push( 'The year of incorporation is required.' );
            break;
          }
          if( isNaN( +this.guild[ propertyName ] ) ) {
            this.status.incorporationYear.errors.push( 'The incorporation year must be a number.' );
          } else {
            if( !yearRegEx.test( this.guild[ propertyName ] ) ) {
              this.status.incorporationYear.errors.push( 'The incorporation year must a valid 4-digit year (1xxx or 2xxx).' );
            }
          }
          break;
        case 'email':
          this.status.email.errors = [];
          if( !emailRegEx.test( this.guild[ propertyName ] ) ) {
            this.status.email.errors.push( 'Please enter a valid email address.' );
          }
          break;
      }

}
So every time one of the text inputs emits a change event, checkValidity() clears the existing validation error array for that control/guild property and executes the relevant validation logic, re-populating that error array with any validation issues that still exist.  And based on the component HTML, any such errors are displayed in an alert area beneath the text input.

The change event for the text inputs doesn't fire until the form input loses focus, which is fine when the user is first inputing data into the form control because you don't want any minimum length validation rules (like the one for the guild name) applied before they finish typing.  But that does mean that if a validation error is displayed, it won't disappear until after the user has modified the input and then exited the input again.  That's not a terrible experience, but I wanted to ensure the user doesn't leave the input again and still end up with an invalid value.  That's why the keyup event executes the checkFix() method in the component.

//guilds-detail.component.ts
...
checkFix( propertyName: string ) {
    if( this.status[ propertyName ].errors.length > 0 ) {
      this.checkValidity( propertyName );
    }
  }
The checkFix() method checks the validity with every keystroke until the validation issues have all been resolved. And that point, one would hope the user wouldn't enter more text in the input that would make the content and the control invalid again.

The rest of the form and form logic is pretty straightforward.  The form concludes with three buttons:
  • The "Cancel" button, which simply navigates back to the table list of guilds.
  • The "Save" button, which is disabled as long as there are any validation errors and when clicked executes checkValidity() for every property contained in the "status" object literal of the component (so for all of the form items).
  • The "Clear Form" button, which sets the name, incorporationYear, and email properties of the guild object to null and clears all of the previous validation errors, providing a clean slate for data entry.
The end result is a form that behaves like this:

In terms of the behavior, I'm pleased with the result, but the implementation could be better.  Most if not all of the validation configuration should belong in the Guild object rather than the component, the repetition of the template code for each form control suggests I could probably create a custom component to encapsulate the shared behavior, and I need to try applying the same technique to more complex forms to see if the implementation holds up under different scenarios.

Other notes regarding this release:
  • I created an HttpResponse domain class to use to pass data back from the service methods to the component methods that called them.  I did that in order to create consistency in how the response data from HTTP calls was packaged and presented to the calling methods, whether the HTTP call returned successfully or returned with a HTTP error code.

  • I wanted to account for the scenario where a user bookmarked the application URL that would pull up a particular Guild in the GuildsDetailComponent, but that guild no longer existed in the data.  So if the GuildService getGuild() method is executed in that scenario, the 404 error response will populate the HttpResponse object returned to the GuildsDetailComponent with an error message that no guild matching that id number exists, and that will end up being displayed instead of the form.