Angular2 – REST sample app, part 2 create update

I continue to learn angular 2. This article is second part of previous post. I continue to create REST APP. And today we will work with update and create operations.

mongodb-crud-operations1

There are a lot of code so I’ll explain only the most interesting parts. All code examples are available here.

My original idea was very common. I wanted to create shared form for both update and create actions. I spent some time learning angular forms and trying to find solution that will satisfy me. Here is what I got eventually.


// https://github.com/radzserg/angular2-rest/blob/5b76d62d19b2474277813abe0eebb89ae5f1a6f2/src/components/users/update/user-update.ts
import {Component, OnInit} from 'angular2/core';
import {User} from '../../../models/user';
import {UserFormComponent} from '../../../forms/user-form/user-form';
import {RouteParams} from 'angular2/router';
import {Http} from 'angular2/http';


@Component({
  selector: 'user-create',
  templateUrl: './user-update.html',
  //styleUrls: ['./app.css'],
  moduleId: module.id,
  directives: [UserFormComponent]
})
export class UserUpdate implements OnInit {

  user: User;
  id: Number;

  constructor(private params: RouteParams, private http: Http) {
    this.id = parseInt(params.get('id'));
  }

  ngOnInit() {

    this.http.get('/users/' + this.id)
      .map(res => res.json())
      .subscribe(
        (userData) => {
          this.user = new User(userData);
          console.log(this.user);
        }
      );
  }

}


// https://github.com/radzserg/angular2-rest/blob/5b76d62d19b2474277813abe0eebb89ae5f1a6f2/src/forms/user-form/user-form.ts
import {Component, Input} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {Router} from 'angular2/router';
import {ControlGroup, FormBuilder, Validators, NgClass, Control} from 'angular2/common';
import {User} from '../../models/user';
import {AppValidators} from '../../validators';
import {ControlGroupHelper} from '../ControlGroupHelper';
import {FieldErrors} from '../../pipes/FieldErrors';


@Component({
  selector: 'user-form',
  moduleId: module.id,
  styleUrls: ['./user-form.css'],
  templateUrl: './user-form.html',
  pipes: [FieldErrors],
  directives: [NgClass]
})
export class UserFormComponent {

  user: User = new User();
  userForm: ControlGroup;

  constructor(protected http: Http, protected router: Router, builder:FormBuilder) {
    this.userForm = builder.group({
      first_name: ['', Validators.compose([Validators.required, Validators.minLength(2)])],
      last_name: ['', Validators.compose([Validators.required, Validators.minLength(2)])],
      email: ['', Validators.compose([Validators.required, AppValidators.email])],
      password: ['', Validators.compose([Validators.minLength(6)])]
    });
  }


  /**
   * Handle errors
   * @param response
   */
  handleError(response: Response) {
    if (response.status === 422) {
      let errors : Object = response.json();
      console.log(errors);
      for (var field in errors) {
        var fieldErrors: string[] = (<any>errors)[field];
        ControlGroupHelper.setControlErrors(this.userForm, field, fieldErrors);
      }
    }

    console.log(response);
  }

  @Input()
  set model (user: User) {
    if (user) {
      this.user = user;
      ControlGroupHelper.updateControls(this.userForm, this.user);
      console.log( (<Control>this.userForm.controls['first_name']).errors);
    }
  }

  onSubmit() {
    if (!this.userForm.valid) {
      return ;
    }

    this.user.attributes = this.userForm.value;

    if (this.user.id) {
      this.http.put('/users/' + this.user.id, JSON.stringify({user: this.user}))
        .map(res => res.json())
        .subscribe(
          (data) => {
            this.router.navigate(['UserList']);
          },
          (response: Response) => {
            this.handleError(response);
          }
        );
    } else {
      this.http.post('/users', JSON.stringify({user: this.user}))
        .map(res => res.json())
        .subscribe(
          (data) => {
            this.user.id = data.id;
            this.router.navigate(['UserList']);
          },
          (response: Response) => {
            this.handleError(response);
          }
        );
    }
  }

}

<!-- https://github.com/radzserg/angular2-rest/blob/5b76d62d19b2474277813abe0eebb89ae5f1a6f2/src/components/users/update/user-update.html --> 


<h1>Update User</h1>
<user-form [model]="user"></user-form>

<!-- https://github.com/radzserg/angular2-rest/blob/5b76d62d19b2474277813abe0eebb89ae5f1a6f2/src/forms/user-form/user-form.html --> 

<form [ngFormModel]="userForm" novalidate (ngSubmit)="onSubmit()" class="well">

<div class="form-group" [ngClass]="{'has-danger': !userForm.controls.first_name.valid}" >
    <label class="form-control-label" for="first_name_field">First Name</label>
    <input type="text" class="form-control" placeholder="First Name" id="first_name_field" ngControl='first_name' >

    <span class="text-help" *ngIf="!userForm.controls.first_name.valid" *ngFor="#e of userForm.controls.first_name.errors | fieldErrors: 'First Name'" >
      {{e}}
    </span>
  </div>

