Angular 2: Is there an easy way using FormBuilder with FormArray in ng2? - forms

Situation
I'm working on a form where I want to list some mails having a checkbox besides the subject and a "check all" checkbox in the same column as the other checkboxes.
The form looks simply like this:
[ ] Check all
------------------------------------------
[ ] This is email subject #1
[ ] This is email subject #2
[ ] ...
When I select Check all all the below checkboxes should be selected and when I click again, all mails should be unselected.
The mails are coming dynamically into the component via an #Input and the list can change at any point of time.
So far so easy, nothing special. BUT it seems not so easy when using the FormBuilder in ng2 for that. Side note: I want to use the FormBuilder to test my code less end-to-end but more with unit tests.
Current code
Template
<form [formGroup]="form">
<div><input formControlName="toggleAll" type="checkbox"></div>
<div>
<ul formArrayName="checkMailList">
<li *ngFor="let mail of mails; let i=index">
<input type="checkbox" [formControlName]="i">
<div>mail.subject</div>
</li>
</ul>
</div>
</form>
Component
#Component({ ... })
export class MailListComponent implements OnChanges {
#Input() mails: Mail[];
private get checkMailList(): FormArray { return this.form.get('checkMailList') as FormArray; }
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
checkMailList: this.fb.array([]);
});
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['mails'] || !changes['mails'].currentValue) {
return;
}
// remove array first from form, as we will get
// mails again when anything updates
if (this.checkMailList.length > 0) {
this.form.removeControl('checkMailList');
this.form.addControl('checkMailList', this.fb.array([]));
}
this.mails.forEach(m => {
this.checkMailList.push(this.fb.control());
});
this.form
.valueChanges
.pluck('toggleAll')
.distinctUntilChanged()
.subscribe(selectAll => {
if (selectAll) {
this.checkMailList.setValue(this.mails.map(_ => true));
} else {
this.checkMailList.reset();
}
});
}
}
Problem
I think that there could occur a race condition/timing issue: I iterate over the mails array provided by #Input but I wire the checkMailList manually in the template to the corresponding index. I iterate over all mails whenever the #Input changes. I don't know if Angular first iterates over all mails in the template and then runs the ngOnChange method or vice versa. Can anyone can give me a profounded answer here?
Forms are the fundamental part of every WebApp. Am I doing it right? Any help would be appreciated.

Related

What is the best way to post form with multiple components using Vue js

