Dynamic Form Creation in angular 6 - angular6

I am trying to get started with creating a dynamic form in Angular 2, and I am using the setup from the Angular documentation . I didn't have any issues with their setup, which just hard codes the data in the service as apposed to an api call. My issue is that when I try to use an api call the values my dynamic form creation is failing.
The data is coming in from the api call successfully. I believe my issue is that [questions] is binding before the data is ready. Can someone tell me a better way to do this or please provide any suggestions to what I'm doing wrong please? Is there a way I could set the properties in the api first?
Here below my approcah looks like :
TS FILE
export class DynamicFormComponent implements OnInit {
#Input() questions: QuestionBase<any>[] = [];
form: FormGroup;
payLoad = '';
constructor(private qcs: QuestionControlService ,private dy :QuestionService) { }
ngOnInit() {
this.questions=this.dy.getDataFromService();
this.form = this.qcs.toFormGroup(this.questions);
}
onSubmit() {
this.payLoad = JSON.stringify(this.form.value);
}
}
HTML
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form" *ngIf="isFormReady">
<div *ngFor="let question of questions" class="form-row">
<app-question [question]="question" [form]="form"></app-question>
</div>
<div class="form-row">
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
<div *ngIf="payLoad" class="form-row">
<strong>Saved the following values</strong><br>{{payLoad}}
</div>
</div>
##QUESTIONSERVICE.TS
getDataFromService(){
let questions: QuestionBase<any>[]=[];
this.auth.dynamicFormData().subscribe(
(result) => {
let data=result;
for(var i=0;i<data.length;i++){
questions.push(new TextboxQuestion(data[i]));
}
});
return questions.sort((a, b) => a.order - b.order);
}
}
RESULT DATA FROM API IS
[{"value":"Sireesha_0","key":"firstName_0","label":"First Name_0","required":true,"order":1,"controlType":"text"},{"value":"Sireesha_1","key":"firstName_1","label":"First Name_1","required":true,"order":1,"controlType":"text"}]
ERRORS
An error occurred: Cannot read property 'valid' of undefined
Error encountered in Error Interceptors TypeError: Cannot read property 'valid' of undefined at DynamicFormQuestionComponent.get [as isValid] (dynamic-form-question.component.ts:13)

I have changed my code like below and it's working good.
html
<form (ngSubmit)="onSubmit()" [formGroup]="form" *ngIf="isFormReady">
<div *ngFor="let question of questions" class="form-row">
<app-question [question]="question" [form]="form"></app-question>
</div>
<div class="form-row">
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
ts
isFormReady:booelan;
ngOnInit() {
this.questions=this.dy.getDataFromService();
}
getDataFromService(){
let questions: QuestionBase<any>[]=[];
this.auth.dynamicFormData().subscribe(
(result) => {
let data=result;
for(var i=0;i<data.length;i++){
questions.push(new TextboxQuestion(data[i]));
}
});
this.form = this.qcs.toFormGroup(this.questions);
this.isFormReady=true;
return questions.sort((a, b) => a.order - b.order);
}

Related

Angular Typescript and HTML

I have an interface that looks like this:
export interface GeneralInfo{
Name: string;
Description: string;
}
Later in a component class, I have the following code
export class SignalsComponent implements OnInit {
objGeneral: GeneralInfo;
constructor(private _apiService: APIService)
openPopUp(){
this._apiService.getJsonData().subscribe(
(res => {
var tempJson = JSON.parse(res);
this.objGeneral = tempJson.General as GeneralInfo;
console.log("json --->", this.objGeneral.Description);
}),
(err => { })
);
}
}
When I look at the browser console all works and I see the data I expect to see. However, when I try to invoke the objGeneral.Description property in HTML, it fails. This is my HTML:
<div class="col-lg-6 col-md-6">
{{objGeneral.Description}}
</div>
What am I doing wrong?
Thank you
1) Quick answer
Add a condition to your div :
<div class="col-lg-6 col-md-6" *ngIf="objGeneral">
{{objGeneral.Description}}
</div>
Or use optional chaining if you still want to render an empty div :
<div class="col-lg-6 col-md-6">
{{objGeneral?.Description}}
</div>
Note that you can use optional chaining in condition :
<div class="col-lg-6 col-md-6" *ngIf="objGeneral?.Description">
2) Complete answer
objGeneral is still not defined when your DOM is rendering, it will be defined when the Observable completes. Means you are asking the DOM to render undefined data.
When I look at the browser console all works
Weird, because you should have this type of error in your console which prevents you to call the property of an undefined object :
ERROR TypeError: Cannot read properties of undefined (reading 'Description')
3) Ressources & useful links you might want to check for more information
https://javascript.plainenglish.io/the-beauty-of-optional-chaining-in-typescript-32dd58ce1380
The way you do it is incorrect - objGeneral exists only after async API call. Instead you should deliver this property via async pipe
export class SignalsComponent implements OnInit {
objGeneral$: BehavioutSubject<GeneralInfo> | null;
constructor(private _apiService: APIService)
openPopUp(){
this.objGeneral$ = this._apiService.getJsonData().pipe(
map(res => JSON.parse(res).General as GeneralInfo)
);
}
}
and use it as
<div class="col-lg-6 col-md-6" *ngIf="objGeneral$ | async as objGeneral">
{{objGeneral.Description}}
</div>

