angular 5 dynamic reactive forms - html

I am attempting to dynamically present users a set of questions for every product selected.
I have created the questions FormGroup, and then I iterate through the selected products and nest the group of questions beneath each product.
To a degree, this seems to work. The form is created, I can see it via the JSON pipe, and can interact with it.
The problem, however, is that all of the form controls for each product only update the controls for the last product (which I can see via {{form.value | JSON}}
Sample Code:
https://stackblitz.com/edit/angular-py4sam
app.component.ts
import { Component, NgModule, VERSION, OnInit } from '#angular/core'
import { BrowserModule } from '#angular/platform-browser'
import { FormControl, FormGroup, FormArray, ReactiveFormsModule, Validators } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
questionsForm: FormGroup;
qaForm: FormGroup;
questions = [
{
'question': 'like?',
'value': ''
},
{
'question': 'own?',
'value': ''
}
]
products = [
{
'make': 'fender'
},
{
'make': 'gibson'
}
]
createQuestionsForm() {
this.questionsForm = new FormGroup({
questions: new FormArray([])
});
const qArray = <FormArray>this.questionsForm.controls['questions'];
this.questions.forEach((item, index) => {
const aGroup = new FormGroup({
answer: new FormControl(item.question),
value: new FormControl(item.value)
})
qArray.push(aGroup);
})
}
ngOnInit() {
this.createQuestionsForm();
this.qaForm = new FormGroup(
{
qa: new FormArray([])
});
const qaFormArray = <FormArray>this.qaForm.controls['qa'];
this.products.forEach((item, index) => {
const fg = new FormGroup({
make: new FormControl(item.make),
form: this.questionsForm
})
qaFormArray.push(fg);
})
}
}
app.component.html
<h3>FORM</h3>
<form [formGroup]="qaForm">
<div formArrayName='qa'>
<div *ngFor="let prod of products; let productCount = index">
<h3>{{ prod.make }}</h3>
<div [formArrayName]=productCount>
<div formGroupName="form">
<div formArrayName="questions">
<div *ngFor="let q of questions; let qCount = index">
<div [formArrayName]=qCount>
<input type="checkbox" formControlName="value"> {{ q.question }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<p>qaform {{ qaForm.value | json }}</p>

The issue here is that you use the same questionsForm for all products:
const fg = new FormGroup({
make: new FormControl(item.make),
form: this.questionsForm
^^^^^^^^^^^^^^^
})
In order to fix it you need to create questionsForm per product.
Here is how it could be accomplish:
createQuestionsForm() {
const questionsForm = new FormGroup({
questions: new FormArray([])
});
const qArray = <FormArray>questionsForm.controls['questions'];
this.questions.forEach((item, index) => {
const aGroup = new FormGroup({
answer: new FormControl(item.question),
value: new FormControl(item.value)
})
qArray.push(aGroup);
})
return questionsForm;
}
...
this.products.forEach(
...
const fg = new FormGroup({
make: new FormControl(item.make),
form: this.createQuestionsForm()
})
Forked Stackblitz

Related

get input values by there class

I have a following angular component ts with an ability to add dynamic fields to a form:
import {Component, ElementRef, OnInit, ViewChild} from '#angular/core';
import {Buildcompany} from "../../models/buildcompany.model";
import {BuildcompanyService} from "../../services/buildcompany.service";
import { FormGroup, FormControl, FormArray, FormBuilder } from '#angular/forms'
#Component({
selector: 'app-add-buildcompany',
templateUrl: './add-buildcompany.component.html',
styleUrls: ['./add-buildcompany.component.css'],
})
export class AddBuildcompanyComponent implements OnInit {
buildcompany: Buildcompany={
shortname:'',
fullname:'',
address:'',
telephone:'',
website:'',
sociallinks:'',
foundationyear:''
}
submitted=false;
#ViewChild('network', {static: true}) networkElement: ElementRef;
netw?: [];
constructor(private buildcompanyService:BuildcompanyService,networkElement: ElementRef) {
this.networkElement=networkElement;
}
ngOnInit(): void {
}
add(){
let row = document.createElement('div');
row.className = 'row';
row.innerHTML = `
<br>
<input type="text" id="netw" name="network[]" #network class="form-control netw">"`;
let row2 = document.createElement('div');
row2.innerHTML = `
<br>
<input type="text" name="links[]" class="form-control">"`;
// #ts-ignore
document.querySelector('.showInputField').appendChild(row);
// #ts-ignore
document.querySelector('.showInputField').appendChild(row2);
}
saveBuildcompany(): void {
// #ts-ignore
this.netw = (<HTMLInputElement>document.getElementsByClassName("netw")).value;
// #ts-ignore
console.log(this.netw[0].value);
const data = {
shortname: this.buildcompany.shortname,
fullname: this.buildcompany.fullname,
address: this.buildcompany.address,
telephone: this.buildcompany.telephone,
website: this.buildcompany.website,
sociallinks: this.buildcompany.sociallinks,
foundationyear: this.buildcompany.foundationyear
};
this.buildcompanyService.create(data)
.subscribe({
next: (res) => {
console.log(res);
this.submitted = true;
},
error: (e) => console.error(e)
});
}
newBuildcompany(): void {
this.submitted = false;
this.buildcompany = {
shortname: '',
fullname:'',
address:'',
telephone:'',
website:'',
foundationyear:''
};
}
}
Is that posible at all to get values of those dynamicly added fields into an array on save() function? I can see in a console that they are actually shown. The class is called netw. Or what is the best approche to this task?
UPDATE
I tryed to add a formgroup and got the following error
Error:
ngModel cannot be used to register form controls with a parent formGroup directive. Try using
formGroup's partner directive "formControlName" instead. Example:

