Programming tutorials shows us a land of promise where everything happens as you think; as soon as you think. But real world doesn’t work that way most of the times. Here; you spend hours debugging some CORS error or thinking why your database table Id column is not auto-incrementing. For the last 2 days; I am participating in a coding interview which spans 2 days and these series of blog is based on that experience – what am I thinking at each stage; what is the issue and how I am resolving them. This is the third part of that.
Designing the rest of the Angular UI
What we left are adding a navigation on top of the page; adding the pages that will be responsible for the UI; and designing the pages in reactive form (Yes; we hate ourself).
Reactive Forms:
Pros:
- They provide more control over the form and its elements, as you can programmatically manipulate the form and its elements.
- They are more efficient as they only update the form and its elements when explicitly told to do so.
- They are better suited for handling complex forms with many fields and validation rules.
Cons:
- They can be more difficult to set up and may require more code to implement.
- They may not be as intuitive to work with for developers who are not familiar with the reactive programming paradigm.
Example:
this.form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
});
Template-Driven Forms:
Pros:
- They are easier to set up and may require less code to implement.
- They are more intuitive for developers who are not familiar with the reactive programming paradigm.
Cons:
- They provide less control over the form and its elements, as the data binding is handled automatically.
- They may not be as efficient as reactive forms, as they update the form and its elements on every change event.
- They are less suitable for handling complex forms with many fields and validation rules.
Example:
<form #form="ngForm" (ngSubmit)="submit(form)">
<input name="name" ngModel required>
<input name="email" ngModel required email>
<input name="password" ngModel required>
<button type="submit">Submit</button>
</form>
Why Reactive over Template-Driven
Reactive Forms provide more control over handling complex forms with many fields and validation rules as they allow you to programmatically manipulate the form and its elements. This allows you to create a form structure that is more modular and reusable, making it easier to manage and maintain.
For example, in a complex form with many fields and validation rules, you can create a custom form control component for each field and reuse it throughout the form. This allows you to keep the form code more organized and maintainable, making it easier to add, remove, or update fields and validation rules.
Reactive forms also offer a powerful way of handling validation. With reactive forms, you can define validation rules for each form control and even for the form itself. This allows you to centralize your validation logic and keep it organized. You can also use the built-in Angular validation directives such as required
, min
, max
, and pattern
and you can also create your custom validators to apply more complex validation rules.
For example, here is a sample of how to create a custom validator and use it with a form control:
import { FormControl } from '@angular/forms';
function validateEmail(c: FormControl) {
let EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
return EMAIL_REGEXP.test(c.value) ? null : {
validateEmail: {
valid: false
}
};
}
this.form = new FormGroup({
email: new FormControl('', [validateEmail])
});
One problem that we run into when we design user-list.ts like this –
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClientService } from 'src/app/http-client.service';
import { UserModel } from '../../../../models/user';
@Component({
selector: 'app-use-list',
templateUrl: './use-list.component.html',
styleUrls: ['./use-list.component.scss']
})
export class UseListComponent implements OnInit {
userList = UserModel[];
constructor(client: HttpClientService) {
client.getData("user").subscribe((res: UserModel[]) => {
console.log(res);
this.userList = res;
});
}
ngOnInit(): void {
}
}
Where we declare UserModel in a model file; we get the following error – Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'typeof UserModel'.
Most probably our use of [] after UserModel is causing the issue. Changing the code like below fixes the issue –
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClientService } from 'src/app/http-client.service';
import { UserModel } from '../../../../models/user';
@Component({
selector: 'app-use-list',
templateUrl: './use-list.component.html',
styleUrls: ['./use-list.component.scss']
})
export class UseListComponent implements OnInit {
userList: UserModel[] = [];
constructor(client: HttpClientService) {
client.getData("user").subscribe((res) => {
console.log(res);
this.userList = res as UserModel[];
});
}
ngOnInit(): void {
}
}
We have since changed the code and our user-create.html file looks like this –
<div class="container">
<form [formGroup]="userForm">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" formControlName="name">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" class="form-control" id="email" formControlName="email">
</div>
<div class="form-group">
<label for="mobile">Mobile</label>
<input type="tel" class="form-control" id="mobile" formControlName="mobile">
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="is_active" formControlName="isActive">
<label class="form-check-label" for="is_active">Is Active</label>
</div>
<button type="submit" class="btn btn-primary" (click)="save()">Save</button>
<!-- <button type="button" class="btn btn-secondary" (click)="cancel()">Cancel</button> -->
</form>
</div>
which is throwing an error – Can’t bind to ‘formGroup’ since it isn’t a known property of ‘form’.
When we add import { FormsModule, ReactiveFormsModule } from '@angular/forms';
in app.module.ts file and in imports list added – FormsModule,
ReactiveFormsModule
This issue is gone however.
The code unto this will be available in branch – version_1.
The Struggle Begins!
When we submit the form; it shows an error – An error occurred while saving the entity changes. See the inner exception for details.
& Cannot insert the value NULL into column 'id', table 'dev_test.dbo.user'; column does not allow nulls. INSERT fails.
Which is understandable! I mean; we haven’t given the object any id value.
But shouldn’t our db take care of that?
Looking into our db script; we can see that we have made id primary key; but we forgot to increment it; specifically forgot to add IDENTITY(1,1) NOT NULL
to our tables.
So we need to add some ways to drop and create tables and setting the identity to each id column.
And our database file looks like this –
IF OBJECT_ID(N'dbo.invitation', N'U') IS NOT NULL
DROP TABLE [dbo].invitation;
GO
IF OBJECT_ID(N'dbo.submission', N'U') IS NOT NULL
DROP TABLE [dbo].submission;
GO
IF OBJECT_ID(N'dbo.answer', N'U') IS NOT NULL
DROP TABLE [dbo].answer;
GO
IF OBJECT_ID(N'dbo.question', N'U') IS NOT NULL
DROP TABLE [dbo].question;
GO
IF OBJECT_ID(N'dbo.survey', N'U') IS NOT NULL
DROP TABLE [dbo].survey;
GO
IF OBJECT_ID(N'dbo.[user]', N'U') IS NOT NULL
DROP TABLE [dbo].[user];
GO
CREATE TABLE [user]
(
id INT PRIMARY KEY IDENTITY(1,1),
email VARCHAR(255) NOT NULL UNIQUE,
mobile VARCHAR(15) NOT NULL,
isActive BIT NOT NULL,
role VARCHAR(255) NOT NULL
);
CREATE TABLE survey
(
id INT PRIMARY KEY IDENTITY(1,1),
title VARCHAR(255) NOT NULL,
created_date DATETIME NOT NULL,
updated_date DATETIME NOT NULL
);
CREATE TABLE question
(
id INT PRIMARY KEY IDENTITY(1,1),
survey_id INT NOT NULL,
text NVARCHAR(MAX) NOT NULL,
FOREIGN KEY (survey_id) REFERENCES survey(id)
);
CREATE TABLE answer
(
id INT PRIMARY KEY IDENTITY(1,1),
question_id INT NOT NULL,
text NVARCHAR(MAX) NOT NULL,
is_correct BIT NOT NULL,
FOREIGN KEY (question_id) REFERENCES question(id)
);
CREATE TABLE invitation
(
id INT PRIMARY KEY IDENTITY(1,1),
survey_id INT NOT NULL,
user_id INT NOT NULL,
invitation_link NVARCHAR(MAX) NOT NULL,
FOREIGN KEY (survey_id) REFERENCES survey(id),
FOREIGN KEY (user_id) REFERENCES [user](id)
);
CREATE TABLE submission
(
id INT PRIMARY KEY IDENTITY(1,1),
survey_id INT NOT NULL,
user_id INT NOT NULL,
answer1 INT NOT NULL,
answer2 INT NOT NULL,
answer3 INT NOT NULL,
score INT NOT NULL,
FOREIGN KEY (survey_id) REFERENCES survey(id),
FOREIGN KEY (user_id) REFERENCES [user](id)
);
We then proceed to design the rest of the UI as well. The complete code is given by version_2.
One issue we stumbled upon is this. When we design the survey front end like this –
<div class="container">
<form [formGroup]="surveyForm" (ngSubmit)="save()">
<div class="form-group">
<label for="title">Survey Title</label>
<input type="text" class="form-control" id="title" formControlName="title">
</div>
<div formArrayName="Questions" class="mt-3">
<div *ngFor="let question of getQuestionControls().controls; let i = index" [formGroupName]="i">
<label>Question {{i+1}}</label>
<input type="text" formControlName="Text" class="form-control mb-3">
<div formArrayName="Answers" class="d-flex flex-wrap">
<div>{{getAnswerControls(question.value).controls}}</div>
<div *ngFor="let answer of getAnswerControls(question.value).controls; let j = index" [formGroupName]="j"
class="form-check mr-3 mb-3">
<input class="form-check-input" type="radio" formControlName="isCorrect" [value]="j"
(change)="selectAnswer(i, j)">
<input type="text" formControlName="Text" class="form-control">
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary mr-3">Save</button>
<button type="button" class="btn btn-secondary" (click)="cancel()">Cancel</button>
</div>
</form>
</div>
And ts file like this –
import { Component, OnInit } from '@angular/core';
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { HttpClientService } from 'src/app/http-client.service';
@Component({
selector: 'app-survey-add',
templateUrl: './survey-add.component.html',
styleUrls: ['./survey-add.component.scss']
})
export class SurveyAddComponent implements OnInit {
_client: HttpClientService;
surveyForm = new FormGroup({
title: new FormControl(),
Questions: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
Answers: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
]
)
}
),
new FormGroup(
{
Text: new FormControl(),
Answers: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
]
)
}
),
new FormGroup(
{
Text: new FormControl(),
Answers: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
]
)
}
),
]
),
});
constructor(client: HttpClientService) { this._client = client }
ngOnInit(): void {
}
getQuestionControls() {
return this.surveyForm.controls['Questions'] as FormArray;
}
getAnswerControls(question: any) {
console.log(question['Answers'] as FormArray);
return question['Answers'] as FormArray;
}
selectAnswer(i: number, j: number) {
}
save() {
this._client.postData("survey", this.surveyForm.value).subscribe((res) => {
console.log(res);
});
}
cancel() {
this.surveyForm = new FormGroup({
title: new FormControl(),
Questions: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
Answers: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
]
)
}
),
new FormGroup(
{
Text: new FormControl(),
Answers: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
]
)
}
),
new FormGroup(
{
Text: new FormControl(),
Answers: new FormArray(
[
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
new FormGroup(
{
Text: new FormControl(),
isCorrect: new FormControl(),
}
),
]
)
}
),
]
),
});
}
}
The output is like this –
As we can see; it’s not showing the answers or anything related to that.
Rewriting the ts file as –
getAnswerControls(question: any) {
console.log(question['Answers'] as FormArray);
return question['Answers'];
}
And the html file as –
......
<div formArrayName="Questions" class="mt-3">
<div *ngFor="let question of getQuestionControls().controls; let i = index" [formGroupName]="i">
<label>Question {{i+1}}</label>
<input type="text" formControlName="Text" class="form-control mb-3">
<div formArrayName="Answers" class="d-flex flex-wrap">
<div>{{getAnswerControls(question.value).controls}}</div>
<div *ngFor="let answer of getAnswerControls(question.value); let j = index" [formGroupName]="j"
class="form-check mr-3 mb-3">
<input class="form-check-input" type="radio" formControlName="isCorrect" [value]="j"
(change)="selectAnswer(i, j)">
<input type="text" formControlName="Text" class="form-control">
</div>
</div>
</div>
</div>
.......
Fixed the issue.
Other episodes in this series:
First Part
Second Part
Third Part
Fourth Part
And the code is given is updated into –
Github