Getting error after upgrading from Angular 8 to 12 - "Property 'controls' does not exist on type 'AbstractControl' "

I had a fully functioning code in Angular 8. I decided to upgrade from 8 to Angular 12.
I have a dynamic reactive form. The form is of question answer format.
Based on who is login in the questions change. The answer is of Yes/No format. Based on who is login in and what answer option is chosen textbox dropdown and checkbox or all the there mentioned appreas.
Here is my code for the form
Component.ts
constructor(
private router: Router,
private datePipe: DatePipe,
private route: ActivatedRoute,
private formBuilder: FormBuilder) {
this.dynamicForm = this.formBuilder.group({
questions: new FormArray([])
});
this.CreateForm();
}
get f() { return this.dynamicForm.controls; }
get t() { return this.f.questions as FormArray; }
CreateForm() {
if (this.formdetail.questionList) {
// CREATE Questions
if (racialQues !== this.formdetail.questionList[i].quetext && this.formdetail.questionList[i].quetext !== auditQues) {
this.t.push(this.formBuilder.group({
quesIndex: [i + 1],
...
controlName: [this.formdetail.questionList[i].selectedAns, [Validators.required]]
}));
} else if (this.formdetail.questionList[i].quetext === auditQues) {
this.t.push(this.formBuilder.group({
quesIndex: [i + 1],
......
selectedValue: [this.formdetail.auditTrail.selectedValue, this.reasonValidator],
}));
} else if (this.formdetail.questionList[i].quetext === racialQues) {
this.t.push(this.formBuilder.group({
quesIndex: [i + 1],
......
selectedAuxAns: [this.formdetail.questionList[i].val, [this.auxAnsValidator]]
}));
}
}
}
Here is the html
<form [formGroup]="dynamicForm">
<!-- Question Start -->
<div *ngFor="let ticket of t.controls; let i = index">
<div [formGroup]="ticket" class="form-row">
<div class="input-group col-md-12" style="padding: .15em .5em">
<div class="input-group-prepend col-md-10" style="padding: 0; margin: 0;">
<label class="input-group-text w-15">{{i + 1}}</label>
<label class="input-group-text w-100" style="text-align: left; white-space: normal;">{{ticket.controls.name.value}}</label>
</div>
<select formControlName="controlName" class="form-control col-md-2" style="height:auto !important;"
[ngClass]="{ 'is-invalid': submitted && ticket.controls.controlName.errors }"
(change)="onChange($event.target.value, i)">
<option [ngValue]="null"> --Select-- </option>
<option *ngFor="let ansItem of formdetail.questionList[i].answerList" [ngValue]="ansItem" >{{ansItem.anstext}}</option>
</select>
<div *ngIf="submitted && ticket.controls.controlName.errors" class="invalid-feedback" style="height:auto !important;"
[ngClass]="{ 'input-group-append': submitted && ticket.controls.controlName.errors,
'col-md-1': submitted && ticket.controls.controlName.errors }">
<div *ngIf="ticket.controls.controlName.errors.required">Required</div>
</div>
.
.
.
.
</div>
/div>
</div>
<!-- Question End -->
</form>
Now I am getting error in "ticket.control"
Property 'controls' does not exist on type 'AbstractControl'.
I am not sure why I am getting this error after upgrade.
I have tried ticket.['control'] or ticket.get('controlName') as suggested by other article of similar error in stackoverflow but nothing works
Here is the snapshot of error
Any help will be appreciated.
Thanks
your t is of type FormArray, FormArray.controls array is of type AbstractControl[], thus, each ticket at
<div *ngFor="let ticket of t.controls; let i = index">
line is of type AbstractControl, which does not have controls property as is seen in TS error in your screenshot. TS does not know that each ticket actually is of type FormGroup (set here this.t.push(this.formBuilder.group({...}) and which has controls property).
So you can try to add another getter tControls:
get f() { return this.dynamicForm.controls; }
get t() { return this.f.questions as FormArray; }
get tControls() { return this.t.controls as FormGroup[]; }
and use it within *ngFor:
<div *ngFor="let ticket of tControls; let i = index">
<div [formGroup]="ticket" class="form-row">
...
Please note, that now [formGroup] will also be properly filled with FormGroup and not AbstarctControl
One of your Angular upgrades probably added
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInputTypes": true,
}
to your tsconfig.json file, you can verify it just by setting these values to false and rebuilding your app, but I recommend to leave these strict checks.
This is a generic answer (so generic code) to help fixing such an issue.
Let's suppose you have a loop through a field (call it elements) of type FormArray. This field is part of a form of type FormGroup
In your ts logic code (logic implementation) you should have something like:
form = new FormGroup({
//.. other fields
elements: new FormArray([]),
});
In your template (html file), you need to toop through the elements field:
<li
*ngFor="let element of elements.controls">
{{ element.value }}
</li>
In order to tell angular cli that elements has a property of controls (so of type FormArray), you need to format it in your ts logic code as the following (implement a getter for elements field):
get elements() {
return this.form.get('elements') as FormArray;
}
'controls' isn't a property of an AbstractControl. instead you can use your form group, so 'dynamicForm.controls.controlName'