as I'm on my Vue spree (started recently but so far I'm really enjoying learning this framework) couple of questions rised up. One of which is how to post form from multiple components. So before I continue forward I wanted to ask you what are you thinking about this way of structuring and point me in right direction if I'm wrong.
Here it goes.
I'm working on a SPA project using ASP.NET CORE 2.1 and Vue JS Template (with webpack)(https://github.com/MarkPieszak/aspnetcore-Vue-starter) and my project is structured in several containers, something like this:
In my app-root i registered several containers
<template>
<div id="app" class="container">
<app-first-container></app-first-container>
<app-second-container></app-second-container>
<!--<app-third-container></app-third-container>-->
<app-calculate-container></app-calculate-container>
<app-result-container></app-result-container>
</div>
</template>
<script>
// imported templates
import firstContainer from './first-container'
import secondContainer from './second-container'
import calculateContainer from './calculateButton-container'
//import thirdContainer from './third-container'
import resultContainer from './result-container'
export default {
components: {
'app-first-container': firstContainer,
'app-second-container': secondContainer,
// 'app-third-container': thirdContainer,
'app-calculate-container': calculateContainer,
'app-result-container': resultContainer
}
}
</script>
In my first container I'm having several dropdowns and two input fields with my script file where I'm fetching data from API and filling dropdowns and input fields with fetched data.
Something like this ( entered some dummy code for demonstration)
<template>
<div>
<h1>Crops table</h1>
<p>This component demonstrates fetching data from the server. {{dataMessage}}</p>
<div class="form-row">
<div class="form-group col-md-6">
<label for="exampleFormControlSelect1" class="col-form-label-sm font-weight-bold">1. Some text</label>
<select class="form-control" id="exampleFormControlSelect1" v-model="pickedCropType" #change="getCropsByType()">
<option v-for="(cropType, index) in cropTypes" :key="index" :value="cropType.id" :data-imagesrc="cropType.imgPath">{{ cropType.name }}</option>
</select>
</div>
<div class="form-group col-md-6">
<label for="exampleFormControlSelect2" class="col-form-label-sm font-weight-bold">2. Some text</label>
<select class="form-control" id="exampleFormControlSelect2">
<option v-for="(crop, index) in cropSelectList" :key="index" :value="crop.id">{{ crop.name }}</option>
</select>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
data() {
return {
cropTypes: null,
cropSelectList: null,
crops: null,
pickedCropType: null,
}
},
methods: {
loadPage: async function () {
try {
//Get crop types and create a new array with crop types with an added imgPath property
var cropTypesFinal = [];
let responseCropTypes = await this.$http.get(`http://localhost:8006/api/someData`);
responseCropTypes.data.data.forEach(function (element) {
cropTypesFinal.push(tmpType);
});
} catch (err) {
window.alert(err)
console.log(err)
}
},
getCropsByType: async function () {
//Get crops by crop type
let responseCrops = await this.$http.get(`http://localhost:8006/api/crop/Type/${this.pickedCropType}`);
var responseCropsData = responseCrops.data.data;
this.cropSelectList = responseCropsData;
}
},
async created() {
this.loadPage()
}
}
</script>
And in my second container I have different dropdowns and different input fields with different scripts etc.
So, my questions are:
1.) I'm having required data form field in first container and in second container I'm having additional data and my submit button is separated in third container (app-result-container). So, is this proper and logical way of structuring containers if not can you point me in right direction?
2.) Is it smart to input script tag in every container where I'm processing/fetching/submitting some data for that particular container? Should I put scripts tag in separated file and keep structure clean, separating html from js file.
Example:
import { something } from 'something'
export default {
data () {
return {
someData: 'Hello'
}
},
methods: {
consoleLogData: function (event) {
Console.log(this.someData)
}
}
}
3.) Can I send input values from one container to another (In my particular case from first and second container to app-calculate-container(third container))?
How to on submit return results container with calculated imported values
If you want components to communicate or share data with one another, you will need to either emit an event from one component up to the parent and pass it down via props, or use some kind of state management model, like Vuex, where each of your components can listen to the store.
Take a look at this code sandbox: https://codesandbox.io/s/8144oy7xy2
App.vue
<template>
<div id="app">
<child-input #input="updateName" />
<child-output :value="name" />
</div>
</template>
<script>
import ChildInput from "#/components/ChildInput.vue";
import ChildOutput from "#/components/ChildOutput.vue";
export default {
name: "App",
components: {
ChildInput,
ChildOutput
},
data() {
return {
name: ""
};
},
methods: {
updateName(e) {
this.name = e.target.value;
}
}
};
</script>
ChildInput.vue
<template>
<input type="text" #input="changeHandler">
</template>
<script>
export default {
name: "ChildInput",
methods: {
changeHandler(e) {
this.$emit("input", e);
}
}
};
</script>
ChildOutput.vue
<template>
<p>{{ value }}</p>
</template>
<script>
export default {
name: "ChildOutput",
props: {
value: {
type: String,
default: ""
}
}
};
</script>
What's going on?
The ChildInput component is a text field and on every change inside it, fires an event (emits using this.$emit() and passes the whole event up).
When this fires, App is listening to the change, which fires a method that updates the name data property.
Because name is a reactive data property and is being passed down as a prop to the ChildOutput component, the screen re-renders and is updated with the text written.
Neither ChildInput nor ChildOutput knows about one another. It's the parent that listens to the event passed to it, then passes the new prop down.
This way of working is fine and simple to understand, but I would strongly recommend looking at Vuex, as this method can get messy and complicated when you go beyond trivial tasks.

Angular 4 Create Dynamic formArray inside array using reactive forms

