There is already a similar question here (Setting initial value Angular 2 reactive formarray) but I am not satisfied with the answer or maybe looking for some other solution.
I think whole point of having FormArray is to pass the array of objects and it should create equal number of components. But in this above example if you look at the provided plunker , even after providing two Addresses object one Address was created because its blank version was already created in ngOnInit() .
So my question is if in ngOnInit() I have it like this addresses: this._fb.array([]) // blank list,
then how should I set its value that it dynamically creates N number of addresses from N number of addresses in my TypeScript array ?
To set and remove the values from the form array refer to the below code. The given code is just for your reference, please adjust to your code accordingly.
import { FormArray, FormBuilder, FormGroup} from '#angular/forms';
export class SomeComponent implements OnInit {
consutructor(public fb:FormBuilder) { }
ngOnInit() {
public settingsForm: FormGroup = this.fb.group({
collaborators:this.fb.array([this.buildCollaboratorsGroup(this.fb)])
});
this.setFormArrayValue();
}
public buildCollaboratorsGroup(fb:FormBuilder): FormGroup {
return fb.group({
email:'',
role:''
});
}
// Here I'm setting only one value if it's multiple use foreach
public setFormArrayValue() {
const controlArray = <FormArray> this.settingsForm.get('collaborators');
controlArray.controls[0].get('email').setValue('yourEmailId#gmail.com');
controlArray.controls[0].get('role').setValue(2);
}
// Here removing only one value if it's multiple use foreach
public removeFormArrayValue() {
const controlArray = <FormArray> this.settingsForm.get('collaborators');
controlArray.removeAt(0);
}
}
I am having the same issue. Here is how I do it:
As you mentioned, you initialize your form Array like this:
addresses: this._fb.array([])
Then inside ngOnInit() (or in my case ionViewDidLoad() - using Ionic 2), you do your async operation to hit your remote database and get back the value either via promise or observable (and subscribe to the observable). Then you patchValue to all the other form control that is NOT formArray (don't use setValue if you have form group and form array!!).
For the formArray, do this:
this.yourForm.setControl('addresses', this.fb.array(data.addresses || []));
Where data.addresses is an array of addresses (you create them from the same form on previous operation.)
Hope this solve your question as well as mine :) FormArray is powerful, wish there is more resources to teach us how to use it correctly.
This is a working code. You can insert it into the project and test it.
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators, FormArray, FormBuilder } from '#angular/forms';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
private addresses: string[] = ['Address 1', 'Address 2', 'Address 3'];
private form: FormGroup;
constructor(private formBuilder: FormBuilder){}
ngOnInit(){
// Init Form
this.form = new FormGroup({
'userData': new FormGroup({
'username': new FormControl(null, [Validators.required]),
'email': new FormControl(null, [Validators.required, Validators.email])
}),
'addresses': new FormArray([])
});
// If you want to insert static data in the form. You can use this block.
this.form.setValue({
'userData': {
'username': 'Vic',
'email': 'email#email.com'
},
'addresses': [] // But the address array must be empty.
});
// And if you need to insert into the form a lot of addresses.
// For example, which belong to one user and so on.
// You must use this block.
// Go through the array with addresses and insert them into the form.
this.addresses.forEach((value) => {
const control = new FormControl(value, Validators.required);
(<FormArray>this.form.get('addresses')).push(control);
});
// Or you can use more better approach. But don't forget to initialize FormBuilder.
this.form.setControl('addresses', this.formBuilder.array(this.addresses || []));
}
}
Related
I am trying to limit users to vote only once on a `forumpost`. I have been struggling with correctly using arrays to handle this functionality in angular for a while now.
Right now my code is failing when loading all my forumposts. The error occurs when building the formgroups and when there are more than 1 userId in my upVoters: string[] so I assume my arrays are wrong. any help, tip or pointing me into the right direction is much appreciated!
my idea:
add upVoters: string[] to Forumpost class.push userId into string[] when voting
compare if userId is already in the voters string[]
true => remove userId from array
false => add userId to array
it is working great until I start loading up the array with multiple userIds.after investing many hrs of research on SO and other coding blogs and similiar I couldn't find an answer that was able to help me solve my problem so I decided to ask the community for help. I found a couple articles to nested FormArrays but none I could find were able to help me with my use case or maybe I do not understand how to implement correctly
Angular 2 Typescript: TypeError: this.validator is not a function
I am defining my entity and my mapping functions in my forum.service.ts file so I can use them anywhere in my application
export class ForumPost {
id: string;
title: string;
writerId: string;
upVoters: string[];
constructor() {
this.id = '';
this.title = '';
this.writerId = '';
this.upVoters = [];
}
}
mapFormToForumPost(form: FormGroup): ForumPost {
const forumPost = form.getRawValue();
return forumPost;
}
mapForumPostToForm(forumPost: ForumPost): FormGroup {
const form = this.formBuilder.group(forumPost);
this.handleVotesFromForumPostForForm(form, forumPost.upVoters);
return form;
}
handleVotesFromObjectToForm(form: FormGroup, arrayUpVoters: string[]) {
form.removeControl('upVoters');
if (arrayUpVoters && arrayUpVoters.length === 0) {
const upVotersForForm = [];
form.addControl('upVoters', this.formBuilder.array(upVotersForForm))
} else {
const upVotersForForm = [];
for (const item of arrayUpVoters) {
upVotersForForm.push(item);
}
form.addControl('upVoters', this.formBuilder.array(upVotersForForm))
}
in my application i have a page where i use an http.get call to getAll forumposts like this. the http request is called in the ngOnInit() of the forumList.component.ts file
forumPosts: FormGroup[] = [];
constructor(private forumService: ForumService, private formBuilder: FormBuilder, private formHelper: FormHelper ) {}
loadTopics() {
this.forumService.getForumPosts(this.postsPerPage, this.currentPage)
.subscribe(response => {
for (const forumPost of response.forumPosts) {
console.log(forumPost);
this.forumPosts.push(this.formBuilder.group(forumPost));
for (const post of this.forumPosts) {
this.formHelper.disableControls(post);
}
}
this.totalPosts = response.maxPosts;
});
}
my corresponding HTML looks like this forumList.component.html
<mat-card class="outerCard" *ngIf="this.forumPosts.length > 0">
<forum-list-post
*ngFor="let forumPostForm of forumPosts | sort: 'votes'"
[forumPostForm]="forumPostForm"
></forum-list-post>
</mat-card>
following my error stacktrace with corresponding locations in my code. you can see its failing when building a formgroup via formbuilder. i have added a console log of my forumPost just before getting mapped to a formGroup.
for anyone encountering same problem here is how i resolved my problem. i decided to refactor my forum.service file by changing the mapping functions a bit. instead of using the formbuilder to build a formgroup i defined the formgroup myself inside my mapping function like this.
mapForumPostToForm(forumPost: ForumPost): FormGroup {
const newform = new FormGroup({
id: new FormControl(forumPost.id),
title: new FormControl(forumPost.title),
writerId: new FormControl(forumPost.writerId),
upVoters: this.formBuilder.array(forumPost.upVoters),
});
return newform;
}
drawback of this is i need to add fields here when i add new variables to my forumPost entity. as it is located inside my forum.service.ts file i just need to make sure not to forget when adding new variables to my entities
import { Storage } from '#ionic/storage';
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute, Route } from '#angular/router';
import { HttpClient, HttpParams } from '#angular/common/http';
import { __core_private_testing_placeholder__ } from '#angular/core/testing';
import { ModalController, NavController } from '#ionic/angular';
#Component({
selector: 'app-matchfixer',
templateUrl: './matchfixer.page.html',
styleUrls: ['./matchfixer.page.scss'],
})
export class MatchfixerPage implements OnInit {
id1: any;
id2: any;
url: string;
fixture: any[]=[];
venue: any[]=[];
venueUrl: any;
player: any[]=[];
playerUrl: any;
toss: any[]=[];
teams: any[]=[];
constructor(
private storage: Storage,
public http: HttpClient,
public navCrl: NavController
) {}
async delay(ms: number) {
await new Promise(resolve => setTimeout(()=>resolve(), ms)).then(()=>console.log('fired'));
}
goconsl(){
console.log(''+this.id1+''+this.id2);
}
goback(){
this.navCrl.pop();
this.navCrl.navigateRoot('/home');
}
fetchVenue(){
this.fixture.forEach((item,index) => {
if(item.status!='NS'){
this.venueUrl='https://cricppp.herokuapp.com/users/venue/'+item.venue_id;
this.http.get(this.venueUrl).subscribe(response => {
this.venue[index]=response["name"];
//this.venue[index]=response;
console.log('venue '+this.venue[index]+' '+index);
});
}
else
{
this.venue[index]='no venue/no match';
console.log('venue '+this.venue[index]+' '+index);
}
});
}
fetchPlayer(){
this.fixture.forEach((item,index) => {
if(item.status!='NS'){
this.playerUrl='https://cricppp.herokuapp.com/users/player/'+item.man_of_match_id;
this.http.get(this.playerUrl).subscribe(response => {
this.player[index]=response["fullname"];
console.log('player '+this.player[index]+''+index);
});
}
else
{
this.player[index]='no pop/no match';
console.log('player '+this.player[index]+''+index);
}
});
}
fetchToss(){
this.fixture.forEach((item,index) => {
if(item.status!='NS'){
this.http.get('https://cricppp.herokuapp.com/users/teams').subscribe(response => {
this.teams=response["teams"];
this.teams.forEach((itm,indx) => {
if(itm.id==item.toss_won_team_id){
this.toss[index]=itm.name;
console.log('toss '+this.toss[index]+''+index);
}
});
});
}
else
{
this.toss[index]='no toss/no match';
console.log('toss '+this.toss[index]+''+index);
}
});
}
ngOnInit() {
this.storage.get('team1Id').then(paras1 => {
this.id1=paras1;
});
this.storage.get('team2Id').then(paras2 => {
this.id2=paras2;
});
this.delay(1000).then(any=>{
this.url = 'https://cricppp.herokuapp.com/users/fixtures/'+this.id1+'/'+this.id2;
this.http.get(this.url).subscribe(response => {
this.fixture = response["allFixtures"];
console.log(this.fixture[0].note);
this.fetchVenue();
this.fetchPlayer();
this.fetchToss();
});
});
}
}
here i have 3 final arrays player[]. toss[]. & venue[]
i wanted to display them sequentially in the html using ngFor but ngFor can iterate through 1 array only
(i wanted to display them like - player[0]->toss[0]->venue[0] ------- player[1]->toss[1]->venue[1]...
and so on...
i was looking into models and/or array of objects but i cant get anything to work, i want to have a model like
export class Data{
public venue: string;
public toss: string;
public player: string;
}
but since i am new, i can't get it to work, whenever i try to assign data by using forEach it shows error this.data[index] is undefined.
(i am doing data: Data[]=[];) (i also imported the model)
i hope i am explaining correctly, i wanted to create an array where each element has 3 propertied venue, toss and player to which i can assign values.
like this.data[index]="response from API";
just like a json has, i also tried looking into array of objects but i cannot get it to work.
i also read about services but i cant get this simple thing to work so i couldn't handle service either.
please help
Firstly, you are making multiple asynchronous calls which might finish fetching data at different times.
So, identifying that you have received all the data from all the calls is tricky. You might want to use Promise.all(), or RxJS's forkJoin(),
or something similar.
Assuming all the data is fetched and is available, you can either create a new Data[] array as you wanted. Or if all those arrays are same length, then you can just iterate through all of them at the same time.
*ngFor iterates over an iterable object such as array, but you can use the index to access values from other arrays too.
For example,
<ng-container *ngFor="let value of player; let i = index">
<div>player[i]</div>
// Can use the same index on other arrays.
<div>toss[i]</div>
<div>venue[i]</div>
</ng-container>
But if arrays are of different length, then you will get undefined values.
I didn't see the code where you are pushing data to data[] array, but you could do something like
var data = [];
for (let i = 0; i < player.length; i++) {
data.push({
"venue": venue[i],
"toss": toss[i],
"player": player[i]
});
}
And then use *ngFor to iterate over this data[] array to display the values.
<ng-container *ngFor="let value of data; let i = index">
<div>data[i].player</div>
<div>data[i].toss</div>
<div>data[i].venue</div>
</ng-container>
I am trying to implement a custom validator for a form in Angular. It would be perfect if I could access the this of the controller, but it's undefined in the validator function.
This is my validator function:
validateSuccessShortName(control: AbstractControl) {
//can't read this.randomProp, because this is undefined
if (control.value.length > this.randomProp.length) {
return {value: control.value};
}
return null;
}
Here is a STACKBLITZ demonstrating the issue.
Am I doing something wrong or is this simply impossible in the Angular framework?
https://stackblitz.com/edit/ionic-tqp9o1?embed=1&file=pages/home/home.ts
And for future improvements way better move all validation in another file, and first params for each function must be config. You validators must not dependents from this just from config object
Just change on this.validateSuccessShortName.bind(this), because you function missing context
shortName: new FormControl('', [
this.validateSuccessShortName.bind(this)
])
Your angular validator doesn't reference your component property. To fix this you need to bind this to the validator.
export class HomePage {
private arr: Array<any>;
private thevalue: string;
public thisForm: FormGroup;
public randomProp: string;
constructor(
private modal: ModalController,
public navCtrl: NavController) {
this.thevalue = "Initial value";
this.randomProp = "This is a random property";
this.thisForm = new FormGroup({
shortName: new FormControl('', [
this.validateSuccessShortName.bind(this)
])
});
}
validateSuccessShortName(control: AbstractControl) {
if (control.value.length > this.randomProp.length) {
return {value: control.value};
}
return null;
}
}
Is there a way to create a reactive form basing on an existing data model with all the validation magic. In the example below author passes whole new object into a formbuilder but what I want to achieve is elegant way to tell formbuilder what field is required or needs some other validation.
https://malcoded.com/posts/angular-fundamentals-reactive-forms
export class PersonalData {
email: string = '';
mobile: string = '';
country: string = '';
}
...
createFormGroupWithBuilderAndModel(formBuilder: FormBuilder) {
return formBuilder.group({
personalData: formBuilder.group(new PersonalData()),
requestType: '',
text: ''
});
}
I just want to skip the process of assigning a FormControl for each field in the model.
#EDIT
After some research and little hint from #xrobert35 I wanted to try and used https://www.npmjs.com/package/#rxweb/reactive-form-validators
They could be "many" way to do what you want to do, but by just extending your actual solution : Your personnal data could look like :
export class PersonalData {
email: string = ['', Validators.required];
mobile: string = ['', Validators.required, Validators.maxLength(10)];
country: string = '';
}
If you need domain base validation (for reusable purpose) you can use rxweb validations.
export class PersonalData {
#required()
email: string;
#required()
mobile: string;
#greaterThan({ fieldName: 'number2' })
number1: number;
#prop()
number2: number;
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
formGroup: FormGroup;
constructor( private validation: RxFormBuilder) {
}
ngOnInit() {
let person: PersonalData = new PersonalData();
this.formGroup = this.validation.formGroup(person);
}
}
If I understand you just want to add validators to your field.
https://angular.io/guide/form-validation
I can't be more precise to giving you the official documentation.
ngOnInit(): void {
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)
});
}
In this exemple the fields name and power are required and of course the syntaxe could differ but the flow is the same.
Does it helps you ?
#EDIT
There is the same post for your use case:
https://stackoverflow.com/a/47576916/7133262
I'm new to Angular and TypeScript and just started working on a project using MEAN stack (MongoDB, Express, Angular, Node.js).
I created this mongoose module :
import * as mongoose from 'mongoose';
var Schema = mongoose.Schema;
const entrepriseSchema = new mongoose.Schema({
name: {type: String, unique: true, required : true},
telephone: Number,
logo: String,
web_site: String,
sites: [
{site_id: {type: Schema.Types.ObjectId, ref: 'Site'}}
]
});
const Entreprise = mongoose.model('Entreprise', entrepriseSchema);
export default Entreprise;
and this is my entreprise.component.ts :
import { Component, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import { FormGroup, FormControl, Validators, FormBuilder } from '#angular/forms';
import { ActivatedRoute } from '#angular/router';
import { EntrepriseService } from '../services/entreprise.service';
import { SiteService } from '../services/site.service';
#Component({
selector: 'app-entreprise',
templateUrl: './entreprise.component.html',
styleUrls: ['./entreprise.component.scss'],
providers: [EntrepriseService, SiteService]
})
export class EntrepriseComponent implements OnInit {
entreprise = {};
sites = [];
id: String;
constructor(private entrepriseService: EntrepriseService,
private siteService: SiteService,
private http: Http,
private route: ActivatedRoute) {
this.id = route.snapshot.params['id'];
}
ngOnInit() {
this.getEntrepriseById(this.id);
//not working
//console.log(this.entreprise.name);
//console.log(this.entreprise.sites);
//this.getSitesIn(this.entreprise.sites);
}
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => this.entreprise = data,
error => console.log(error)
);
}
getSitesIn(ids) {
this.siteService.getSitesIn(ids).subscribe(
data => this.sites = data,
error => console.log(error)
);
}
}
when I try to display the properties of the returned from entreprise.component.html it works fine and displays all the properties :
<h3>{{entreprise.name}}</h3>
<div *ngFor="let site of entreprise.sites">
{{site.site_id}}
</div>
{{entreprise.logo}}
{{entreprise.web_site}}
but how can I access the same properties on the TypeScript side ?
The commented code in the EntrepriseComponent is what I'm trying to accomplish but it's not working since this.entreprise is type {} .
The Enterprise model/schema that you created in Mongoose in Node.js resides on the server side. If you want the TypeScript code on the UI to recognize the properties in Enterprise, you will have to create a class in your angular codebase.
Create a folder named, say, models at the same level as your services folder. (Optional)
Create two files named site.ts and enterprise.ts in the models folder created in the previous step (You can put these file at a different location if you want) with the following contents:
site.ts
export interface Site {
site_id?: string;
}
enterprise.ts
import { Site } from './site';
export interface Enterprise {
name?: string;
telephone?: string;
logo?: string;
web_site?: string;
sites?: Site[];
}
Now, inside the EntrepriseComponent file, add the following imports
import { Enterprise} from '../models/entreprise';
import { Site } from '../models/site';
And change the first lines inside the EntrepriseComponent file to
export class EntrepriseComponent implements OnInit {
entreprise: Enterprise = {};
sites: Site[] = [];
Now, the enterprise attribute will be of type Enterprise and you will be able to access the properties that we declared in the enterprise.ts file.
Update:
Also, you cannot console.log(this.enterprise.name) immediately after this.getEntrepriseById(this.id); in your ngOnInit() function. This is because the web service you are making to get the enterprise object would not have resolved when you are trying to log it to the console.
If you want to see the enterprise object in the console or you want to run some code that needs to run after the service call has resolved and the this.enterprise object has a value, the best place to do this would be your getEntrepriseById function. Change the getEntrepriseById function to
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => {
this.enterprise = data;
console.log(this.enterprise.name);
// Any code to run after this.enterprise resolves can go here.
},
error => console.log(error)
);
}