How do I perform field validation within the Angular typescript and not in the form?

Due to specific requirements, I need to validate the presence of my delivery.address field within the typescript code, so I am calling a function, addressIsValid(), to do this as shown in the code below.
<div class="col col-md-6 col-lg-12">
<input class="form-control" type="text" name="address" id="address"
[(ngModel)]="delivery.address" #address="ngModel">
<ng-container *ngIf="delivery.method=='Delivery'">
<div *ngIf="!addressIsValid()" class="primary-color">
<!-- <div *ngIf="address.invalid && (address.dirty
|| address.touched)" class="primary-color"> -->
Address is required
<!-- </div> -->
</div>
</ng-container>
</div>
Typescript function:
public addressIsValid() {
return !this.delivery.address == undefined
&& !this.delivery.address == null;
}
The problem is after valid value is entered into the field, the error message: "Address is required." does not go away. How do I fix this?
I think the problem is in your addressIsValid function.
Take this 2 objects for example:
const o = { name: 'john' };
const o2 = { name: undefined };
!o.name --> false;
!o2.name --> true;
Neither of the above fulfills the condition == undefined or == null.
Thus, you will always get a falsy value.
You could modify your function like this:
public addressIsValid() {
return this.delivery.address !== undefined
&& this.delivery.address !== null;
}
You probably wanted to check this:
public addressIsValid() {
return !!this.delivery.address;
}
example
If that is not the case you need to debug what this.delivery actually contains after your method call.
console.log(this.delivery)
And tell us what that contains.
I would suggest reading about Angular Reactive Forms. You can easily define validation rules in TypeScript and drive the display of error messages in your view.
this.heroForm = new FormGroup({
'name': new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
'alterEgo': new FormControl(this.hero.alterEgo),
'power': new FormControl(this.hero.power, Validators.required)
});
<div *ngIf="name.errors.required">
Name is required.
</div>
<div *ngIf="name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors.forbiddenName">
Name cannot be Bob.
</div>
Also take a look at the FormBuilder service, which will help condense the form creating syntax.

How to use custom pipes Angular2