Angular 13 Parent-Child not Communicating

Trying to get an array from a parent component to a child component. The Angular docs make it look really simple but I'm not sure what I'm doing wrong. The variable is activePost. Angular version is 13
Parent ts file (HomepageComponent):
import { Component, OnInit } from '#angular/core';
import { PostService } from 'src/app/services/post.service';
import { map } from 'rxjs/operators';
import { Post } from 'src/app/models/post.model';
import { ActivatedRouteSnapshot, Router } from '#angular/router';
import { PostDisplayComponent } from '../components/post-display/post-display.component';
#Component({
selector: 'app-homepage',
templateUrl: './homepage.component.html',
styleUrls: ['./homepage.component.scss']
})
export class HomepageComponent implements OnInit {
posts?: Post[];
category?:'';
currentpost?: Post;
currentIndex = -1;
title = '';
content='';
activePost: Post;
images: string[] =["../assets/img/damwon.jpg",
"../assets/img/FPX.jpg",
"../assets/img/2015skt.webp",
"../assets/img/2017SSG.webp",
"../assets/img/2014SSW.webp",
"../assets/img/TPA.webp",
"../assets/img/Fnatic.webp"]
backgroundImage: string = '';
constructor(private PostService: PostService,
private route: Router) { }
ngOnInit() {
let ran = Math.floor(Math.random()*6);
console.log(ran, Math.random()*100)
this.backgroundImage = this.images[ran];
this.retrieveposts();
}
refreshList(): void {
this.currentpost = undefined;
this.currentIndex = -1;
this.retrieveposts();
}
retrieveposts(): void {
this.PostService.getAll().snapshotChanges().pipe(
map(changes =>
changes.map(c =>
({ id: c.payload.doc.id, ...c.payload.doc.data() })
)
)
).subscribe(data => {
this.posts = data;
});
}
setActivepost(post: Post, index: number): void {
this.currentpost = post;
this.currentIndex = index;
console.log("Post:", post, "Index:", index);
this.activePost = this.currentpost
this.route.navigate(['/Read/'])
}
}
Child ts file (post-display component)
import { Component, OnInit, Input, OnChanges, Output, EventEmitter } from '#angular/core';
import { Post } from 'src/app/models/post.model';
import { PostService } from 'src/app/services/post.service';
import { HomepageComponent } from 'src/app/homepage/homepage.component';
#Component({
selector: 'app-post-display',
templateUrl: './post-display.component.html',
styleUrls: ['./post-display.component.scss']
})
export class PostDisplayComponent implements OnInit {
#Input() activePost: Post;
#Output() refreshList: EventEmitter<any> = new EventEmitter();
currentPost: Post = {
title: '',
description: '',
category:'',
published: false,
content: ''
};
message = '';
constructor(private PostService: PostService) { }
ngOnInit(): void {
console.log(this.activePost)
}
}
Child HTML:
<div class="container" style="padding-top: 200px;">
<div class="post">
ACTIVE POST HERE:
{{activePost}}
</div>
Looking at the console, the child component always returns undefined for activePost. I'm not sure if this is because I dont have anything in the parent html code for the child to look at? I feel like I should just be able to do this in the .ts file.
Help would be appreciated. Let me know if there are other project docs I should share as well.
Edit, added parent html:
<header class="header" [ngStyle]="{'background-image': 'url(' + backgroundImage + ')'}">
<div class="content">
<h1 class="heading">
<span class="small">Samsite:</span>
Stat
<span class="no-fill">check</span>
</h1>
<!--write a blog-->
</div>
</header>
<section class="blogs-section">
<div class="blog-card"
*ngFor="let post of posts; let i = index"
>
<h1>Title: {{ post.title }}</h1>
<h2> Category: {{ post.category }}</h2>
<p class="blog-overview"> Preview: {{ post.description }}</p>
<div class="btn" (click)="setActivepost(post, i)">Read!</div>
</div>
</section>

