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.

No comments:

Post a Comment