Angular2 dynamically generating Reactive forms - html

I have a concept question and would like some advice.
So I have a component, myFormArray, which is a reactive form. It takes in an input array, and creates a number of FormControls accordingly.
#Component({
selector: 'myFormArray',
templateUrl: 'formarray.component.html'
})
export class FormArrayComponent{
#Input() classFields: any[];
userForm = new FormGroup();
ngOnInit(){
// psuedocode here, but I know how to implement
for (# of entries in classFields)
userForm.FormArray.push(new FormControl());
}
Now, in my parent html, I will be dynamically generating multiple myFormArrays. If that is confusing, assume I'm doing this:
<myFormArray [classFields] = "element.subArray"/>
<myFormArray [classFields] = "element2.subArray"/>
<button (click) = "save()"> //I don't know how to implement this!
And at the very end of the page, I want a save button, that can somehow grab all the values the user inputs in to all the forms, and pushes all this data to an array in a Service component. I'm not sure exactly how to do this part. Note that I don't want individual submit buttons for each dynamically generated form component.
How would I implement this functionality? Thanks!

Your start is good, but you have to write your source code differently.
Instead of this example app.components.ts is main component and my-array.component.ts is child component.
Our test data
classFields1: any[] = ['firstname', 'lastname', 'email', 'password'];
classFields2: any[] = ['country', 'city', 'street', 'zipcode'];
Step 1. Use FormBuilder for form creation (app.component.ts)
You must import FormBuilder and FormGroup from #angular/forms like this:
import { FormBuilder, FormGroup } from '#angular/forms';
and then define in constructor:
constructor(private formBuilder: FormBuilder) { }
Step 2. Define new empty FormGrooup
Now you can define new empty FormGroup in ngOnInit like this:
ngOnInit() {
this.myForm = this.formBuilder.group({});
}
Step 3. Create FormControls dynamically (app.component.ts)
Now you can start with dynamically creation of your FormControls by iteration of classFields. For this I would recommend to create own function. This function gets two parameter: arrayName and classFields. With arrayName we can set custom name of our FormArray-control. classFields-Array we will use for iteration. We create constant variable for empty FormArray, which we called arrayControls. After this we iterate over classFields and create for each element FormControl, which we called control, and push this control into arrayControls. At the end of this function we add our arrayControls to our Form with custom name by using arrayName. Here is an example:
createDynamicArrayControls(arrayName: string, classFields: any[]) {
const defaultValue = null;
const arrayControls: FormArray = this.formBuilder.array([]);
classFields.forEach(classField => {
const control = this.formBuilder.control(defaultValue, Validators.required);
arrayControls.push(control);
})
this.myForm.addControl(arrayName, arrayControls);
}
Import FormControl and FormArray from #angular/forms. Your import line should be like this:
import { FormBuilder, FormGroup, FormArray, FormControl } from '#angular/forms';
Now call createDynamicFormControls-Function in ngOnInit.
Step 4. HTML Template for this dynamic Form (app.component.html)
For this example I create following template:
<h1>My Form</h1>
<form [formGroup]="myForm">
<div formGroupName="test1">
<app-my-array [classFields]="classFields1" [arrayFormName]="myForm.controls.test1"></app-my-array>
</div>
<div formGroupName="test2">
<app-my-array [classFields]="classFields2" [arrayFormName]="myForm.controls.test2"></app-my-array>
</div>
<button type="button" (click)="saveForm()">Submit</button>
</form>
Here we have new div element with formGroupName. This group name is our arrayName in our form. We give our form arrays via #Input to my-array.component.
Step 5. MyArrayComponent
Now this component is very simnple:
import { Component, OnInit, Input } from '#angular/core';
import { FormGroup } from '#angular/forms';
#Component({
selector: 'app-my-array',
templateUrl: './my-array.component.html',
styleUrls: ['./my-array.component.css']
})
export class MyArrayComponent implements OnInit {
#Input() classFields: any[];
#Input() arrayFormName: FormGroup;
constructor() { }
ngOnInit() { }
}
Here we have only two #Input varibales. (I know, this variable can have a better names :-) ).
Step 6. HTML for MyArrayComponent
<div [formGroup]="arrayFormName">
<div *ngFor="let class of arrayFormName.controls; let index = index;">
<label [for]="classFields[index]">{{ classFields[index] }}</label>
<input type="text" [id]="classFields[index]" [formControlName]="index" />
</div>
</div>
<br>
And here is working example on Stackblitz: https://stackblitz.com/edit/angular-wawsja
If you have some question ask me in comments or read the Angular documentation abour Reactive Forms here.

