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.
//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;
...
//app.routing.ts
{ path: 'guilds/:id', component: GuildsDetailComponent}, //Edit route
{ path: 'guild', component: GuildsDetailComponent }, //Add route
//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
}
...
<!-- 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>
...
<!-- 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>
//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;
}
}
//guilds-detail.component.ts
...
checkFix( propertyName: string ) {
if( this.status[ propertyName ].errors.length > 0 ) {
this.checkValidity( propertyName );
}
}
- 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.
-
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.
No comments:
Post a Comment