diff --git a/MovieHunter/app/movies/movie-edit.component.html b/MovieHunter/app/movies/movie-edit.component.html index 0a139b9..5a78ec8 100644 --- a/MovieHunter/app/movies/movie-edit.component.html +++ b/MovieHunter/app/movies/movie-edit.component.html @@ -1,68 +1,74 @@ + +
- {{pageTitle}} + {{pageTitle$ | async}}
-
+
-
+
- - - {{formError.title}} - + +
- -
+ + +
- - - {{formError.director}} - + + +
- -
+ +
- - - {{formError.starRating}} - + + +
- -
+ +
- - - {{ formError.description}} - + +
diff --git a/MovieHunter/app/movies/movie-edit.component.ts b/MovieHunter/app/movies/movie-edit.component.ts index f1c02bb..1341d92 100644 --- a/MovieHunter/app/movies/movie-edit.component.ts +++ b/MovieHunter/app/movies/movie-edit.component.ts @@ -1,121 +1,130 @@ -import { Component } from '@angular/core'; -import { FormBuilder, ControlGroup, Control, Validators } from '@angular/common'; -import { ROUTER_DIRECTIVES, OnActivate, RouteSegment } from '@angular/router'; +import {Component, Input} from '@angular/core'; +import {FormBuilder, ControlGroup, Control, Validators, AbstractControl} from '@angular/common'; +import {ROUTER_DIRECTIVES, OnActivate, RouteSegment} from '@angular/router'; +import {IMovie} from './movie'; +import {MovieService} from './movie.service'; +import {RangeValidator} from '../shared/number.validator.directive'; +import {Observable} from 'rxjs/Rx'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/from'; +import {NumberValidator} from '../shared/number.validator'; -import { IMovie } from './movie'; -import { MovieService } from './movie.service'; -import { NumberValidator } from '../shared/number.validator'; +@Component({ + selector: 'help-block', + template: ` + {{message}} + ` +}) +export class HelpBlock { + @Input() message:string; +} @Component({ templateUrl: 'app/movies/movie-edit.component.html', - directives: [ROUTER_DIRECTIVES] + directives: [ROUTER_DIRECTIVES, RangeValidator, HelpBlock] }) export class MovieEditComponent implements OnActivate { - pageTitle: string = 'Edit Movie'; - editForm: ControlGroup; - titleControl: Control; - formError: { [id: string]: string }; - private _validationMessages: { [id: string]: { [id: string]: string } }; - movie: IMovie; - errorMessage: string; - - constructor(private _fb: FormBuilder, - private _movieService: MovieService) { - - // Initialization of strings - this.formError = { - 'title': '', - 'director': '', - 'starRating': '', - 'description': '' - }; - - this._validationMessages = { - 'title': { - 'required': 'Movie title is required', - 'minlength': 'Movie title must be at least three characters.', - 'maxlength': 'Movie title cannot exceed 50 characters.' - }, - 'director': { - 'required': 'Director is required', - 'minlength': 'Director must be at least 5 characters.', - 'maxlength': 'Director cannot exceed 50 characters.' - }, - 'starRating': { - 'range': 'Rate the movie between 1 (lowest) and 5 (highest).' - } - }; + editForm:ControlGroup; + movie$:Observable; + pageTitle$:Observable; + + titleMessage$:Observable; + directorMessage$:Observable; + starRatingMessage$:Observable; + descriptionMessage$:Observable; + + constructor(private _fb:FormBuilder, + private _movieService:MovieService) { } - routerOnActivate(curr: RouteSegment): void { + routerOnActivate(curr:RouteSegment):void { let id = +curr.getParam('id'); this.getMovie(id); } - getMovie(id: number) { - this._movieService.getMovie(id) + getMovie(id:number) { + this.movie$ = this._movieService.getMovie(id); + + this.pageTitle$ = this.movie$ + .map(({movieId, title}) => movieId + ? `Edit Movie ${title}` + : `Add Movie` + ); + + this.movie$ .subscribe( - movie => this.onMovieRetrieved(movie), - error => this.errorMessage = error); + movie => this.createForm(movie), + console.log.bind(console)); } - onMovieRetrieved(movie: IMovie) { - this.movie = movie; + // if 'INVALID', lookup and push out the validation message + // otherwise, push out an empty string + createMessageStream = (control:AbstractControl, messages:any)=> + control + .statusChanges + .switchMap((status:string) => + status === 'INVALID' + ? Observable.from( + Object.keys(control.errors) + .map(key => messages[key]) + ) + : Observable.of('') + ); - if (this.movie.movieId === 0) { - this.pageTitle = 'Add Movie'; - } else { - this.pageTitle = `Edit Movie: ${this.movie.title}`; - } + createForm(movie:IMovie) { + const {title, director, starRating, description} = movie; - this.titleControl = new Control(this.movie.title, Validators.compose([Validators.required, - Validators.minLength(3), - Validators.maxLength(50)])); - this.editForm = this._fb.group({ - 'title': this.titleControl, - 'director': [this.movie.director, - Validators.compose([Validators.required, - Validators.minLength(5), - Validators.maxLength(50)])], - 'starRating': [this.movie.starRating, - NumberValidator.range(1, 5)], - 'description': [this.movie.description] + const titleControl = new Control(title, Validators.compose([ + Validators.required, + Validators.minLength(3), + Validators.maxLength(50) + ])); + this.titleMessage$ = this.createMessageStream(titleControl, { + 'required': 'Movie title is required', + 'minlength': 'Movie title must be at least three characters.', + 'maxlength': 'Movie title cannot exceed 50 characters.' }); - this.editForm.valueChanges - .map(value => { - // Causes infinite loop - // this.titleControl.updateValue(value.title.toUpperCase()); - value.title = value.title.toUpperCase(); - return value; - }) - .subscribe(data => this.onValueChanged(data)); - // this.editForm.valueChanges - // .debounceTime(500) - // .subscribe(data => this.onValueChanged(data)); - } + const directorControl = new Control(director, Validators.compose([ + Validators.required, + Validators.minLength(3), + Validators.maxLength(50) + ])); + this.directorMessage$ = this.createMessageStream(directorControl, { + 'required': 'Director is required', + 'minlength': 'Director must be at least 5 characters.', + 'maxlength': 'Director cannot exceed 50 characters.' + }); - onValueChanged(data: any) { - for (let field in this.formError) { - if (this.formError.hasOwnProperty(field)) { - let hasError = this.editForm.controls[field].dirty && - !this.editForm.controls[field].valid; - this.formError[field] = ''; - if (hasError) { - for (let key in this.editForm.controls[field].errors) { - if (this.editForm.controls[field].errors.hasOwnProperty(key)) { - this.formError[field] += this._validationMessages[field][key] + ' '; - } - } - } - } - } + const descriptionControl = new Control(description, Validators.compose([ + Validators.required, + Validators.minLength(3) + ])); + this.descriptionMessage$ = this.createMessageStream(descriptionControl, { + 'required': 'Description is required', + 'minlength': 'Description must be at least 5 characters.' + }); + + const starRatingControl = new Control(starRating, Validators.compose([ + NumberValidator.range(1, 5), + ])); + this.starRatingMessage$ = this.createMessageStream(starRatingControl, { + 'min': 'Requires a rating greater than 0', + 'max': 'Requires a rating less than 5', + 'NaN': 'Requires a number', + }); + + this.editForm = this._fb.group({ + 'title': titleControl, + 'director': directorControl, + 'starRating': starRatingControl, + 'description': descriptionControl + }); } saveMovie() { if (this.editForm.dirty && this.editForm.valid) { - this.movie = this.editForm.value; - alert(`Movie: ${JSON.stringify(this.movie)}`); + alert(`Movie: ${JSON.stringify(this.editForm.value)}`); } } } diff --git a/MovieHunter/app/movies/movie.service.ts b/MovieHunter/app/movies/movie.service.ts index 8c8a100..1ba38a7 100644 --- a/MovieHunter/app/movies/movie.service.ts +++ b/MovieHunter/app/movies/movie.service.ts @@ -21,7 +21,8 @@ export class MovieService { return this._http.get(this._moviesUrl) .map(res => this.handleMap(res, id)) .do(data => console.log('Data: ' + JSON.stringify(data))) - .catch(this.handleError); + .catch(this.handleError) + .share(); } private handleError(error: Response) { diff --git a/MovieHunter/app/shared/number.validator.directive.ts b/MovieHunter/app/shared/number.validator.directive.ts new file mode 100644 index 0000000..679f0c1 --- /dev/null +++ b/MovieHunter/app/shared/number.validator.directive.ts @@ -0,0 +1,26 @@ +import {Directive, provide, Attribute, forwardRef} from '@angular/core'; +import {NG_VALIDATORS, Validator, AbstractControl} from '@angular/common'; +import {NumberValidator} from './number.validator'; + +export const RANGE_VALIDATOR = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RangeValidator), + multi: true +}; + +@Directive({ + selector: '[range][ngControl]', + providers: [RANGE_VALIDATOR] +}) +export class RangeValidator implements Validator { + private _validator:(c:AbstractControl)=> any; + + constructor(@Attribute('range') range:string) { + const [min, max] = range.split(',').map(v => parseInt(v)); + this._validator = NumberValidator.range(min, max); + } + + validate(c:AbstractControl):{} { + return this._validator(c) + } +} \ No newline at end of file diff --git a/MovieHunter/app/shared/number.validator.ts b/MovieHunter/app/shared/number.validator.ts index 407e414..9ba62cc 100644 --- a/MovieHunter/app/shared/number.validator.ts +++ b/MovieHunter/app/shared/number.validator.ts @@ -1,30 +1,24 @@ import { Control } from '@angular/common'; -// For simple validation with no parameters. -// Return type is a key/value pair -interface IValidationResult { - [key: string]: boolean; -} - -// For validation with parameters. -// Return type is a function that takes in a control -// and returns a key/value pair -interface IValidationFunction { - (c: Control): IValidationResult; -} export class NumberValidator { - static range(min: number, max: number): IValidationFunction { + static range(min:number, max:number){ return (control: Control): { [key: string]: boolean } => { - if (control.value && (isNaN(control.value) || control.value < min || control.value > max)) { - return { 'range': true }; + if (control.value < min ) { + return { 'min': true }; + } + else if(control.value > max){ + return { 'max' : true } + } + else if(isNaN(control.value)){ + return { 'NaN': true } } return null; }; } - static rangeHardCoded(control: Control): IValidationResult { + static rangeHardCoded(control: Control){ if (control.value && (isNaN(control.value) || control.value < 1 || control.value > 5)) { return { 'range': true }; }