Related

Why ngfor directive is not working, though I have created proper object in Typescript class

I have created proper Ingredient object in Typesript, also proper "model" for the object. Though ngfor directive is not working. I am getting this error "NG0303: Can't bind to 'ngforOf' since it isn't a known property of 'a'" on inspecting in browser.
My model code
export class Ingredient {
constructor(public name: string, public amount: number){}
}
My TypeScript code
import { Component, OnInit } from '#angular/core';
import { Ingredient } from '../shared/ingredient.model';
#Component({
selector: 'app-shopping-list',
templateUrl: './shopping-list.component.html',
styleUrls: ['./shopping-list.component.css']
})
export class ShoppingListComponent implements OnInit {
ingredients: Ingredient[] = [
new Ingredient ("Apple", 5),
new Ingredient ("Tomato", 5)
];
constructor() { }
ngOnInit(): void {
}
}
My HTML code
<div class="row">
<div class="col-xs-10">
<app-shopping-edit></app-shopping-edit>
<hr>
<ul class = "list-group">
<a class = "list-group-item"
style = "cursor : pointer"
*ngfor = "let ingredient of ingredients">
{{ ingredient.name }}({{ ingredient.amount}})
</a>
</ul>
</div>
</div>
Page is loading properly, but ingredients are not loading. On inspecting in browser this error "NG0303: Can't bind to 'ngforOf' since it isn't a known property of 'a'" is coming.
Can someone please help.
Try moving the *ngFor inside the ul tag.
Edit: You have a typo.. It's *ngFor="", not *ngfor="".
If you are inside AppModule check if you have BroswerModule in the imports array there.
If you are in some different module then check if CommonModule is a part of that module's array. CommonModule provides us with core Angular features like *ngIf or *ngFor in your specific case.
Also watch out, you typed *ngfor instead *ngFor.

angular : passing the input value to the component

I want to affect the value of this input to a variable 'numToAdd' which is in my component and then add 1 to the variable 'numToAdd', but I can't pass the value of the html input to my component variable 'numToAdd'. How to bind my html input to a variable in the component ?
my html code
<input type="number" id=num class=num>
my component
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-afficher-projets',
templateUrl: './afficher-projets.component.html',
styleUrls: ['./afficher-projets.component.css']
})
export class AfficherProjetsComponent implements OnInit {
ngOnInit(): void {}
numToAdd: number;
constructor() { }
add: void {
this.numToAdd++;
}
}
Two-way binding a primary functionality of angular. Use the ngModel directive to bind input to variables.
<input type="number" id="num" class="num" [(ngModel)]="numToAdd">
The above code binds the variable numToAdd with the input. Find more details here

How to Show Placeholder if *ngFor Let Object of Objects from HTML Binding returns an empty array