Here, we are creating dynamically form array's inside array.
Below is the sample structure of expected result given below.
{
"optionsRadios": null,
"Package_Title": null,
"HotelData": [
{
"Htitle": "",
"HDescription": "",
"hotelStar": "",
"RoomData": [
{
"Hotel_Room_Type": ""
},
{
"Hotel_Room_Type": ""
}
]
}
]
}
I want to create HotelData Dynamically, within that HotelData array i want to create RoomData array fields also dynamically.
I created HotelData fields by the following codes:
export class AddPackageComponent implements OnInit {
ngOnInit() {
this.invoiceForm = this._formBuild.group({
Package_Title: [],
HotelData: this._formBuild.array([this.addRows()])
})
}
addRows() {
return this._formBuild.group({
Htitle: [''],
HDescription: [''],
hotelStar: ['']
});
}
addHotel() {
const control: FormArray = this.invoiceForm.get(`HotelData`) as FormArray;
control.push(this.addRows());
}
}
You are on the right track, we just need to add some more code...
addRows need the form array RoomData, and here we also initially push an empty form group of room. If you don't want that, modify it.
addRows() {
let group = this._formBuild.group({
...
RoomData: this._formBuild.array([])
});
// push formgroup to array initially
this.addRoom(group.controls.RoomData)
return group;
}
addRoom looks like this:
addRoom(hotel:any) {
let group = this._formBuild.group({
Hotel_Room_Type: ['']
})
hotel.push(group)
}
addRoom is also the method we are calling from template when we want to add a new room to a hotel. Remember to pass the current hotel as parameter from template.
As for adding a new hotel, your addHotel stays the way you have it now.
Then over to your template, the relevant part should look something like this:
<div formArrayName="HotelData">
<div *ngFor="let hotel of invoiceForm.get('HotelData').controls; let i = index" [formGroupName]="i" >
<!-- form controls here -->
<button (click)="addRoom(hotel.get('RoomData'))">Add Room</button>
<div formArrayName="RoomData">
<div *ngFor="let room of hotel.get('RoomData').controls; let j = index" [formGroupName]="j">
<!-- form controls here -->
</div>
</div>
</div>
</div>
Finally, here's a Demo: http://plnkr.co/edit/7tcLcnzALew3oKGenjwK?p=preview

Angular 2 / PrimeNG - Expression has changed after it was checked. Binding NgModel on last invalid form control