<div class="form-group" [ngClass]="{'has-danger': !userForm.controls.last_name.valid}" >
    <label class="form-control-label" for="last_name_field">Last Name</label>
    <input type="text" class="form-control" placeholder="Last Name" id="last_name_field" ngControl="last_name" >

    <span class="text-help" *ngIf="!userForm.controls.last_name.valid" *ngFor="#e of userForm.controls.last_name.errors | fieldErrors: 'Last Name'" >
      {{e}}
    </span>
  </div>

<div class="form-group" [ngClass]="{'has-danger': !userForm.controls.email.valid}" >
    <label class="form-control-label" for="email_field">Email</label>
    <input type="email" class="form-control" placeholder="Email" id="email_field" ngControl="email" >

    <span class="text-help" *ngIf="!userForm.controls.email.valid" *ngFor="#e of userForm.controls.email.errors | fieldErrors: 'Email'" >
      {{e}}
    </span>
  </div>

<div class="form-group" [ngClass]="{'has-danger': !userForm.controls.password.valid}" >
    <label class="form-control-label" for="password_field">Password</label>
    <input type="password" class="form-control" placeholder="Password" id="password_field" ngControl="password" >

    <span class="text-help" *ngIf="!userForm.controls.password.valid" *ngFor="#e of userForm.controls.password.errors | fieldErrors: 'Password'" >
      {{e}}
    </span>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

And now let’s make a step back. And will try to understand why I got such variant.

If you start google ‘angular2 form’  you’ll find angular form guide. And first think that you will know will be ngModel – you create some input tag and bind your model.

        <input type="text" class="form-control" [(ngModel)]="model.name" >

That works until you don’t need validation. Then angular guide propose using of ngControl directive like this


<div class="form-group">
    <label for="name">Name</label>
    <input type="text" class="form-control" required [(ngModel)]="model.name" ngControl="name" #name="ngForm" >

    <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
          Name is required
    </div>
</div>

Ok here we added ngControl=name. And now we have local variable name that has NgContol functionality. So far, so good. You also can check that input is set via required attribute. And you can see an example how to show error alert. Unfortunately in the real world I need something more than required validation. I’d also want to check email format, check if some flag it set (You accept bla bla field), or check password and password confirmation fields – I need more options for validation.

Ok then angular has some extra tool – Form Builder. It allows you to create form in component, like this:

this.loginForm = builder.group({
      login: ["", Validators.required],
      passwordRetry: builder.group({
        password: ["", Validators.required],
        passwordConfirmation: ["", Validators.required, asyncValidator]
      })
    });

As you can see we can specify validators for each control. And we also can use our own validators. Check out my angular2 email validator. That’s what I need. Ok next question – how to make your model interact with you form? I tried a few solutions. Originally I wanted to use  model and group control inside one input, but I got some issue with init and I also didn’t like idea that we have some kind of concurrency. I decided to use only form inside template and implement interaction between model and form inside component.

// update form with model 
(<Control>this.userForm.controls['first_name']).updateValue(user.first_name, true);

// update model with form values. Check prev part to understand how user.attributes = work
this.user.attributes = this.userForm.value;

I made simple helper class for this. Ok now we get data for our model from server, init User class in the client side. And pass data from model to form. Form is responsible for validation. When data is updated and it’s valid we pass it back to model and make update request on server side.

The other think that I’d like to explain – how I handle errors from server side. We know how to add custom validators on client side. But still we can get errors from server side. And the key point here is error format.

// NgControl uses following format.
{required: true}
// server side format depends on your framework. I used rails so I had following format
{
   'fieldName': ['error message1', 'error message2']
}

So I wanted to mix both these formats. And I did the following trick

   handleError(response: Response) {
    if (response.status === 422) {
      let errors : Object = response.json();
      console.log(errors);
      for (var field in errors) {
        var fieldErrors: string[] = (<any>errors)[field]; 
        // pass field errors from server side to NgControl
        ControlGroupHelper.setControlErrors(this.userForm, field, fieldErrors);
      }
    }

    console.log(response);
  }
    <span class="text-help" *ngIf="!userForm.controls.first_name.valid" *ngFor="#e of userForm.controls.first_name.errors | fieldErrors: 'First Name'" >
      {{e}}
    </span>
// pipe that will mix both format 
import {Pipe} from 'angular2/core';

@Pipe({
  name: 'fieldErrors'
})
export class FieldErrors {
  transform(dict: Object, fieldName?: string): string[] {
    var a: string[] = [];
    for (var key in dict) {
      if (dict.hasOwnProperty(key)) {
        var index = <any>key;
        if (!isNaN(index)) { // is numeric
          a.push((<any>dict)[key]);
        } else {
          a.push(this.translateKeyToMessage(key, fieldName)); // angular2 error {required: true}
        }
      }
    }
    return a;
  }


  private translateKeyToMessage(key: string, fieldName?: string) : string {
    switch (key) {
      case 'required':
        return fieldName + ' is required field';
      case 'minlength':
        return fieldName + ' is too short';
      case 'maxlength':
        return fieldName + ' is too long';
      default:
        console.log(key);
        return '';
    }
  }

}

I think that’s it for today as other code parts are pretty simple I guess. If you have any questions or suggestions – please comment. Any feedback are kindly welcome.

Leave a Reply

Your email address will not be published. Required fields are marked *