I display data from a template that I create in another component (team-announcements.component)... in the main team.component.ts I use the selector for team-announcements, and add the [announcement]="announcements" and ngFor="let announcement of announcements" to load the data. If the array returns no data IE there are no announcements, how can I display a placeholder like "No Announcements"?
This is where I load the announcements in team.component.html. The data is served through an API service and is retrieved in "team.component.ts", the HTML for the objects in question is below.
team.component.ts (get announcement functions):
getAnnouncements() {
this.teamsService.getTeamAnnouncements(this.team.slug)
.subscribe(announcements => this.announcements = announcements);
console.log("announcements", this.announcements);
}
team.component.html
<div class="team-announcement">
<div class="announcement-title">Message of the Day</div>
<app-team-announcements
[announcement]="announcement"
*ngFor="let announcement of announcements">
</app-team-announcements>
</div>
This is how "app-team-announcements" above is templated in a separate file, "team-announcement.component.html" and is exported, and then used in the above code...
team-announcements.component.ts
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '#angular/core';
import { Team, Announcement, User, UserService } from '../core';
import { Subscription } from 'rxjs';
#Component({
selector: 'app-team-announcements',
templateUrl: './team-announcement.component.html'
})
export class TeamAnnouncementComponent implements OnInit, OnDestroy {
constructor(
private userService: UserService
) {}
private subscription: Subscription;
#Input() announcement: Announcement;
#Output() deleteAnnouncement = new EventEmitter<boolean>();
canModify: boolean;
ngOnInit() {
// Load the current user's data
this.subscription = this.userService.currentUser.subscribe(
(userData: User) => {
this.canModify = (userData.username === this.announcement.author.username);
}
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
team-announcements.component.html
<div class="announcement-text">
{{announcement.body}}
</div>
I am unsure of how or where to "If" check the array length to display a placeholder. Can anyone help?
If you want to hide it and display something else instead you can use the else property from *ngIf:
<div class="team-announcement">
<div class="announcement-title">Message of the Day</div>
<ng-container *ngIf="announcements.length != 0; else emptyArray">
<app-team-announcements
[announcement]="announcement"
*ngFor="let announcement of announcements">
</app-team-announcements>
</ng-container>
</div>
<ng-template #emptyArray>No announcements...</ng-template>
When you want an element with *ngFor to depend on a condition (*ngIf), a good alternative is to nest the element with an *ngFor in a <ng-container> with an *ngIf. A good thing about <ng-container> is that it wont actually be part of the DOM but will obey the *ngIf.
You could insert a div wich is only displayed when your array is empty:
<div class="team-announcement">
<div class="announcement-title">Message of the Day</div>
<app-team-announcements
[announcement]="announcement"
*ngFor="let announcement of announcements">
</app-team-announcements>
<div *ngIf="announcements.length===0"> No announcements </div>
</div>
Edit: Corrected the errors

How to prevent html injection in Angular 5

I want to prevent users to enter html injection to textbox. I have researched some example but they are generally about allowing html tags through pipes and saying angular automatically sanitizes html tags. But in my example when I enter <script>alert('blabla')</script> to text box, it is accepted and registered to db like this..
How can I prevent it ?
My template code is:
<div fxLayout="row">
<mat-form-field fxFlex="20">
<input matInput name="label" [(ngModel)]="product.label" placeholder="Label"
required>
</mat-form-field>
</div>
and my ts file is:
import { Product } from '../../../';
#Component({
selector: '....',
templateUrl: '....',
styleUrls: ['....']
})
export class ProductEditComponent implements OnInit {
product: Product = <Product>{};
constructor(some services etc.) {
}
ngOnInit() {
//some code
}
Note again: I want to prevent entering html script injection, not allowing it with bypass or pipe...
you could use DomSanitizer for that
import { DomSanitizer } from "#angular/platform-browser"
import { SecurityContext } from "#angular/core";
constructor(private sanit:DomSanitizer){
var dom = this.sanitizer.sanitize(SecurityContext.HTML, "<script> alert('HELLO'); </script>");
console.log(dom);
}
if it returns null, then it was an issue with html,else it returns passed html

very basic angular2 form not getting validated

I have a very basic form consisting of a input field and a button. I am trying to use angular2 validators to show an error message when anything other than numbers are entered into the input field and also to disable the submit button when the input field is invalid or empty. For some reason the error message shows regardless of what gets entered... Any idea what i'm doing wrong?
my code:
app.component.html:
<div class="row">
<div class="col-md-4 col-md-push-4">
<form [formGroup]="barcodeForm" role="form" (ngSubmit)="submitBarcode(barcode)" >
<div class="form-group">
<input type="number" class="form-control" id="barcode" placeholder="Enter Barcode" [formControl]="barcodeForm.controls['barcode']" [(ngModel)]="barcode" name="barcode" #focusInput>
<div [hidden]="barcode.valid || barcode.pristine" class="alert alert-danger">A barcode can only consist of numbers (0-9)...</div>
</div>
<button class="btn btn-submit btn-block" [disabled]="!(barcode.valid) || barcode.pristine">Submit</button>
</form>
</div>
</div>
part of app.component.ts:
import { Component, ViewChild, ElementRef, AfterViewInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import { RestService } from "./services/rest.service";
import { ProductModel } from "./models/product.model";
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
#ViewChild('focusInput') focusInput: ElementRef;
barcode: string;
barcodeForm: FormGroup;
product: ProductModel;
constructor(fb: FormBuilder, private restService: RestService){
this.barcodeForm = fb.group({
'barcode':['', [Validators.required, Validators.pattern("[0-9]+")]]
});
}
In angular2 there are two types of forms: template driven and model driven.
Model driven is defining the form's structure in code and binding inputs to its controls (via formControl and formGroup).
And template driven is using ngModel and defining validators on the template.
I can see most of your code is targeted for model driven which in my opinion is better anyway, but you still have ngModel on your input, do you need it for something?
I dont see you using it anywhere other than barcode.valid which shouldn't work since barcode is merely a string. You want to bind the disabled property to barcodeForms.controls['barcode'].valid instead and then remove the use of ngModel. It might conflict with formControl since both of them initialize a FormControl instance for that element.