Setting selected in dropdownlist with Angular

I have a list displayed in a dropdownlist, but it displays the default as a blank and not as the first item in the dropdown.
I have tried adding let i = 0 and then [selected]="i = 0", but this does not seem to set the default item to the first item, however I am receiving the correct value back from i.
Below is my code:
<div class="form-group">
<label for="userName">User Name</label>
<select formControlName="userName" class="form-control" (change)="userChange($event)">
<option *ngFor="let row of usersModel;let i = index" value="{{ row.id }}" [selected]="i == 0">{{ row.userName }} {{ i }}</option>
</select>
</div>
Here is my TypeScript File:
import { Component, OnInit } from '#angular/core';
import { UserAdminService } from '../../services/user-admin.service';
import { FormBuilder, Form, FormControl, FormGroup } from '#angular/forms';
import { Router } from '#angular/router';
#Component({
selector: 'lib-add-user-to-role',
templateUrl: './add-user-to-role.component.html',
styleUrls: ['./add-user-to-role.component.css']
})
export class AddUserToRoleComponent implements OnInit {
addUserToRoleForm: FormGroup;
rolesModel: any[];
usersModel: any[];
selectedRole: string;
selectedUser: string;
constructor(private userAdminService: UserAdminService, private formBuilder: FormBuilder, private router: Router) {
var roleControl = new FormControl('');
var userControl = new FormControl('');
this.addUserToRoleForm = formBuilder.group({ roleName: roleControl, userName: userControl });
}
ngOnInit() {
this.userAdminService.getRoles().subscribe(roles => {
this.rolesModel = roles;
this.selectedRole = this.rolesModel[0].name;
this.userAdminService.getUsersNotInRole(this.selectedRole).subscribe(users => {
this.usersModel = users;
this.selectedUser = this.usersModel[0].id;
console.log(this.usersModel[0].userName);
this.addUserToRoleForm.controls['roleName'].setValue(this.rolesModel[0].name);
this.addUserToRoleForm.controls['userName'].setValue(this.usersModel[0].userName);
});
});
}
userChange(event: any) {
this.selectedUser = event.target.value;
console.log('Selected ' + this.selectedUser);
}
AddUserToRole() {
this.userAdminService.addUserToRole(this.selectedUser, this.selectedRole)
.subscribe(result => {
if (result.success) {
this.router.navigate(['/roleusermaint']);
}
else {
console.log('Error Received on adding user to role');
}
});
}
}
As you can see I added {{ i }} in the text to make sure it shows the value of i and it does:
What am I missing ?
Thanks for any help!
#Axle, if you're using a Reactive Form, you needn't use [selected] nor (change), just, when you create the form you give value to userName
Using the constructor
const firstId=usersModel[0].id
this.form=new FormGroup({
userName:new FormControl(firstId)
})
Using formBuilder
const firstId=usersModel[0].id
this.form=this.fb.group({
userName:firstId
})
Using setValue, after create the form
const firstId=usersModel[0].id
this.form.get('userName').setValue(firstId)
As you are using Angular reactive form, try to keep the logic in ts file itself.
Using setValue(), you can set the default value to a control.
To set the default value to form control you could to it like,
this.form.controls['country'].setValue(this.countries[0].id)
In template use it like,
<option *ngFor="let country of countries" [value]="country.id">
{{ country.name }}
</option>
Working Stackblitz
Ref:
A complete sample code would be something like,
app.component.ts
import { Component } from '#angular/core';
import { FormGroup, FormControl } from '#angular/forms';
import {Country} from './country';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
countries = [
{
id: 'us',
name: 'United States'
},
{
id: 'uk',
name: 'United Kingdom'
},
{
id: 'ca',
name: 'Canada'
}
];
form: FormGroup;
ngOnInit(){
this.form = new FormGroup({
country: new FormControl('')
});
this.form.controls['country'].setValue(this.countries[0].id)
}
}
app.component.html
<form [formGroup]="form">
<select formControlName="country">
<option *ngFor="let country of countries" [value]="country.id">
{{ country.name }}
</option>
</select>
</form>

