I have created a dynamic form field that I add and remove through a callback function on buttons DelBtn() to remove item and AddBtn() to add item
Each of this form fields have a value input
I also have a totalval field. I expect the sum of values in all the value field not to exceed the totalval. If it does we display an a error message and if the value equals we make the reason form field appear.
Example:
If I have totalValue = 100 . Now lets say I have my first form field value = 90.
Then I duplicate the form and in the next field set value = 10 then the reason field should appear because 90 + 10 = 100.
As the totalValue has been reached the reason form field should appear and the add button should be disabled.
If in the second attempt if the user tries to enter a value more than 10 then an error message should be shown.
Below is my current code
In my TS File I have
ischecks: boolean = true;
formsArray = [""];
count: number = 0;
totalval: number = 100;
ngOnInit(): void {}
constructor() {}
clickCount(): void {
this.count++;
}
DelBtn = delIndex => this.formsArray.splice(delIndex, 1);
AddBtn = () => this.formsArray.push("");
HTML
<h2> Form</h2>
<pre style="font-weight: bolder;font-family:verdana;margin-left: 35px;">Total Value:{{totalval}} </pre>
<div *ngFor="let i of formsArray; let a = index">
<div>
<form>
<table>
<div>
<label for="fname">Value:</label><br>
<input type="text" id="fname" name="fname" ><br>
<tr>
<td>
<button type="button" class="btn btn-outline-success"
style="border-radius:40px;margin-left: 50px" (click)="DelBtn(a)" ><span class="fa fa-plus"></span>Delete</button>
</td>
</tr>
<tr>
</div>
</table>
</form>
</div>
</div>
<div *ngIf=ischecks style="margin-left:35%">
<label for="fname">Reason:</label><br>
<input type="text" id="fname" name="fname" ><br>
</div>
<br>
<button type="button" class="btn btn-outline-success"
style="border-radius:40px;margin-left: 50px;margin-bottom: 30%;" (click)="AddBtn()" ><span class="fa fa-plus"></span>Add</button>
https://stackblitz.com/edit/angular-ivy-3pbdwv?file=src%2Fapp%2Fapp.component.html
Note: If you do not understand the question feel free to ask me in comments adn I am working in angular(typescript)
This will be difficult to achieve with basic Javascript. Below approach uses ReactiveForm approach
We Follow the below steps
Add the ReactiveFormsModule to the imports array of the module
#NgModule({
imports:[ ReactiveFormsModule, ... ],
Inject the FormBuilder class
constructor(private fb: FormBuilder) {}
Define the form
myForm = this.fb.group({
totalVal: [100],
formsArray: this.fb.array([this.fb.control("", { validators: [Validators.required] })]),
reason: ["", [Validators.required]]
}, { validators: [sumMatches] });
We have added a cusom validator sumMatches. We will use this to check whether the sum of the total value has been matched
function sumMatches(control): ValidationErrors | undefined {
const totalVal = Number(control.get("totalVal").value);
const formsArrayTotal = control
.get("formsArray")
.value.reduce((a, b) => Number(a) + Number(b), 0);
if (formsArrayTotal !== totalVal) {
return {
sumMismatch: true
};
}
return;
}
Next we define helper getter functions to extract properties from the formGroup
get sumMismatch(): boolean {
return this.myForm.hasError('sumMismatch')
}
get arrayFullyFilled() {
return !this.formsArray.controls.some(item => item.errors)
}
get formsArray() {
return this.myForm.get("formsArray") as FormArray;
}
get totalVal() {
return this.myForm.get("totalVal") as FormControl;
}
We also need to amend the functions to add and remove items from the formArray
DelBtn = delIndex => this.formsArray.controls.splice(delIndex, 1);
AddBtn = () => this.formsArray.push(this.fb.control(""));
Finally we can implement the formGroup in the html
<h2> Form</h2>
<span class='totalVal'>Total Value:{{ totalVal.value }}</span>
<form [formGroup]='myForm'>
<ng-container formArrayName='formsArray'>
<table *ngFor="let item of formsArray.controls; let i = index">
<tr>
<td>
<div>
<label [attr.for]="'fname' + i">Value:</label><br>
<input type="number" [formControlName]="i" type="text" [id]="'fname' + i" name="fname" ><br>
</div>
</td>
<td>
<button type="button" class="btn btn-outline-success"
s (click)="DelBtn(i)" ><span class="fa fa-plus"></span>Delete</button></td>
<tr>
</table>
</ng-container>
<div *ngIf='!sumMismatch && arrayFullyFilled'>
<label for="fname">Reason:</label><br>
<input type="text" id="fname" name="fname" ><br>
</div>
<br>
<button type="button" class="btn btn-outline-success"
(click)="AddBtn()" ><span class="fa fa-plus"></span>Add</button>
<br>
<span class="error" *ngIf="sumMismatch && myForm.touched">Total Value Mismatch</span>
</form>
I have extracted css to own file
.totalVal {
font-weight: bolder;
font-family: verdana;
}
.btn-outline-success {
border-radius: 40px;
margin-left: 50px;
}
.error {
color: red;
}
See this Demo
Edit 1 - How Does the validator work?
To understand this we look at how we build our form group. We defined a structure that produces a value in the form
{
totalVal: 100,
formsArray: [''],
reason: ''
}
By defining our form group as this.fb.group({ ... }, {validators: [ sumMatches ] } the form group with the above value will be passed to the sumMatches function
In the sumMatches we will have a something like a formGroup with the value
{
totalVal: 100,
formsArray: ['50', '20', '10'],
reason: ''
}
In the above we simply extract the 100 from the formGroup using control.get('totalVal').value same to formArray. Since formArray value will be an array then we can use reduce function to sum this.. We finally compare this and return null if they match and an Object if they do not match.
With the above approach, angular reactive forms will update the value of the form valid status based on what is provided by the user. We can hence leverage this valid status to update the UI
arrayFullyFilled()
get arrayFullyFilled() {
return !this.formsArray.controls.some(item => item.errors)
}
The above code tries to find if the user has filled ALL the inputs in the array. In our array we get all the controls, check if some of them have errors and if any has error return false otherwise return true. This is made possible considering that in my formGroup I had made the formControls as required using Validators.required validation
Related
I have a dynamic form that allows a user to add the item they need, the quantity and the cost per item
<div class="form-group" v-for="(input,k) in inputs" :key="k">
<input type="text" class="form-control" v-model="input.item">
<input type="text" class="form-control" v-model="input.quantity">
<input type="text" class="form-control" v-model="input.cost">
<span>
<i class="fas fa-minus-circle" #click="remove(k)" v-show="k || ( !k && inputs.length > 1)">Remove</i>
<i class="fas fa-plus-circle" #click="add(k)" v-show="k == inputs.length-1">Add fields</i>
</span>
</div>
<button>Submit</button>
</form>
I'm then calculating the total cost of all the materials combined using a computed property as follows:
computed: {
totalCost: function () {
let total = 0
for (let i = 0; i < this.inputs.length; i++){
total += this.inputs[i].cost * this.inputs[i].quantity
}
return total
}
},
What, I the want to do is pass the value of the total cost to a form using the data property as follows. However the total_cost field remains undefined.
data () {
return {
inputs: [{
item: '',
quantity: '',
cost: '',
maintenance_id: this.maintenance_id,
total_cost: this.totalCost,
}],
How do I pass a computed property to a form so that it can be submitted as part of that form?
you can handle the submit by yourself like this
<button #click.prevent="onSubmit">Submit</button>
and send all the data from here.
you can acess total as this.totalCost inside onSubmit function and you do not need that total_cost data property
I have a piece of JSON which represents some actions and parameters a user can set.
I want to display the actions in a dropdown, and when a user selects one - the required parameters associated with the action are displayed.
Example:
User selects an action with a single parameter, a single input box is displayed
User selects an action with two parameters, two input boxes are displayed.
I nearly have it working but the *ngFor isn't displaying the inputs for the selected action:
In onChange - if I print this.steps, I can see that this.steps[i].SelectedAction.UIParameters has a value so I'm not sure why it isn't being rendered.
JSON:
[
{
"ActionEnum": "CLICKELEMENT",
"Name": "Click Element",
"UIParameters": [
{
"ParameterEnum": "ELEMENTID",
"Description": "The id of the element to click"
}
]
},
{
"ActionEnum": "INPUTTEXT",
"Name": "Input Text",
"Description": "Enters text into the element identified by it's id",
"UIParameters": [
{
"ParameterEnum": "ELEMENTID",
"Description": "The id of the element"
},
{
"ParameterEnum": "TEXTVALUE",
"Description": "The text to enter into the element"
}
]
}
]
Typescript:
import { Component, Output, EventEmitter, OnInit } from "#angular/core";
import { ActionService } from "../services/action-service";
import { Action } from "../models/Action";
#Component({
selector: 'app-scenario-step-editor-component',
template: `
<form #formRef="ngForm">
<div *ngFor="let step of steps; let in=index" class="col-sm-3">
<div class="form-group">
<label class="sbw_light">Action:</label><br />
<select (change)='onChange()' [(ngModel)]="step.Action" name="action_name_{{in}}">
<option *ngFor="let action of this.availableActions" [(ngModel)]="steps[in].value" name="action_name_{{in}}" class="form-control" required>
{{action.Name}}
</option>
</select>
<div *ngIf="steps[in].SelectedAction">
<label class="sbw_light">Parameters:</label><br />
<ng-template *ngFor="let parameter of steps[in].SelectedAction.UIParameters">
<label class="sbw_light">{{parameter.ParameterEnum}}</label><br />
<input (change)='onChange()' type="text" [(ngModel)]="steps[in].Parameters" name="parameter_name_{{in}}" class="form-control" #name="ngModel" required />
</ng-template>
</div>
</div>
</div>
<button id="addStepBtn" type="button" class="btn btn-light" (click)="addScenarioStep()">Add Scenario Step +</button>
</form>`
})
export class ScenarioStepEditorComponent implements OnInit {
#Output() onSelectValue = new EventEmitter<{stepInputs: any[]}>();
steps = [];
availableActions: Action[];
constructor(private actionService: ActionService) {}
ngOnInit(): void {
this.actionService.list().subscribe( result => {
this.availableActions = result;
},
error => console.log('Error getting actions...') );
}
/* When user picks an option, save the chosen action with the rest of the parameters*/
onChange() {
for (let i = 0; i < this.steps.length; i++) {
let actionIndex = this.availableActions.findIndex(a => a.Name === this.steps[i].Action);
this.steps[i].SelectedAction = this.availableActions[actionIndex];
}
this.onSelectValue.emit( {stepInputs: this.steps} );
}
addScenarioStep() {
this.steps.push({value: ''});
}
}
<ng-template *ngFor="let parameter of steps[in].SelectedAction.UIParameters">
<label class="sbw_light">{{parameter.ParameterEnum}}</label><br />
<input (change)='onChange()' type="text" [(ngModel)]="steps[in].Parameters" name="parameter_name_{{in}}" class="form-control" #name="ngModel" required />
</ng-template>
Just replace ng-template with ng-container:
<ng-container *ngFor="let parameter of steps[in].SelectedAction.UIParameters">
<label class="sbw_light">{{parameter.ParameterEnum}}</label><br />
<input (change)='onChange()' type="text" [(ngModel)]="steps[in].Parameters" name="parameter_name_{{in}}" class="form-control" #name="ngModel" required />
</ng-container>
Reasons:
ng-container was suitable for that situation. It just holds "something" and can be iterated.
ng-template defines a template. You didn't need a template here, templates are not meant to be used for that. It could work, of course, but it's not suitable for your scenario.
Read more about ng-template and ng-container here: https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/
As a final side note, you could use ng-template by defining an item and you could use ng-container with *ngTemplateOutlet to render the template. Check the above guide for some examples.
I am trying to understand and work around to validate a simple form but the problem I am facing is, when the page loads, it shows the "Error" or "Success" message, which should display only either on keypress or mouseout event.
Moreover, I cannot figure out how to validate the dropdown and finally when a user click submit, it can check all fields are filled and correct to submit the form. Following is my code and my link to JSFiddle
HTML
<div id="app">
<div>
<label for="name">Name</label>
<input type="text" #keypress="checkField" v-model="name">
<span v-if="checkName">Checks out </span>
<span v-if="!checkName">Pleas enter valid name</span>
</div>
<div>
<label for="Age">Age</label>
<input type="number" #keypress="checkField2" v-model="age">
<span v-if="checkAge">Enter Valid Age </span>
<span v-if="!checkAge">Not a valid age</span>
</div>
<div>
<select name="" id="">
<option disabled selected>Please Choose</option>
<option v-for="gender in genders" :value="gender">
{{gender}}
</option>
</select>
<span v-if="genderField">Please select a gender</span>
<span v-if="!genderField">Green means go</span>
</div>
<div>
<button #click="checkSubmit(e)">Submit</button>
</div>
</div>
JS
data: {
name: "",
checkName: "",
age: "",
checkAge: "",
genders : ["Male",'Female',"Other"],
genderField: ""
},
methods: {
checkField() {
if (!this.amount) {
this.checkName = true
}
},
checkGender() {
if(!this.genders){
this.genderField = true
}
},
checkSubmit(e){
//check if all fields are filled before submitting
alert("it is working")
e.preventDefault()
}
}
})
There is a lot of ways to validate forms. I have a few tips for this kind of case.
Use a form element with #submit.prevent="..." event handler. It will ensure a better user experience;
Do not use #key* event handlers to validate or format a value, instead, use #input. It will prevent you from a lot of headache;
Vue provide a API to watch all the attribute changes, not only when the user changes it.
For solve your problem, you can create a validation attribute and set its content acording the other attributes change.
See the example below:
BTW: I recommend that you take a look on vuelidate
const app = new Vue({
data() {
return {
name: null,
age: null,
gender: null,
genders: ['Male', 'Female', "Other"],
validations: {}
}
},
methods: {
submit(e) {
const keys = Object.keys(this.validations);
// required fields
const required = ['name', 'age', 'gender'];
for (const key in required) {
if (!keys.includes(required[key])) {
alert('Please, insert a ' + required[key]);
return;
}
}
for (const key in this.validations) {
if (!this.validations[key]) {
alert('Please, insert valid ' + key);
return;
}
}
alert("ok");
}
},
watch: {
name(newValue) {
this.validations.name = newValue > '';
},
age(newValue) {
this.validations.age = newValue > 0;
},
gender(newValue) {
this.validations.gender = this.genders.includes(newValue);
}
}
});
app.$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form #submit.prevent="submit">
<div>
<label for="name">Name</label>
<input type="text" v-model="name">
<span v-if="'name' in validations && validations.name">Checks out </span>
<span v-if="'name' in validations && !validations.name">Pleas enter valid name</span>
</div>
<div>
<label for="age">Age</label>
<input type="number" v-model="age">
<span v-if="'age' in validations && !validations.age">Enter Valid Age</span>
</div>
<div>
<label for="gender">Gender</label>
<select name="gender" v-model="gender">
<option disabled selected>Please Choose</option>
<option v-for="gender in genders" :value="gender">
{{gender}}
</option>
</select>
<span v-if="'gender' in validations && validations.gender">Green means go</span>
<span v-if="'gender' in validations && !validations.gender">Please select a gender</span>
</div>
<input type="submit" value="Submit">
</form>
</div>
In Angular, we have a built-in option to validate forms. But Vue offers very limited functionality when it comes to create and validate forms. To add more functionality, we have to install a separate package called Vuelidate to validate our Vue application forms.
What is Vuelidate?
According to the Vuelidate website:
“Vuelidate 2 is a simple, but powerful, lightweight model-based validation for Vue.js 3 and Vue 2.x.”
Install
npm install #vuelidate/core #vuelidate/validators
Reference:
https://aaqibqs.medium.com/learn-form-validation-in-vue-3-in-10-minutes-with-vuelidate-8929c5059e66
What am I doing wrong here. Need help.
When I click on "Add" button, the data I selected in the rows of table are becoming blank. But When I select Delete button and then click on Add button, then it not emptying of one time only. If I see console, I can see data there, but it is not showing up on the screen.
Here is my code:
html:
<div class="container">
<div class="row" style="margin-bottom: 10px;">
<div class="col">
<div class="row111">
<label for="Select Image" class="col-sm-311 col-form-label111">Add Threshold and Notification for this Inspection: </label>
<div class="col-sm-9">
</div>
</div>
</div>
<div class="col">
<div class="float-right">
<button class="btn btn-default" type="button" (click)="addNotification()">Add</button>
</div>
</div>
</div>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>#</th>
<th>Threshold</th>
<th>Notification Level</th>
<th>Message</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<!-- <tr> -->
<tr *ngFor="let notification of notificationArray; let i = index">
<td>{{i+1}}</td>
<td>
<select class="form-control" maxlength="50" readonly
required
id="threshold"
name="notification.thresholdId"
[(ngModel)]="notification.thresholdId"
#threshold="ngModel" >
<option value="0" selected> Select </option>
<option *ngFor="let threshold of thresholdList" value="{{threshold.thresholdId}}" >
{{threshold.threshold}}
</option>
</select>
</td>
<td>
<input [(ngModel)]="notification.notificationLevel" required class="form-control" type="text" name="notification.notificationLevel" />
</td>
<td>
<input [(ngModel)]="notification.message" required class="form-control" type="text" name="notification.message" />
</td>
<td>
<button class="btn btn-default" type="button" (click)="deleteNotification(i)">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
component.ts:
notificationArray: Array<NotificationLevel> = [];
newNotification: any = {};
ngOnInit(): void {
this.newNotification = {thresholdId: "0", notificationLevel: "", message: ""};
this.notificationArray.push(this.newNotification);
}
addNotification(index) {
this.newNotification = {thresholdId: "0", notificationLevel: "", message: ""};
this.notificationArray.push(this.newNotification);
console.log(this.notificationArray); // I can see all entered data in console
return true;
}
deleteNotification(index) {
if(this.notificationArray.length ==1) {
alert("At least one Notification Required for an Inspection");
return false;
} else {
this.notificationArray.splice(index, 1);
return true;
}
}
Ok, solve the problem:-
The main problem is in name="" attribute. name="" must differ in each row .We can solve it in two ways:
1) Either Change the name="" attributes name dynamically so that for every row it has a different name, i did it to add index number with it as
<input [(ngModel)]="notification.notificationLevel" name="notificationThreshold{{i}}" required class="form-control" type="text" />
2) Or avoid name="" and so We can avoid [(ngModel)] By [value] and (input) and can use as like :-
<input [value]="notification.notificationLevel" (input)="notification.notificationLevel=$event.target.value" required class="form-control" type="text"/>
In fact, all of your notificationArray elements is the same instance of newNotification. By doing this : this.newNotification = {thresholdId: "0", notificationLevel: "", message: ""}; you actually reset the first line too because of it.
Try to store newNotification in a var instead :
notificationArray: Array<NotificationLevel> = [];
newNotification: any = {};
ngOnInit(): void {
var newNotification = {thresholdId: "0", notificationLevel: "", message: ""};
this.notificationArray.push(newNotification);
}
addNotification(index) {
console.log(this.notificationArray);
// here is the correction
var newNotification = {thresholdId: "0", notificationLevel: "", message: ""};
this.notificationArray.push(newNotification);
console.log(this.notificationArray);
return true;
}
deleteNotification(index) {
if(this.notificationArray.length ==1) {
alert("At least one Notification Required for an Inspection");
return false;
} else {
this.notificationArray.splice(index, 1);
return true;
}
}
update :
I don't know if it's related but you'll have duplicate threshold id on your select. Use your i to make it unique.
It's the same for your input names. You should add [] after the field name otherwise only the last row will be submit.
I added a console.log of you array after the push. Can you tell in comment if the first index is reset at this point ?
Of course the row is cleared because you are clearing it:
this.newNotification = {thresholdId: "0", notificationLevel: "", message: ""};
So on clicking the add button, the bound values are overridden and then you are pushing it into the array. Thus they are cleared :)
I'm a beginner angular2 programmer and studying it now.
I'm creating a form that when user clicks a employee, it shows editable form with employee's current data. But, when I click a user first time (even click the same user again), user's information doesn't come up to input value.
<!-- User list part-->
<table>
<tr *ngFor="let emp of employees" (click)="empBtn(emp.empId)">
<td>{{ emp.empName }}</td>
<td>{{ getDepartName(emp.departId) }}</td>
<td>{{ emp.empPhone }}</td>
<td>{{ emp.wages.wage }}</td>
</tr>
</table>
<!-- Editable part -->
<button (click)="addBtn()">Add</button>
<div *ngIf="modeAdd || modeEdit">
<form #form ="ngForm" (ngSubmit) = "onSubmit(form.value)">
<label>Name </label><input type="text" name="name" value="{{ eName }}" ngModel>
<label>Department </label><select name="depart" value="{{ eDepartment }}" ngModel><option *ngFor="let depart of departments">{{ depart.departName }}</option></select>
<label>Phone </label><input type="text" value="{{ ePhone }}" name="phone" ngModel>
<label>Wage </label><input type="text" value="{{ eWage }}" name="wage" ngModel>
<button type="submit">Submit</button>
</form>
</div>
But, If I click another user, it shows values inside form's input tags. I was able to solve this with setting eName, eDepartment, ePhone, eWage as two-way binding with like [(ngModule)]="eName", but I don't know why my code doesn't work correctly.
This is typescript part. empBtn is called when user click a user and it sets eName, eDepartment, ePhone, eWage. FYI, even though I declare default value on eName, eDepartment, ... it doesn't come up as well. Thank you for your consideration.
eName: string;
eDepartment: string;
ePhone: string;
eWage: number;
empBtn(empId: number): void {
console.log('click Employee ID: ', empId);
this.getEmployee(empId);
this.modeAdd = false;
this.modeEdit = true;
}
getEmployee(employeeId: number){
let selectedEmp: Employee = this.employeesObj.getEmployee(employeeId);
if(!selectedEmp){
console.log("employees: Invalid Employee ID");
}else{
console.log("employees: Get Employee info",selectedEmp.empName);
this.eName = selectedEmp.empName;
this.eDepartment = this.getDepartName(selectedEmp.departId);
this.ePhone = selectedEmp.empPhone;
this.eWage = selectedEmp.wages.wage;
}
}
I suggest you to study reactive form but with this problem.
http://blog.thoughtram.io/angular/2016/06/22/model-driven-forms-in-angular-2.html
HTML
<form [formGroup]="form" (ngSubmit) = "onSubmit(form.value)">
<label>Name </label>
<input type="text" formControlName="eName">
<label>Department </label>
<select formControlName="eDepartment">
<option *ngFor="let depart of departments">{{ depart.departName }}</option>
</select>
<label>Phone </label>
<input type="text" formControlName="ePhone">
<label>Wage </label>
<input type="text" formControlName="eWage">
<button type="submit">Submit</button>
</form>
importing modules
import { Component, OnInit} from '#angular/core';
import { FormGroup, FormControl, Validators, FormBuilder } from '#angular/forms';
On your component
form: FormGroup; // declare form as formgroup
eName: string;
eDepartment: string;
ePhone: string;
eWage: number;
constructor(private fb: FormBuilder){ }
ngOnInit() {
this.updateForm()
}
updateForm(): void {
this.form = this.fb.group({
eName: [this.eName, Validators.required],
eDepartment: [this.eDepartment, Validators.required],
ePhone: [this.ePhone, Validators.required],
eWage: [this.eWage, Validators.required],
})
}
empBtn(empId: number): void {
console.log('click Employee ID: ', empId);
this.getEmployee(empId);
this.modeAdd = false;
this.modeEdit = true;
this.updateForm();
}
getEmployee(employeeId: number){
let selectedEmp: Employee = this.employeesObj.getEmployee(employeeId);
if(!selectedEmp){
console.log("employees: Invalid Employee ID");
}else{
console.log("employees: Get Employee info",selectedEmp.empName);
this.eName = selectedEmp.empName;
this.eDepartment = this.getDepartName(selectedEmp.departId);
this.ePhone = selectedEmp.empPhone;
this.eWage = selectedEmp.wages.wage;
}
}
Last year, I'm also doing the same way you did. For me, reactive forms are better. You can also search for the FormBuilder and Validators.
Hope it helps.