Populating select dropdown using nested objects of Model [Angular] - javascript

I have a select dropdown which needs to be prepopulated with the array of objects. However, the ngModel doesn't bind with the data. I've tried to demonstrate it in simpler way, but in reality, I have a home page, when I navigate from home page to contact Page, I need to populate the form data. If I directly enter the contact page, the form must be empty.
In the below example, to make it simpler, I am populating the data on page load, but issue I am facing is similar to my former example, ngModel of select doesn't get updated with data from JSON.
component:
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = 'Angular';
adViewList: AdViewModel = new AdViewModel();
callListType = [
{
callType: {
'id': 1,
'description': 'description1',
}
},
{
callType: {
'id': 2,
'description': 'description2',
},
}
]
compareByOptionId(idFist, idSecond) {
return idFist && idSecond && idFist.callType.description === idSecond;
}
prepopulateData = {
inciNum: "12365",
callType: {
'id': 2,
'description': 'description2',
}
}
ngOnInit() { // on page load
const someDate:any = this.prepopulateData;
this.adViewList = someDate
}
}
export class AdViewModel {
inciNum: string;
callType = Availability;
}
export class Availability {
id: string;
description: string;
}
HTML:
<input type="text" id="inputcomplaint" aria-describedby="complaintHelp"
autocomplete="new-password" name="complaint"
[(ngModel)]="adViewList.inciNum" #complaint="ngModel">
<select [(ngModel)]="adViewList.callType.description"
[compareWith]="compareByOptionId">
<option style="display:none"></option>
<option *ngFor="let data of callListType" [ngValue]="data">
{{data.callType.description}} </option>
</select>
Demo

I tested the following and it seems to work. Notice the change. I'm mapping [(ngModel)]="adViewList.callType" instead of [(ngModel)]="adViewList" because, callListType is a list of callType not AdViewModel
Also note, I'm printing the <p> List: {{adViewList | json}} </p> to check if the model is getting updated and it seems to be getting the values from what is selected.
Update: To have the default value appear as per the onNgInit() you have, you need to change ngValue to [ngValue]="data.callType" and the compare function needs to check ids, not objects. See code below.
Try this:
In your template:
<select name="select" [(ngModel)]="adViewList.callType"
[compareWith]="compareByOptionId">
<option style="display:none"></option>
<option *ngFor="let data of callListType" [ngValue]="data.callType">
{{data.callType.description}} </option>
</select>
In the component:
compareByOptionId(idFist, idSecond) {
return idFist.id === idSecond.id
}
PS: Sorry, I do not have a stackblitz id to share what I tested, I just edited your files to test.

You have 2 problems:
ngValue is invalid, change it to [value]
your binding is wrong, you need to match the [(ngModel)]="someValue" to the [value]="someValue" as it is your model id.
SOLUTION: Change [ngValue]="data" to [value]="data.callType.description" like this:
<select name="select" [(ngModel)]="adViewList.callType.description" [compareWith]="compareByOptionId">
<option style="display:none"></option>
<option *ngFor="let data of callListType" [value]="data.callType.description">
{{data.callType.description}}
</option>
</select>
NOTE: I suggest you to work with the id property instead of the description property as value like this:
[(ngModel)]="adViewList.callType.id"
[value]="data.callType.id"

Related

Vue 3 - V-Model Confusion