How to push array elements into object in angular 7

I have some checkboxes whose value coming from json using ngFor. When I select those checkboxes and click submit, I need to capture the 'li' tag value and selected checkbox value in the form of array of object mentioned in output in code section. Here I am getting only 'li' tag value/text in the array but I am not getting how to push it into object along with selected checkbox value like output format.Here is the code below.
home.component.html
<div class="col-md-3" id="leftNavBar">
<ul *ngFor="let item of nestedjson">
<li class="parentNav">{{item.name}}</li>
<li class="childData">
<ul>
<li *ngFor="let child of item.value">{{child}}<span class="pull-right"><input type="checkbox"></span></li>
</ul>
</li>
</ul>
<div><button type="submit" (click)="getit()">submit</button></div>
</div>
home.component.ts
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import Speech from 'speak-tts';
import { RxSpeechRecognitionService, resultList, } from '#kamiazya/ngx-speech-recognition';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
providers: [ RxSpeechRecognitionService ]
})
export class HomeComponent implements OnInit {
data:any;
nestedjson:any;
message = '';
test:any;
constructor(private formBuilder: FormBuilder,public service: RxSpeechRecognitionService) {
}
ngOnInit() {
this.nestedjson = [
{ name: "parent1", value: ["child11", "child12"] },
{ name: "parent2", value: ["child2"] },
{ name: "parent3", value: ["child3"] }
];
}
getit(){
const data = this.nestedjson;
let duplicatePushArray = [];
for(let i = 0; i < data.length ; i++){
if(duplicatePushArray.indexOf(data[i].name) === -1) {
duplicatePushArray.push(data[i].name);
} else {
console.log(`${data[i]} is already pushed into array`);
}
}
console.log('Final Array: ', duplicatePushArray)
/*output: [{"name":"parent1","value":["child11","child12"]},{"name":"parent2","value":["child2"]},{"name":"parent3","value":["child3"]}]*/
}
}
You don't have anything happening when the checkbox is selected. I would recommending adding an onChange binding so that you can save all of your checked children to an array that you can reference when you click submit.
home.component.html
<li *ngFor="let child of item.value">{{child}}
<span class="pull-right">
<input type="checkbox" (change)="addtoChecked(child)">
</span>
</li>
home.component.ts
private checkedChildren = <string[]>[];
public addToChecked(child: string): void {
if(this.checkedChildren.indexOf(child) > -1){ // you can also pass in the $event from the html to this method to determine if it was checked or not
this.checkedChildren = this.checkedChildren.filter(c => c !== child);
} else {
this.checkedChildren.push(child);
}
}
getit(): void {
const output = <{name: string, value: string[]}[]>[];
this.checkedChildren.forEach((child) => {
const jsonData = this.nestedjson.find(j => j.value.indexOf(child) > -1);
if(!jsonData) {
// something went wrong
return;
}
const existingDataIndex = output.findIndex(o => o.name == jsonData.name);
if(existingDataIndex === -1){
output.push({ name: jsonData.name, value: [child]});
} else {
output[existingDataIndex].value.push(child);
}
});
console.log(output);
}
To achieve expected result, use below option of using reduce and checked flags for checkboxes
Add checked array to original Array to keep track of checked boxes
this.nestedjson.forEach(v => v.checked = Array(v.value.length).fill(false));
Updated array of checked values based on checked checkboxes
Using reduce updated Final array by filtering out only checked boxes
let duplicatePushArray = this.nestedjson.reduce((acc, v) => {
let temp = {name: v.name, value: []};
v.checked.forEach((val, i) => {
if(val){
temp.value.push(v.value[i]);
}
})
if(temp.value.length > 0){
acc.push(temp)
}
return acc
}, []);
Updated app.component.html and app.component.ts files below for reference
app.component.html:
<div class="col-md-3" id="leftNavBar">
<ul *ngFor="let item of nestedjson">
<li class="parentNav">{{item.name}}</li>
<li class="childData">
<ul>
<li *ngFor="let child of item.value; let i = index">{{child}}<span class="pull-right"><input type="checkbox" (change)="item.checked[i] = !item.checked[i]"></span></li>
</ul>
</li>
</ul>
<div><button type="submit" (click)="getit()">submit</button></div>
</div>
app.component.ts:
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
data:any;
nestedjson:any;
message = '';
test:any;
constructor(private formBuilder: FormBuilder) {
}
ngOnInit() {
this.nestedjson = [
{ name: "parent1", value: ["child11", "child12"] },
{ name: "parent2", value: ["child2"] },
{ name: "parent3", value: ["child3"] }
];
this.nestedjson.forEach(v => v.checked = Array(v.value.length).fill(false));
}
getit(){
const data = this.nestedjson;
let duplicatePushArray = this.nestedjson.reduce((acc, v) => {
let temp = {name: v.name, value: []};
v.checked.forEach((val, i) => {
if(val){
temp.value.push(v.value[i]);
}
})
if(temp.value.length > 0){
acc.push(temp)
}
return acc
}, []);
console.log('Final Array: ', duplicatePushArray)
/*output: [{"name":"parent1","value":["child11","child12"]},{"name":"parent2","value":["child2"]},{"name":"parent3","value":["child3"]}]*/
}
}
Sample working code for reference - https://stackblitz.com/edit/angular-b9fmyz?file=src/app/app.component.html