I'm having a problem where when the very last element in my form has a value bound to it the error "Expression has changed after it was checked." is thrown.
I will preface by saying this is based off of the Angular 2 website example here -
https://angular.io/docs/ts/latest/cookbook/dynamic-form.html#!#top
The way my app works is first I build a dynamic form with controls in my form component based off a model.
My form components html loops the questions in the model like so
<form *ngIf="showForm" [formGroup]="formGroup">
<!-- questions-->
<div *ngIf="questions.length > 0">
<div *ngFor="let question of questions">
<question [question]="question" [formGroup]="formGroup"></question>
</div>
</div>
<button pButton type="submit" label="Submit" icon="fa-check-circle-o" iconPos="left"
[disabled]="!formGroup.valid" (click)="submitFinalForm()"></button>
</form>
Below is the question component html that uses the data that was passed in from the form component to display certain types of questions via ngSwitch
<label [attr.for]="question.field">
{{ question.question }}
</label>
<div [ngSwitch]="question.type">
<!-- Radio / Checkbox -->
<radio-checkbox-question *ngSwitchCase="1" [formGroup]="formGroup" [question]="question"></radio-checkbox-question>
</div>
Finally here is the radio-checkbox-question component
<div *ngIf="showQuestion" [formGroup]="formGroup">
<!-- Radio -->
<div *ngIf="model.radiocheckbox == 'radio'">
<div *ngFor="let label of model.labels; let i = index;">
<p-radioButton name="{{model.field}}"
value="{{i}}"
label="{{label}}"
formControlName="{{model.field}}"
[(ngModel)]="questionAnswerRadio"></p-radioButton>
</div>
</div>
</div>
Here is the actual component TS
import { Component, Input, OnInit } from "#angular/core";
import { FormGroup } from "#angular/forms";
import { RadioCheckboxQuestion } from "../Questions/radio.checkbox.question.model";
#Component({
selector: "radio-checkbox-question",
templateUrl: "radio.checkbox.component.html"
})
export class RadioCheckboxComponent implements OnInit {
#Input() question: any;
#Input() formGroup: FormGroup;
model: RadioCheckboxQuestion = new RadioCheckboxQuestion();
showQuestion: boolean = false;
questionAnswerRadio: string = "";
ngOnInit(): void {
// question essential properties
if (this.question.hasOwnProperty("field") && this.question["field"] &&
this.question.hasOwnProperty("labels") && this.question["labels"]) {
this.model.field = this.question["field"];
this.model.labels = this.question["labels"];
// assume always radio for debugging
this.model.radiocheckbox = "radio";
// set existing answer
if (this.question.hasOwnProperty("QuestionAnswer") && this.question["QuestionAnswer"]) {
if (this.model.radiocheckbox == "radio") {
this.questionAnswerRadio = this.question["QuestionAnswer"];
}
}
this.showQuestion = true;
}
}
}
I've also seen many SO issues like the following
Angular 2 dynamic forms example with ngmodel results in "expression has changed after it was checked" which basically state that [(ngModel)] should not be used with dynamic forms, but the primeNG documentation says the components can work with model driven forms and the only way to set the answer (that I know of) is [(ngModel)]. I believe what might happen here is since I set the only question in the formGroup to a value that the formGroup becomes valid in between the change detection and causes the error
Error in ./FormComponent class FormComponent - inline template:17:48 caused by: Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.
From your template it looks like you are using both model drive (formControlName)
and template driven (ngModel).
<p-radioButton name="{{model.field}}"
value="{{i}}"
label="{{label}}"
formControlName="{{model.field}}"
[(ngModel)]="questionAnswerRadio"></p-
<radioButton>
Please select one way and try again.
I suggest you to remove the [(ngModel)]
The only way i've found to get the change detection to be happy with my multi-nested components and primeNG was to implement full change detection manually. What that basically means was in every component I had to add something like the following
import ChangeDetectorRef
constructor(private change: ChangeDetectorRef)
{}
ngOnInit() {
// code here that inits everything
this.change.markForCheck();
}
Anything less then this caused the change detection errors to pop-up in different and unique ways in the components that used primeNG.

How can I detect/watch "dirty-status" of an angular2-form in the right way?