I am teaching myself vue 3. I have read article after article on v-model and each time I think I understand how it works I get confused again.
My goal: I built a custom dropdown component. I need the ability to control the value of this dropdown from the parent. When the dropdown changes I want to let the parent know the new value and the index.
Child component.vue
<div>
<select
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
>
<option v-for="option in options" :key="option">
{{ option }}
</option>
</select>
</div>
</template>
<script>
export default {
props: ["options", "modelValue"],
emits: ["update:modelValue"],
methods: {
selected() {
//??????
//want to emit this to the parent
let selectedIndex = this.$event.target.selectedIndex + 1
//this.$emit(value, selectedIndex)
},
},
};
</script>
parent.vue
<template>
<my-drop-down :options="options" v-model="selectedOption" />
</template>
<script>
import myDropDown from "./components/base_dropdown.vue";
export default {
name: "App",
data: () => ({
selectedOption: "2 Line",
selectedIndex: 0,
options: ["1 Line", "2 Line", "3 Line"],
}),
components: {
myDropDown,
},
methods: {
//How can I call this when the select value changes??
onSelectChange(selected, index) {
console.log(`Parent L3rd Change, name: ${selected}, index: ${index} `);
},
},
};
</script>
The two way binding is working correctly. I can control the value of the dropdown from either the child or the parent. But how do I call the onSelectChange method in my child component
Also, and this is may be a dumb question but...
v-model="selectedOption" is the same as writing :value="modelValue" #input="$emit('update:modelValue', $event.target.value)"
so why is the parent written like this <my-drop-down :v-model="selectedOption" />
and the child written like this <select :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
and not simply
<select :v-model="selectedOption />
If you want to call a method inside your parent component when the "select value changes", It is better to call it inside a Vue watch like the codes below:
Parent component:
<template>
<my-drop-down :options="options" v-model="selectedOption" />
</template>
<script>
import myDropDown from "../components/baseDropdown.vue";
export default {
name: "parentModel",
data: () => ({
selectedOption: "2 Line",
// selectedIndex: 0,
options: ["1 Line", "2 Line", "3 Line"],
}),
components: {
myDropDown,
},
computed: {
/* It is better to use computed property for "selectedIndex", because it is related to "selectedOption" and changes accordingly. */
selectedIndex: function () {
return this.options.indexOf(this.selectedOption)
}
},
watch: {
selectedOption(newSelect, oldSelect) {
this.onSelectChange(this.selectedOption, this.selectedIndex)
}
},
methods: {
//How can I call this when the select value changes??
onSelectChange(selected, index) {
console.log(`Parent L3rd Change, name: ${selected}, index: ${index} `);
},
},
}
</script>
<style scoped>
</style>
Child component:
<template>
<div>
<select
:value="modelValue"
#change="$emit('update:modelValue', $event.target.value)"
>
<!-- You can use v-model also here. But it only changes the value of "modelValue" and does not emit anything to parent component. -->
<!-- <select v-model="modelValue">-->
<option v-for="option in options" :key="option">
{{ option }}
</option>
</select>
</div>
</template>
<script>
export default {
name: "baseDropdown",
props: ["options", "modelValue"],
emits: ["update:modelValue"],
/* --------------------------------- */
/* You don't need this method, because "$emit('update:modelValue', $event.target.value)" that is used in "select" tag itself is enough to emit data to the parent component. */
/* --------------------------------- */
// methods: {
// selected() {
//
// //??????
// //want to emit this to the parent
// let selectedIndex = this.$event.target.selectedIndex + 1
// //this.$emit(value, selectedIndex)
// },
// },
}
</script>
<style scoped>
</style>
And about your second part of the question:
v-model="selectedOption" is the same as writing :value="modelValue" #input="$emit('update:modelValue', $event.target.value)"
In my opinion it is not a true statement for two reasons:
Reason one: according to Vue docs :
v-model="selectedOption" is the same as writing :value="selectedOption"
#input="event => selectedOption = event.target.value"
you can't see any $emit in the above statement. But in your case you want to emit data to the parent component.
Reason two: again according to Vue docs it is better to use change as an event for <select> tag.
You look to be needing a watcher in your parent component, one that watches for changes to the selectedOption property, and then uses the new value to get the index from the options array and adds one to it, and uses the new value to set the selectedIndex property.
Per the Vuejs API section on Watchers:
Computed properties allow us to declaratively compute derived values. However, there are cases where we need to perform "side effects" in reaction to state changes - for example, mutating the DOM, or changing another piece of state based on the result of an async operation.
With Options API, we can use the watch option to trigger a function whenever a reactive property changes.
So, for your code, it might look something like:
watch: {
selectedOption(newValue, oldValue) {
console.log('In watcher. New value: ' + newValue)
// get the index of the String in the array and use it to set
// the selectedIndex:
this.selectedIndex = this.options.findIndex(i => i === newValue) + 1;
console.log('selectedIndex: ' + this.selectedIndex)
}
},
As for your question's "second part",
why is the parent written like this <my-drop-down :v-model="selectedOption" />
and the child written like this <select :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
and not simply <select :v-model="selectedOption />
It is probably best to ask that as a separate question, in order to maintain question specificity as required by the site, but as I see it, selectedOption is not acting as a model for the select tag, and in fact selectedOption isn't even a property of the child component, nor should it be.