I have the following JSON object: http://pastebin.com/1TguvZXc
Here is my Models Component HTML:
<button *ngFor="let category of categories" (click)="chooseCategory(this.category)" type="button" name="button" class="btn btn-default" id="{{category}}">
{{category}}
</button>
<div *ngFor="let model of models?.models">
<div *ngFor="let year of model['years']">
<div *ngFor="let style of year['styles'] | chooseCategory">
{{model.name}}, {{style.submodel.body }}
</div>
</div>
A (pipe?) method from my models.component:
chooseCategory(selectedCategory: string): void {
if((selectedCategory === '')) {
this.filterByPipe.transform(this.models,
['models.years.styles.submodel.body'], selectedCategory);
}
}
Additionally, I would like to use the FilterByPipe pipe from ngx-pipes to filter out by category in models.years.styles.submodel.body.
The code from my HTML roduces the following error:
Unhandled Promise rejection: Template parse errors:
The pipe 'chooseCategory' could not be found ("or="let model of models?.models">
<div *ngFor="let year of model['years']">
<div *ngFor="let s[ERROR ->]tyle of year['styles'] | chooseCategory">
{{model.name}}, {{style.submodel.body }}
I think that you not even read the documentation. Yu should create pipe in this way:
#Pipe({
name: 'somePipe'
})
export class SomePipe {
transform(value: any[]): any[] {
//some transform code...
}
}
and then can you call that in HTML file in this way:
<div *ngFor="let elem of elements | somePipe"></div>
Dont forget to declare your pipe in module.
#NgModule({
declarations: [ SomePipe ]
})
That's what you use is a method, not a pipe.
If you want to executing pipe depend on (f.e.) button click you should build Pipe with argument:
#Pipe({
name: 'somePipe'
})
export class SomePipe {
transform(value: any[], args: any[]): any[] {
let someFlag: boolean = false;
if(args && args[0]) someflag = true;
if(someflag) {
//some transform code...
}
}
}
to call this pipe in this way
<div *ngFor="let elem of elements | somePipe : yesOrNo"></div>
and then can you use in your component method to click button
yesOrNo: boolean = false;
onClickButton(event: any) {
event.preventDefault();
yesOrNo = !yesOrNo;
}
Since you're importing the pipe and calling it from a button in your component, you don't need to call the pipe directly in your component. Also, chooseCategory is just a method, not a pipe. Then, remove the pipe from the following line:
<div *ngFor="let style of year['styles'] | chooseCategory">

Pass $scope along with $location.Path() in AngularJS

I have two screens in my project, MemberList.HTML and EditMember.html.
MemberList.HTML displays all members with Edit link for each member.
When I click on Edit link, it calls the function ng-click="EditMember(member)" and code for EditMember(member) is
$scope.EditMember = function (member) {
var getData1 = angularService.GetMember(member.ID);
getData1.then(function (mem) {
$scope.Member = mem.data;
alert($scope.Member.FirstName);
$location.path('/members/editmember');
}, function (error)
{
alert('Error in getting Member record');
}
);
};
code for EditMember.Html
<div>
<div class="bottom-margin">
<div class="left-label">ID</div>
<div><input type="text" name="txtID" id="txtID" ng-model="Member.ID"/></div>
</div>
<div class="bottom-margin">
<div class="left-label">First Name</div>
<div><input type="text" name="txtFirstName" ng-model="Member.FirstName"/></div>
</div>
</div>
<div>
<div class="bottom-margin">
<div class="left-label"><input type="button" name="btnCancel" value="Cancel" /></div>
<div><input type="button" name="btnSave" value="Save" /></div>
</div>
</div>
Route configuration is
$routeProvider.when('/members/editmember',
{
templateUrl: '/Template/EditMember.html',
controller: 'myCntrl'
});
Now the problem is, in alert it is showing me the First Name but it is not displaying any data in EditMember.Html.
Everything is in same angular CONTROLLER, there is no different controller is used here.
How do I pass $scope with member data to EditMember.Html? What am I doing wrong?
Unlike services, controllers are not singletons in angular. When you changed the location, a new instance of that controller was created, and therefore a new scope.
Personally, I would pass a reference in the URL to the member you want to edit, e.g. /members/edit/1234, and load that data in when the controller loads, or during routing using $routerProvider resolve.
Personally, I would also consider using a different controller for editing vs viewing, and moving any shared functionality into services - just to keep things coherent.
#glennanthonyb, I did something like this....I am using the same controller here.
In route, I have added
$routeProvider.when('/members/editmember/:ID',
{
templateUrl: '/Template/EditMember.html',
controller: 'myCntrl'
});
and in the Controller I have added $routeParams parameter
if ($routeParams.ID != null) {
GetMember();
}
function GetMember() {
var getData = angularService.GetMember($routeParams.ID);
getData.then(function (mem) {
$scope.Member = mem.data;
}, function (error) {
alert('Error in getting records');
});
}
In MemberList.Html instead of calling a function, I am using href
Edit
I am not sure if it is the right way to do it or not but it is working.