How to create a custom form validator to accept only valid JSON in Angular

In my Angular app, I have a reactive form which for simplicity I will assume to have only one control called configJson which is represented by a <textarea> in the DOM.
I need to validate this form control to only accept valid JSON text from the user input, and display an error message otherwise.
Here's my component's class and template:
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup } from '#angular/forms';
#Component({
selector: 'app-configuration',
templateUrl: './configuration.component.html',
styleUrls: ['./configuration.component.scss']
})
export class ConfigurationComponent implements OnInit {
form: FormGroup;
constructor() {}
ngOnInit() {
this.form = new FormGroup({
'configJson': new FormControl(),
});
// TODO: someone add JSON validation
}
loadJsonConfiguration() {
const config = JSON.parse(this.form.get('configJson').value);
// some logic here using the parsed "config" object...
}
}
<form [formGroup]="form">
<div class="form-group">
<label for="json-config-textarea">Parse from JSON:</label>
<textarea
class="form-control"
id="json-config-textarea"
rows="10"
[formControlName]="'configJson'"
></textarea>
</div>
<div [hidden]="form.get('configJson').pristine || form.get('configJson').valid">
Please insert a valid JSON.
</div>
<div class="form-group text-right">
<button
class="btn btn-primary"
(click)="loadJsonConfiguration()"
[disabled]="form.get('configJson').pristine || form.get('configJson').invalid"
>Load JSON Configuration</button>
</div>
</form>
I originally tried to edit the answer by the OP, but it was rejected by peer reviewers due to:
This edit was intended to address the author of the post and makes no
sense as an edit. It should have been written as a comment or an
answer.
So, here is my modified version:
import {AbstractControl, ValidationErrors, ValidatorFn} from '#angular/forms';
export function jsonValidator(control: AbstractControl): ValidationErrors | null {
try {
JSON.parse(control.value);
} catch (e) {
return { jsonInvalid: true };
}
return null;
};
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup, Validators } from '#angular/forms';
import { jsonValidator } from './json.validator';
#Component({
selector: 'app-configuration',
templateUrl: './configuration.component.html',
styleUrls: ['./configuration.component.scss']
})
export class ConfigurationComponent implements OnInit {
form: FormGroup;
ngOnInit() {
this.form = new FormGroup({
configJson: new FormControl(Validators.compose(Validators.required, jsonValidator))
});
}
loadJsonConfiguration() {
...
}
}
One solution is creating a custom form validator and attach it to the form control. The job of the validator is to only accept valid JSON.
This is how my validator looks like:
import {AbstractControl, ValidationErrors, ValidatorFn} from '#angular/forms';
export function jsonValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const error: ValidationErrors = { jsonInvalid: true };
try {
JSON.parse(control.value);
} catch (e) {
control.setErrors(error);
return error;
}
control.setErrors(null);
return null;
};
}
It can be easily unit-tested with the following:
import { FormControl, ValidationErrors, ValidatorFn } from '#angular/forms';
import Spy = jasmine.Spy;
import { jsonValidator } from './json.validator';
describe('JSON Validator', () => {
let control: FormControl;
let spySetErrors: Spy;
let validator: ValidatorFn;
const errorName = 'jsonInvalid';
beforeEach(() => {
control = new FormControl(null);
validator = jsonValidator();
spySetErrors = spyOn(control, 'setErrors').and.callThrough();
});
for (const { testId, valid, value } of [
{ testId: 1, valid: true, value: '{}' },
{ testId: 2, valid: true, value: '{"myKey": "myValue"}' },
{ testId: 3, valid: true, value: '{"myKey1": "myValue1", "myKey2": "myValue2"}' },
// more valid cases can be added...
{ testId: 4, valid: false, value: 'this is not a valid json' },
{ testId: 5, valid: false, value: '{"theJsonFormat": "doesntLikePendingCommas",}' },
{ testId: 6, valid: false, value: '{"theJsonFormat": doesntLikeMissingQuotes }' },
// more invalid cases ca be added...
]) {
it(`should only trigger the error when the control's value is not a valid JSON [${testId}]`, () => {
const error: ValidationErrors = { [errorName]: true };
control.setValue(value);
if (valid) {
expect(validator(control)).toBeNull();
expect(control.getError(errorName)).toBeFalsy();
} else {
expect(validator(control)).toEqual(error);
expect(control.getError(errorName)).toBe(true);
}
});
}
});
In the component's ngOnInit, the new validator should be added:
this.form.get('configJson').setValidators([
Validators.required, // this makes the field mandatory
jsonValidator(), // this forces the user to insert valid json
]);
So the component's class now looks like this:
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup, Validators } from '#angular/forms';
import { jsonValidator } from './json.validator';
#Component({
selector: 'app-configuration',
templateUrl: './configuration.component.html',
styleUrls: ['./configuration.component.scss']
})
export class ConfigurationComponent implements OnInit {
form: FormGroup;
constructor() {}
ngOnInit() {
this.form = new FormGroup({
'configJson': new FormControl(),
});
this.form.get('configJson').setValidators([
Validators.required,
jsonValidator(),
]);
}
loadJsonConfiguration() {
const config = JSON.parse(this.form.get('configJson').value);
// some logic here using the parsed "config" object...
}
}