I need to send values from my select options

I am using angular I want to send information about what element I selected in the select option. Specifically I want to send the data-value to a variable in my product-form.component.ts.
I tried using ngModel but I keep getting errors saying that it doesn't recognize (click)=selectCategory1(category) use a function I am using template forms for my forms that could be the reason. You can see my code live at :
https://stackblitz.com/github/RashellSmith/Dashboard-FrontEnd
Product Form component html
<div class="form-group">
<label for="productCategory">Product Category</label>
<select [(ngModel)]="model.category" (click)=selectCategory1(category) name="category" class="form-control" id="productCategory" (click)="value()">
<option *ngFor="let category of categories" (click)=selectCategory1(category) data-value="{{category.category}}" id={{category.categoryName}} >{{category.categoryName}}</option>
</select>
</div>
Product Form component ts
export class ProductFormComponent implements OnInit {
suppliers: Supplier[];
categories: Category[];
value: number;
model = new Newproduct("name",new Category( this.value,"name"),66,33,true,new Supplier(null,null),76);
selectCategory1(Category){
console.log(Category);
}
submitted = false;
get diagnostic() { return JSON.stringify(this.model); }
onSubmit() { this.submitted = true; }
constructor(private supplierService: SupplierService,private categoryService: CategoryService) { }
ngOnInit() {
this.supplierService.getAll().subscribe(data => {
this.suppliers = data;
});
this.categoryService.getAll().subscribe(data => {
this.categories = data;
});
}
}
You are definitely trying to over-complicate things. You need to bind a simple (change) method to your select list which would be be triggered on value change. You can either pass the value to this function using template reference variable as
<Select #select (change)="SelectChanged(select.value)">
Or you can bind [(ngModel)] directive and access that directly in your component class on (change)
A sample template code:
<select [(ngModel)]="selectedOption" (change)="GetSelectedValue(selectedOption)">
<option *ngFor="let option of options" [ngValue]="option">{{option}}</option>
</select>
And component class would look like
GetSelectedValue(val) {
//do something with val
}
Stackblitz at: https://stackblitz.com/edit/angular-r4d7ul

How do I bind a Display Value of an Angular-Material Select field to ngModel

