Creating a single array from multiple inputs in vue application - javascript

I have a vue application that has a simple form section, for example, these two inputs:
<div class=" form-group col-lg-6">
<label>Name</label>
<input v-model="newUserName" class="form-control" type="text" name="newUserName">
</div>
<div class=" form-group col-lg-6">
<label>Email</label>
<input v-model="newUserEmail" class="form-control" type="text" name="newUserEmail">
</div>
So I have them set to their own v-models, and when I dump those in submission they indeed show the correct input values separately. the problem is, I want to use array.push or something similar so that, when the submit function is hit, it pushes them into a single 'details' array.
So for the example below, i want to push name and email to the details array and only show the array with both values in the console
data() {
return {
details: [],
newUserName:'',
newUserEmail: '',
}
},
methods: {
showDetails() {
let data = {
details: this.details
};
console.log(data);
}
}

Well, you can just push them to the array. When clicking on the submit button you can call some method submit() for example:
submit() {
this.details.push(this.newUserName)
this.details.push(this.newUserEmail)
console.log(this.details)
}
If you want to reset it on every submit you can just add this.details = [] in the begining of the method

Related

Reactive form validation for dynamic and hidden fields

First off, I have an Angular reactive form that has a button that can add another FormArray to the form. All validation good and working as expected. Things have gotten a little tricky when introducing another dynamic control to the already dynamic form group. This control is shown/hidden based on a selection made in another form control.
When the control is shown I introduce validation, when the control is hidden the validation is cleared. This ensures that my form remains valid/invalid correctly.
Its acting a little buggy e.g. when I complete a group of inputs and add another dynamic group of inputs, both triggering the hidden control... then to amend the previous hidden input - the form remains true. e.g.
Selecting 123 triggers the "Other Code" control, removing the value should make the form invalid, but it stays true at this point.
I have a change function assigned to the select to determine whats been selected.
selectClick(x) {
const f = this.form;
let items = this.form.get('justificationItems') as FormArray;
if (x === '123') {
items.controls.forEach(i => {
console.log(i)
i['controls'].other.setValidators([Validators.required]);
// i['controls'].other.updateValueAndValidity();
});
} else {
items.controls.forEach(i => {
i['controls'].other.clearValidators();
// i['controls'].other.updateValueAndValidity();
});
}
f.updateValueAndValidity();
}
I suspect when changing the select property to trigger the above it does not do it to the correct index item, and it does it for all?
StackBlitz - https://stackblitz.com/edit/angular-prepopulate-dynamic-reactive-form-array-ufxsf9?file=src/app/app.component.ts
the best way to "clear/add Validators" really is enabled or disabled the formControls. Remember a formControl has as status one of this:
type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
So we can simple enabled/disabled the FormControl. Futhermore, when we create the formGroup we can created disabled, so at first will be not INVALID
Well, the code is a bit confussed. first the use of i['controls'].other (really you can use i.controls.other an a estrange mix using FormBuilder and new Form.
As always we has a FormArray we create a getter
get justificationItems()
{
return this.form.get('justificationItems') as FormArray;
}
In stead use two differents functions to create the form, we can use and unique
createJustificationField(x: any=null): FormGroup {
x=x || {name:null,description:null,code:null,other:null}
return new FormGroup({
name: new FormControl(x.name, [Validators.required]),
description: new FormControl(x.description, [Validators.required]),
code: new FormControl(x.code, [Validators.required]),
other: new FormControl({value:x.other,
disabled:x.code!=='123'},[Validators.required]),
});
}
See that we can use as
this.createJustificationField(..an object..)
this.createJustificationField()
Our functions: createForm, addItem and selectClick (I like more another name like codeChange but is a minor change) becomes like
createForm() {
this.service.getmodel().subscribe((response:any) => {
this.form = new FormGroup({
justificationItems: new FormArray(
response.justificationItems.map(x=>
this.createJustificationField(x))
),
});
});
}
addItem(): void {
this.justificationItems.push(this.createJustificationField());
this.form.updateValueAndValidity();
}
selectClick(x,i) {
if (x === '123')
this.justificationItems.at(i).get('other').enable()
else
this.justificationItems.at(i).get('other').disable()
this.form.updateValueAndValidity();
}
And the .html becomes more clear in the way
<form *ngIf="form" [formGroup]="form">
<div formArrayName="justificationItems">
<div
*ngFor="
let orgs of justificationItems.controls;
let i = index;
let last = last
"
[formGroupName]="i"
>
<label>Name </label>
<input formControlName="name" placeholder="Item name" /><br />
<label>Description </label>
<input
formControlName="description"
placeholder="Item description"
/><br />
<label>Code </label>
<select
(change)="selectClick($event.target.value, i)"
formControlName="code"
>
<option value="123">123</option>
<option value="456">456</option></select
><br />
<ng-container *ngIf="justificationItems.at(i).value.code === '123'">
<label>Other Code </label>
<input formControlName="other" placeholder="other" /><br /><br />
</ng-container>
<button
*ngIf="last"
[disabled]="justificationItems.at(i).invalid"
type="button"
(click)="addItem()"
>
Add Item
</button>
</div>
</div>
<button [disabled]="!form.valid" type="button">Submit</button>
</form>
<p>Is form valid: {{ form?.valid | json }}</p>
see the stackblitz
Root cause: When selectClick trigger, you clear or set validation for all controls other in array form. You should set only for one form in formArray.
I rewrite your function:
selectClick(x, index) {
const f = this.form;
let items = this.form.get('justificationItems') as FormArray;
if (x === '123') {
items.controls[index]['controls'].other.setValidators([Validators.required]);
} else {
items.controls.forEach(i => {
items.controls[index]['controls'].other.clearValidators();
i['controls'].other.updateValueAndValidity();
});
}
items.controls[index]['controls'].other.updateValueAndValidity();
}
change code in template:
<select
(change)="selectClick($event.target.value, i)"
formControlName="code"
>

Fetch input values created by ngFor on button click Angular

I have an input field which is being generated using *ngFor and I want to capture all those values inside the input field on submit button click. I have tried all possible ways but not able to get the entered data, please help me. Below is my code I have tried.
html Code:
arr= [{name:"test", percentage: "29"},{name:"abc", percentage: "45"}, {name:"def", percentage: "63"}]
<div *ngFor= "let obj of arr">
<input type="text" [value]="obj.percentage">
</div>
<button (click)="submit()"></button>
Your current solution does not have any sort of form manager. There are two solutions:
Reactive Forms
// component.ts
form = new FormGroup( {
test: new FormControl(null)
// Add your other fields here
} );
// Patch your values into the form
arr.forEach(value => {
this.form.get(value.name).patchValue(value.percentage);
})
// html
<div *ngFor= "let obj of arr">
<input type="text" [formControl]="form.get(obj.name)" >
</div>
Your form will store all the values. To get them once, you could use form.value
NgModel
// component.ts
test: boolean;
// Add your other fields here
// Patch your values into the form
arr.forEach(value => {
this.[value.name] = value.percentage;
})
// html
<div *ngFor= "let obj of arr">
<input type="text" [(ngModel)]="obj.name" >
</div>
Both of these examples are cutting corners but they should give you enough of an idea of where to head next. I'd suggest the Reactive Forms path.

Vuejs and HTML creating a complex JSON Object dynamically and displaying the same to user using V-for

I am developing a Vuejs application within which I have a field. For this field, users can provide the values and this field expands dynamically based on the user-provided values.
The field name is extensions, initially an Add Extension button will be displayed. With on click of the button, a bootstrap modal will be displayed which has 3 fields: namespace (text), localname (text), datatype(dropdown: string/complex). If the datatype is string then a simple text field will be displayed. However, if the datatype is complex then another button should be displayed and with on click of the button again the same modal is displayed with fields and the process continues. So the created JSON based on this will expand horizontally and vertically.
I am able to complete the first iteration and display the elements to users on the front end. However, for further iteration, I am not understanding how to achieve it using the recursive approach. Since I don't know how many times users may create the extensions I need to have an approach that dynamically does this.
Can someone please help me how to create and display the JSONarray using Vuejs which expands horizontally and vertically?
Following is the code I have so far:
<template>
<div class="container-fluid">
<div class="row">
<div class="col-md-3">
<span>Extensions</span>
<button class="form-control" #click="createExtensions()">
Add Another
</button>
</div>
</div>
<div v-for="extension in extensionList" :key="extension.ID" class="form-inline">
<span>{{ extension.namespace+ ":"+extension.localName }}</span>
<input v-if="extension.dataType == 'string'" type="text" #input="AddExtensionText($event,extension.ID)">
<button v-if="extension.dataType == 'complex'" #click="AddComplextExtension(extension.ID)">
Add another
</button>
</div>
<b-modal
id="Extension"
title="Add Another Element"
size="lg"
width="100%"
:visible="showModal"
>
<b-form id="AddExtension" #submit.prevent="submitExtension">
<div class="form-group">
<label for="message-text" class="col-form-label">Namespace URI:</label>
<input
v-model="extension.namespace"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">Local Name:</label>
<input
v-model="extension.localName"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label for="AddExtensionDataType" class="col-form-label">Data Type:</label>
<b-form-select v-model="extension.dataType" class="form-control">
<b-form-select-option value="string">
String
</b-form-select-option>
<b-form-select-option value="complex">
Complex
</b-form-select-option>
</b-form-select>
</div>
</b-form>
<template #modal-footer="{ cancel }">
<b-btn #click="cancel">
Cancel
</b-btn>
<b-btn variant="primary" type="submit" form="AddExtension">
OK
</b-btn>
</template>
</b-modal>
</div>
</template>
<script>
export default {
data () {
return {
extensionList: [],
extensionCount: 0,
extension: {
namespace: '',
localName: '',
dataType: 'string'
},
showModal: false
}
},
methods: {
// Method to create extensions and add
createExtensions () {
this.showModal = true
},
// Function to submit the each extension
submitExtension () {
this.showModal = false
const extensionObj = {}
extensionObj.ID = this.extensionCount
extensionObj.namespace = this.extension.namespace
extensionObj.localName = this.extension.localName
extensionObj.dataType = this.extension.dataType
this.extensionList.push(extensionObj)
this.extensionCount++
},
// On addition of the input field value update List
AddExtensionText (event, extensionID) {
const extension = this.extensionList.filter(ex => ex.ID === extensionID)[0]
if (typeof extension !== 'undefined') {
extension.text = (typeof event.target.value !== 'undefined') ? event.target.value : ''
}
},
// Add complex extension
AddComplextExtension (extensionID) {
this.showModal = true
}
}
}
</script>
<style>
</style>
This is the initial field I have:
This is what I want to achieve where everything is created dynamically and JSON expands both horizontally and vertically:
Can someone please let me know how to create such dynamic JSON using Vuejs and display the same in the frontend.
To display data recursively, you need to use recursive components.
Abstract your v-for code into another component file (let's call it NodeComponent.vue). Pass your extensionList to this component, then inside this component, add another NodeComponent for each extension which has type complex.
Since your extension would be another array if it is complex, you can pass it directly into this NodeComponent as a prop and let recursion work its magic.
NodeComponent.vue
<template>
<div>
<div
v-for="extension in extensionList"
:key="extension.ID"
class="form-inline"
>
<span>{{ extension.namespace + ":" + extension.localName }}</span>
<input
v-if="extension.dataType == 'string'"
type="text"
#input="$emit('AddExtensionText', {$event, id: extension.ID}"
/>
<NodeComponent v-if="extention.dataType == 'complex'" :extentionList="extension" #AddExtensionText="AddExtensionText($event)"/>
<button
v-if="extension.dataType == 'complex'"
#click="AddComplextExtension(extension.ID)"
>
Add another
</button>
</div>
</div>
</template>
<script>
export default {
props: {
extensionList: Array,
extension: Object,
},
methods: {
AddComplextExtension(extensionID) {
// Emit event on root to show modal, or use this.$bvModal.show('modal-id') or create a dynamic modal, see: https://bootstrap-vue.org/docs/components/modal#message-box-advanced-usage
}
AddExtensionText({ value, id }) {
const i = this.extensionList.findIndex((el) => el.ID === id);
this.$set(extensionList, i, value);
}
}
};
</script>
Note that I emit a custom event from child NodeComponents on changing input text so that the parent can make this change in its extensionList array, using this.$set to maintain reactivity.
EDIT: If you want to add new Node components:
You need to have a parent component that holds the first NodeComponent in it. In here you'll define the modal (if you define it inside NodeComponent, you'll have a separate modal reference for each NodeComponent. Judging from your code you're probably using Bootstrap-Vue, it injects modals lazily when shown, so I don't think this will affect your performance too much, but it still doesn't feel like good code.). You need to emit event on root to show the modal. You need to send the extensionList as payload with this event like this: this.$root.emit('showModal', extensionList). In you parent component you can listen to the event and show the modal. Now inside your submitExtension function, you can use this extensionList and push a new object to it. The corresponding NodeComponent will update itself since arrays are passed by reference.
this.$root.on('showModal`, (extensionList) => {
this.editExtensionList = extensionList;
showModal = true;
}
submitExtension() {
this.showModal = false
const extensionObj = {}
extensionObj.ID = this.extensionCount
extensionObj.namespace = this.extension.namespace
extensionObj.localName = this.extension.localName
extensionObj.dataType = this.extension.dataType
this.editExtensionList.push(extensionObj)
this.extensionCount++
}
All being said, at this point it might be worthwhile to invest in implementing a VueX store where you have a global extentionList and define mutations to it.

v-model is not updating value in real time for a specific field. Other fields with same datatype are working fine. What's wrong?

I have a specific model in which there are multiple values that can be updated through form given on the webpage. The values are like "Quantity", "Rate", "Amount", "Net rate", etc.
The issue is, I am binding those values with my inputs using v-model and it is working perfectly perfect for all the fields except the "net rate" field! It s not updating value in real time but when I update some value in field and click refresh on Vue-devtools UI, it gets updated! It's just not being updated as soon as the value changes in the field. And that too, For a specific field called "net_rate"!
I have no Idea what's going on here! Here is my code, The first field with id discount_perc is working perfectly! When I update anything in this field, it gets updated as soon as I edit value in the field. But same is not happening with the net_rate field.
<div class="inline-form-group col-sm-12 col-md-4 col-lg-1 text-right">
<label for="discount_perc" style="color:teal;font-size:14px;">Dis %</label>
<input type="text" ref="discount_perc" #keydown.enter="$refs.net_rate.focus()" #input="setAmount()" v-model="selectedItem.discount_perc" class="form-control text-right" />
</div>
<div class="inline-form-group col-sm-12 col-md-4 col-lg-1 text-right">
<label for="net_rate" style="color:teal;font-size:14px;">Net rate</label>
<input type="text" ref="net_rate" v-model="selectedItem.net_rate" #input="updateAmount()" #keydown.enter="addItem()" #keydown.tab="addItem()" class="form-control text-right" />
</div>
Here are my methods that I am firing on input event for both of these fields.
setAmount: function () {
var discount_percAmount = this.selectedItem.discount_perc?(this.selectedItem.discount_perc*this.selectedItem.price)/100:0;
this.selectedItem.net_rate = this.selectedItem.price-discount_percAmount;
if(this.selectedItem.size_breadth > 0 && this.selectedItem.size_length > 0){
this.selectedItem.item_amt = this.selectedItem.net_rate*this.selectedItem.quantity*this.selectedItem.size_breadth*this.selectedItem.size_length;
} else {
this.selectedItem.item_amt = this.selectedItem.net_rate*this.selectedItem.quantity;
}
},
updateAmount: function () {
if(this.selectedItem.size_breadth > 0 && this.selectedItem.size_length > 0){
this.selectedItem.item_amt = parseFloat(this.selectedItem.net_rate)*this.selectedItem.quantity*this.selectedItem.size_breadth*this.selectedItem.size_length;
} else {
this.selectedItem.item_amt = parseFloat(this.selectedItem.net_rate)*this.selectedItem.quantity;
}
},
I know there is one redundant piece of code that can be converted into a method, I will deal with it later, right now, My priority is to get this thing working.
I tried alerting net_rate value calculation in updateAmount() function and it works. But it's not updating the value in model. It requires refresh! I am scratching my head since more than 24 hours but can't solve the issue.
Has anyone faced the same issue before? Please let me know the reason and possible solution! Any kind of help would be appreciated!
UPDATE: Here is my Data.
data () {
return {
availableParties:[],
party: [],
availableArchitechs: [],
availableStaff: [],
availableLocations: [],
location: '',
availableItemCodes: [],
selectedItem: [],
quotation: {
party_id: null, date:new Date().toISOString().split('T')[0], architech: '',staff: '', items: [], order_no: '',
item_amt: 0,gst_0_amt:0,gst_5_amt:0,gst_12_amt:0,gst_18_amt:0,gst_28_amt:0,final_amt:0
},
latestQuotation: [],
partySpecificItemInfo:{
rate: 0,
discount_perc: 0,
net_rate: 0
},
updateAllowed: true,
selectedItemImageExist: false,
}
},
The problem here is that you've got an Array initialized in VueJS in your data property which is observable. But then you replace it with an Object in your function, and that Object is not observable. You can do some work to make that observable, too, or just start with an Object by default so it's already observable, then use Vue.set to add properties that become observable to that object.
Make it an object by default:
selectedItem: {},
Use Vue.set when you want to work on that object:
Vue.set(this.selectedItem, 'property', value)
Just replace property with things like net_rate and value with whatever should be the value of that property in the object.
Now it will be reactive and will have the observer attached to it so the value will correctly update.
Docs on Vue.set
Note this line from the docs here:
Vue cannot detect normal property additions (e.g. this.myObject.newProperty = 'hi')

Angular array.push() creates a duplicate for ng-repeat

I have the follow HTML:
<div ng-repeat="contact in vm.contacts">
<input type="text" ng-model="contact.firstName">
<input type="text" ng-model="contact.lastName">
<button ng-click="vm.addNew()">add</button>
</div>
and the following code on my Angular controller:
vm.contacts = [];
vm.addNew = addNew;
init();
function init() {
addNew();
}
function addNew() {
vm.contacts.push({});
}
So, when the page is started, the controller adds an empty object to the vm.contacts array.
My problem is: once I fill the fields and click on the add button, instead of creating an array entry with an empty object, angular is duplicating the previuous array entry.
So, if I enter "John" for first name and "Smith" for last name, and then click on the add button, the resulting array will be:
[
{firstName: "John", lastName: "Smith"},
{firstName: "John", lastName: "Smith"}
]
And the same contact will be displayed twice.
How do I prevent this from happening?
I've tried both to use track by $index on the ng-repeat declaration, and angular.copy() on the addNew function, and nothing seems to work. I want to be able to add a new empty contact, I do not wish to replicate or duplicate one.
Thanks in advance!
Something like this:
VIEW
<div ng-app="app" ng-controller="MainController as vm">
{{vm.contacts}}
<div ng-repeat="contact in vm.contacts">
<input type="text" ng-model="contact.firstName">
<input type="text" ng-model="contact.lastName">
<button ng-click="vm.addNew()">add</button>
</div>
</div>
CONTROLLER
angular
.module('app', [])
.controller('MainController', MainController)
function MainController($timeout) {
var vm = this;
vm.contacts = [{}];
vm.addNew = addNew;
function addNew() {
vm.contacts.push({});
}
}
JSFIDDLE
I think the problem arises here:
<input type="text" ng-model="contact.firstName">
<input type="text" ng-model="contact.lastName">
Once the array receives the first and last contact names, the input after that can only be the first and last name stored in contact as the first values are being stored in contact and from there on can only use those values.

Categories