Validations

When we created our FormControls, we provided a parameter - Validators.required, along with the value of the control. This is one of the built-in Angular validators - Validators.required.

Angular allow us to pass in custom validators in order to validate data in our forms. There are two types of validation functions, we can pass for each formcontrol, a synchronous (for synchronous client-side validations) validator and an asynchronous (if we need to make server calls to validate data) one. Angular sets properties (such as value, status, pristine & untouched) on the FormControl & FormGroup objects based on it’s validity and input status in the browser. It also sets appropriate css classes like ng-valid, ng-invalid, ng-pristine, ng-touched to the form elements so the appearance of the page can easily be customized based on the css.

For now, the built-in required validator will suffice for us. We’ll see how we use this for validating the input and provide visual clues to the user using the FormControl properties. Let’s change the questionText input -

<md-input-container class="full-width required">

<input md-input placeholder="Question" formControlName="questionText" class="required">

<md-hint align="end" *ngIf="questionForm.controls.questionText.errors && !questionForm.controls.questionText.pristine" class="error">Question is required!</md-hint>

</md-input-container>

and the scss (note the use of /deep/ to force a style down through the md components.) -

/deep/md-input-container.ng-invalid.ng-touched .md-input-underline {

border-color: red;

}

/deep/md-hint.error {

color: red;

}

/deep/ is a shadow dom scoping selector and not a part of standard scss. Your IDE may not recognize /deep/ and flag it as an error, but the cli will compile it correctly and your app should work just fine. More about deep and other Angular selectors - https://angular.io/docs/ts/latest/guide/component-styles.html#!#sts=%2Fdeep%2F

We can now add similar validations to the answer inputs too.

Tags

For tags, we’ll auto-generate a list of tags based on the text inside questions and answers, that match-up the Tag list in the database.

Let’s fetch the list of tags in the component first (similar to Tag component).

We’ll create a function to extract the tags from the question and all the answer fields -

//question-add-update.component.ts autoTags: string[] = [];

tags: string[]; computeAutoTags() {

let allTextValues: string[] = [this.questionForm.value.questionText];

this.questionForm.value.answers.forEach(answer => allTextValues.push(answer.answerText)); let wordString: string = allTextValues.join(" "); let matchingTags: string[] = [];

this.tags.forEach(tag => {

let patt = new RegExp('\\b(' + tag.replace("+", "\\+") + ')\\b', "ig");

if (wordString.match(patt))

matchingTags.push(tag);

});

this.autoTags = matchingTags;

}

We must invoke this function, when either the question or any of the answers change. With reactive forms, this is extremely easy to achieve using the FormControl’s valueChanges observable. We use the debounceTime operator to wait for 500ms before recomputing the autotags -

ngOnInit() {

...

let questionControl = this.questionForm.get('questionText'); questionControl.valueChanges.debounceTime(500).subscribe(v => this.computeAutoTags());

this.answers.valueChanges.debounceTime(500).subscribe(v => this.computeAutoTags());

...

}

And change the template to show the tags -

<md-chip-list>

<md-chip *ngFor="let tag of autoTags">

{{tag}}

</md-chip>

</md-chip-list>

Now that we have autotags in place, let’s add the ability for user to manually add/remove user-entered tags.

Add the button to the html -

<button md-button color="primary" [disabled]="questionForm.controls.tags.value==''" (click)="addTag()">ADD</button>

And addTag and removeEnteredTag functions to the component -

enteredTags: string[] = [];

addTag() {

let tag = this.questionForm.get('tags').value;

if (tag) {

if (this.enteredTags.indexOf(tag) < 0)

this.enteredTags.push(tag);

this.questionForm.get('tags').setValue('');

}

}

removeEnteredTag(tag) {

this.enteredTags = this.enteredTags.filter(t => t !== tag);

}

And let’s display these entered tags as chips -

<md-chip-list>

<md-chip *ngFor="let tag of autoTags">

{{tag}}

</md-chip>

<md-chip *ngFor="let tag of enteredTags">

{{tag}} <span (click)="removeEnteredTag(tag)" class="remove-tag">x</span>

</md-chip>

</md-chip-list>

Cool! We’ve come a long way. Most of the functionality is in. We still need some tag validations to eliminate duplicates between auto tags and entered tags, and do some comparison ignoring the case of the tags, but we’ll save that for another day and do it as a self exercise.

Save

The last part of our Add form is to Save the data we entered. Let’s first add a new method in our question service to save a question to our db -

//question.service.ts



saveQuestion(question: Question): Observable<Question> {

let url = this._serviceUrl; return this.http.post(url, question)

.map(res => res.json());

}

And then add the save method to our component. Once saved we’ll redirect to the questions page. (Note that you would need to add the appropriate imports and inject the router and questionService in your component) -

//question-add-update.component.ts saveQuestion(question: Question) {

this.questionService.saveQuestion(question).subscribe(response => {

this.router.navigate(['/questions']);

});

}

We need this save method to be called from the form submit event -

//question-add-update.component.ts onSubmit() {

//validations

if (this.questionForm.invalid)

return; //get question object from the forms

let question: Question = this.getQuestionFromFormValue(this.questionForm.value); //call saveQuestion

this.saveQuestion(question);

}



getQuestionFromFormValue(formValue: any): Question {

let question: Question; question = new Question();

question.questionText = formValue.questionText;

question.answers = formValue.answers;

question.categoryIds = [formValue.category];

question.tags = [...this.autoTags, ...this.enteredTags] return question;

}

And the submit event on our form -

<!-- question-add-update.component.html --> <form (ngSubmit)="onSubmit()" class="question-form" [formGroup]="questionForm" novalidate>

Let’s try this out to ensure our save works! (interim commit link)