I'm pretty new to the Angular development scene and have started with a Simple Taxi Booking Form. I append a Material-Select field with an Array with 2 properties like that:
{value: 24, view: '1010 - Vienna'}
that works well but when I submit the form and check the console.log field zip is equal to 24. How can I achieve, that ngModel binds to the view property of my dropdown?
Thank you very much!
Expected Behavior:
When I submit the Form via onSubmit(), property zip of the form object should output "1010 - Innere Stadt" and not 24
I want to include that in the Form object ngForm has created, then I could directly send it to my Express API to store it in a Database. Image attached below
Image to Form Object
app.component.html
<form (ngSubmit)="onSubmit(form)" #form="ngForm">
<mat-select placeholder="Postleitzahl" [(ngModel)]="zipValue" name="zip">
<mat-option *ngFor="let z of zip" [value]="z.value" >{{z.view}}
</mat-option>
</mat-select>
<button type="submit">submit</button>
</form>
app.component.ts
import { Component, OnInit, OnChanges } from '#angular/core';
import { NgForm } from '#angular/forms';
#Component({
selector: 'app-form-de',
templateUrl: './form-de.component.html',
styleUrls: ['./form-de.component.css']
})
export class FormDeComponent {
zip = [
{ view: '1010 - Innere Stadt', value: 24 },
{ view: '1020 - Leopoldstadt', value: 25 },
]
onSubmit(form: NgForm) {
console.log(form.value);
}
}
Make following change in the component -
Have a variable zipValue. Set it’s initial value to one of you zip array values [it is also fine to have undefined] -
export class FormDeComponent {
zipValue;
zip = [
{ view: '1010 - Innere Stadt', value: 24 },
{ view: '1020 - Leopoldstadt', value: 25 },
]
ngOnItit() {
}
onSubmit(form: NgForm) {
console.log(form.value.zip);
console.log(this.zipValue);//you should see mat-select selected value; which would be an object.
}
}
In template make the following change-
EDIT 1 - Use z.view in mat-option like this [This change is for - when you were needed "view" in zip
<mat-option *ngFor="let z of zip" [value]="z.view" >{{z.view}}
</mat-option>
EDIT 2 - Use z in mat-option like this [This change is for - when you need both view and value in zip; This is the same as my very first solution]
<mat-option *ngFor="let z of zip" [value]="z" >{{z.view}}
</mat-option>
In "Edit 2" on click of button, console.log(form.value.zip); will return {view: '1010 - Innere Stadt', value: 24} which has both value and view. While user will see the "view" in Mat-Select. This is the standard way to implement Mat-Select.
see the following stackblitz - https://stackblitz.com/edit/angular-cvjpxq?file=app/select-overview-example.ts
If you still want form.zip.value to return "1010 - Innere Stadt" and you want "value" then "EDIT 1" is the solution and then you will have to find that object in your zip array like this -
const foundZip = this.zip.find(z => z.view === form.value.zip)

Select v-model Object select

How we select item props together in v-model
I mean i select something only take one value.
I want to select value it should be binding name and dept together
Because i will push the these values on table.
<select v-model="name">
<option v-for="member in members" :key="member.id" :label="member.name" :value="member.name">
</option>
</select>
data(){
return {
members: [{id: 1, name: 'alpkaan', dept: 'quality'}]
}
}
I found one way it's running
:value="{'id':member.id, 'name':member.name}"
But;
Select button not work correctly. Because of v-model="name"
How solved this situation idk. :(
In your select you must have a variable that will contain the selected value,
an event that will listen to each value change and with a method related to this event, you will be able to browse your object array to find the object with the selected value.
NOTE : I delete the id with delete this.memberSelectedData.id but you can leave it if you need it, here is a screenshot.
And there is the code:
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<select v-model="memberSelected" #change="slectMember()">
<option v-for="member in members" :key="member.id" :label="member.name" :value="member.name"></option>
</select>
<p>Member name : {{memberSelected}}</p>
<p>Member data : {{memberSelectedData}}</p>
</div>
</template>
<script>
export default {
name: "app",
data() {
return {
members: [{ id: 1, name: "alpkaan", dept: "quality" }, { id: 2, name: "alpkaan2", dept: "quality2" }],
memberSelected: "",
memberSelectedData: {}
};
},
methods: {
slectMember() {
this.members.map( member => {
if(member.name === this.memberSelected){
this.memberSelectedData = member
delete this.memberSelectedData.id
}
})
}
}
};
</script>

Display input box on condition in angular reactive form

I am making angular 6 application where i am using angular reactive form.
Html:
<form [formGroup]="form">
<h2>Click the add button below</h2>
<button (click)="addCreds()">Add</button>
<div formArrayName="credentials" *ngFor="let creds of form.controls.credentials?.value; let i = index">
<div [formGroupName]="i" style="display: flex">
<select (ngModelChange)="changeAction($event)" formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}"> {{option.value}} </option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="showLabel">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
</form>
<pre>
{{ form ?.value | json }}
</pre>
Ts:
form: FormGroup;
showLabel: boolean = false;
options : any = [
{ "key" : "set", "value": "Set" },
{ "key" : "wait", "value": "Wait" },
{ "key" : "go", "value": "Go" }
]
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
credentials: this.fb.array([]),
});
}
addCreds() {
const creds = this.form.controls.credentials as FormArray;
creds.push(this.fb.group({
action: '',
name: '',
label: ''
}));
}
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
Working stackblitz: https://stackblitz.com/edit/angular-form-array-example-yksojj
In this given example there will be an add button, upon clicking that button you will get a select-box with values as set,wait,go and an input called name.. Upon click over add button the same row will be added and forms each object inside array.
Also you can see an if condition inside html for label,
<div *ngIf="showLabel">
<input placeholder="Label" formControlName="label">
</div>
The thing i am in the need was in the select box if i choose set or go then the label needs to displayed otherwise it should not be displayed for which i have written,
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
To be clear enough If the user clicks three times add button then the dropdown and name field alone needs to be displayed for three times whereas if the user selects the value from dropdown as set or go then the label input needs to be displayed to that particular row alone where the dropdown has the value set and go.. If the selection was wait then there should not be label box for the row which has dropdown value as wait.
Kindly help me to achieve the expected result..
If you add disabled:true property to formControl it will exclude fromControl from formGroup then you can enable formControl manually
creds.push(this.fb.group({
action: '',
name: '',
label: {disabled:true, value: ""}
}));
Then enable using enable method
changeAction(e,index) {
if(e === "set" || e === "go") {
this.showLabel = true;
this.form.get('credentials').at(index).get('label').enable();
} else {
this.showLabel = false;
this.form.get('credentials').at(index).get('label').disable();
}
}
Example:https://stackblitz.com/edit/angular-form-array-example-5buwyr
Please, NOT use (ngModelChange) or changeAction($event) to get the value of a control in an array -well, ngModelChange is for Template driven form, not for Reactive Form.
First change your form, create the form using a div with formArrayName="credentials", and a inner div *ngFor="let creds of form.get('credentials').controls
<!--a div with formArrayName--->
<div formArrayName="credentials" >
<!--a div *ngFor over "controls", in this div don't forget formGroupName-->
<div *ngFor="let creds of form.get('credentials').controls; let i = index"
[formGroupName]="i" style="display: flex">
<select formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}">
{{option.value}}
</option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="??????????????">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
Well, now the condition. To get the value of "action" you can use
form.get('credentials').at(i).value.action
//or, better
creds.value.action
So, your div becomes like
<div *ngIf="creds.value.action=='set' ||
creds.value.action=='go'">
<input placeholder="Label" formControlName="label">
</div>
This aproach avoid unnecesary code in your .ts.
this.showLabel
The scope of this variable is your whole component. Therefore, turning it on or off will show and hide all inputs.
You need a per-row value (creds.showLabel in your interface), or use this in your template :
*ngIf="['set','go'].includes(creds.action)"
Updated Stackblitz
By the way, this :
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
is more elegant written this way :
changeAction(e) {
this.showLabel = ['set','go'].includes(e)
}
or this way :
changeAction(e) {
this.showLabel = e in ['set','go']
}
Well, Jeremy's answer is pretty nice and enforces to use most of the native apis given by platform/language, however, here is a traditional approach to understand what is the actual flow and scope of objects, etc,etc...
Root Cause: The show hide field is made global to the scope of component, not at per formgroup level. So changing a single value, and using for all, will affect to all.
Solution:
Use Jeremy's answer for clean coding and less error prone code.
Manage an array of variable that will take care of sho/hide detail for each form group.
In the below answer, added some comments for easy understanding, and added console.log to see what exactly happening. Also, played with index i created in *ngFor and showed how you can make use of these things in future.
import { Component } from '#angular/core';
import { FormControl, FormGroup, FormArray, FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: 'app.component.html',
})
export class AppComponent {
form: FormGroup ;
showLabel = [];
creds : FormArray;
options : any = [
{ "key" : "set", "value": "Set" },
{ "key" : "wait", "value": "Wait" },
{ "key" : "go", "value": "Go" }
]
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
credentials: this.fb.array([]),
});
this.creds = this.form.controls.credentials as FormArray;
}
addCreds() {
this.creds.push(this.fb.group({
action: '',
name: '',
label: ''
}));
// every time adding new foem grp, adding lable show/hide data also.
this.showLabel.push(false);
}
changeAction(e, i) {
//every time input clikced, managing show/hide for that particular data also
console.log("iii", i, e.target.value);
if( e.target.value == "set" || e.target.value == "go") {
this.showLabel[i] = true;
} else {
this.showLabel[i] = false;
}
}
}
<form [formGroup]="form">
<h2>Click the add button below</h2>
<button (click)="addCreds()">Add</button>
<div formArrayName="credentials" *ngFor="let creds of form.controls.credentials?.value; let i = index">
<div [formGroupName]="i" >
<select (change)="changeAction($event, i)" formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}"> {{option.value}} </option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="showLabel[i]">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
</form>
<pre>
{{ form ?.value | json }}
</pre>
See live stackblitz working code
Note: Traditional is meant here.. as we every time do i.e. handle our problem on our own and create new problem on us to solve. It's not a tradition. :P

Categories

Resources