I have a form in Angular2 (e.g)
<form id="myLovelyForm" name="myLovelyForm" #myLovelyForm="ngForm">
<label [attr.for]="myLovelyCheckbox">
<input [attr.id]="myLovelyCheckbox" type="checkbox"
[(ngModel)]="myLovelyCheckbox">
<span class="myLovelyCheckbox">myLovelyCheckbox</span>
</label>
</form>
and an animation, which should start, if the form is dirty:
<div
id="myLovelyNotification"
class="myLovelyNotification"
[#notification]="myLovelyForm.form.dirty">
.....
.....
</div>
The animation works properly if I set [#notification] = true, but my myLovelyForm.dirty does not fire, if I touch the form and change an element.
If the #notification is false, the animation stops, i.e. if the checkbox was selected before and I unselect it mistakenly and select it again, the form is not pristine (touched) but not dirty anymore, therefore the animation should stop. If I set the #notification = false manually, it works properly.
The big question is: How can I detect/watch "dirty-status" of an angular2-form in the right way?
Simply -
#ViewChild('f') templateForm: any;
ngOnInit() {
this.templateForm.valueChanges.subscribe((value: any) => {
if (this.templateForm.dirty) {
console.log('template form dirty - yes: ', value);
} else {
console.log('template form dirty - no: ');
}
});
}
Where your template contains:
<form #f="ngForm" (ngSubmit)="save(f)>
...
</form>
However this is still using template forms which are really there to help bridge the gap with Angular1 apps. Model Driven forms are the Angular 2 way of doing it for anything but real basic applications. See for example:
https://blog.thoughtram.io/angular/2016/06/22/model-driven-forms-in-angular-2.html
http://blog.ng-book.com/the-ultimate-guide-to-forms-in-angular-2/
And use custom components to really extend and excell your app - https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html
You can subscribe to form changes:
this.physicalForm.valueChanges
.map((value) => {
return value;
})
.subscribe((value) => {
if(this.selectedPhysical.weight != this.physicalForm.value.weight) {
this.selectedPhysical.weight = this.physicalForm.value.weight;
}
this.isDirty == this.physicalForm.touched;
});
If this event fires, then you know your form is dirty.
this is an example from my actual app (nut.abbr is the formcontrolName):
ngOnInit() {
for (let nut of this.userSettings.nutrientData) {
this.foodSettingsForm.controls[nut.abbr].valueChanges
.subscribe(v => { console.log("value: ", v); this.completeValueChange(nut.abbr, v); });
}
}
completeValueChange(field: string, value: boolean) {
this.isChanged = true;
Nutrient.updateNutrient(field, value, this.userSettings.nutrientData);
}

Angularjs check if section in the form is valid

I want to check my angular form validity with a little tweak,
I have a form builded dynamically with directives involved, Now the form has more than one page to it, so i play with ng-hide/ng-show when i move from page to page...
All i want to do is to check the validity of the first chunk of form inputs, for example:
I have 3 pages, 3 questions in every 'page', before the user can go to the next page, it should check for validation on the three inputs, and only than! he can move to the next one...
on my form tag i have 'novalidate' so i must do all the validations myself...
What you're after is ng-form
You can't nest HTML <form></form> tags but you can with ng-form to split your form into sections.
i.e.
<form name="myForm">
<ng-form name="subForm1">
<input name="txtField1" type="text" ng-model="Field1" ng-maxlength="50" required>
</ng-form>
<ng-form name="subForm2">
<input name="txtField2" type="text" ng-model="Field2" ng-maxlength="10" required>
</ng-form>
<button type="button1" ng-disabled="subForm1.$invalid" ng-click="doSomething() ">Button 1</button>
<button type="button1" ng-disabled="subForm2.$invalid" ng-click="doSomething()" >Button 2</button>
<button type="button3" ng-disabled="myForm.$invalid" ng-click="doSomething()" >Button 3</button>
</form>
In this instance button1 and button2 are disabled on parts of the form where as button3 is disabled based on the whole forms input
Source: https://docs.angularjs.org/api/ng/directive/ngForm
You can use the Angular's form element property $dirty, or you could check if the element you want to validate has the class ng-dirty.
If you'd like, read more here, it explains how to use and check this.
Angular JS has some pretty features which you can take advantage of especially the class .ng-valid and .ng-invalid. As the user fills your form, angular dose a real time update on the state of form fields by changing the classList to correspond to the current state of the form.
Any for field that is has been altered and does not pass the Angular validation will have a class .ng-invalid well all classes that passed the validation will have .ng-valid. While ng-pristine indicates that the form have not been modified ng-dirty tells you that the form has been modified. Not that ng-pristine and ng-dirty cannot be used to ascertain the validity of the field.
Meanwhile for your case I have created a CodePen
angular.module("paged", [])
.controller("FormCtrl", function($scope) {
$scope.form = {page: 1};
$scope.canShow = function(i) {
return (i === $scope.form.page);
};
$scope.submit = function(form) {
alert("Form Submitted", form);
};
$scope.gotoPage = function(pageTo) {
var show = false;
var els = document.getElementsByTagName("input"); //Just with input only to keep it simple
for (var i = 0; i < els.length; i++) {
if (els[i].hasAttribute("data-page") && els[i].getAttribute("data-page") == pageTo - 1) {
if (els[i].classList.contains("ng-invalid")) {
show = false;
break;
}
}
show = true;
}
if (show) {
$scope.form.page = pageTo;
if (pageTo == 4) {
$scope.submit($scope.form);
}
}
}
});
to show you how this can done. As someone will rightfully say, there may ways to kill a rat. I think this is one of them