How do I set Angular 2 ngModel from a firebase observable on a select dropdown - javascript

I am trying to set the default value in a select dropdown from a firebase object using angularfire 2. But I don't know how to make it work with setting the default value of the select box. ngModel doesn't allow something like (ngModel)="(default_tax | async)"
code:
public default_tax$:Observable<any>;
public tax$:Observable<Tax>;
ngOnInit(): void {
this.tax$ = this.db.list(`tax_rates`);
this.default_tax$ = this.db.object(`settings/default_tax_rate`);
}
template:
<select name="tax_rate" (ngModel)="default_tax.$key" (ngModelChange)="onSelect($event)">
<option *ngFor="let tax of (taxes$ | async)" [ngValue]="tax.$key">
{{tax.name}} - {{tax.rate}}
</option>
</select>
Firebase object:
account:
setttings:
default_tax_rate: "-somekey"
tax_rates:
"-somekey":
"name":"5.5"
"-someotherkey"
"anothername":"4.5"

Ok, I found out how to do this. I was writing the ngModel wrong.
WRONG:
(ngModel)="default_tax.$key"
(ngModel)="(default_tax | async)"
CORRECT:
[ngModel]="(default_tax | async)?.$value"
Note that ngModel is wrapped with [] instead of [()] or (). Brackets[] indicate input, where parenthesis() indicate output. Having both [()] is for two-way binding (both input and output).
A great guide to help understand this in more details is http://blog.ng-book.com/the-ultimate-guide-to-forms-in-angular-2/

Related

Angular Mat-Autocomplete Sending Entire Object on Post Request

I have an autocomplete field that behaves correctly on the frontend, but it's sending the entire object to the backend due to [value]="option". When I change it to [value]="option.id" the form submits correctly, however this breaks the [displayWith] function once an option is selected. The input is blank due to the fact displayWith uses the object to update the input.
What Can I modify to send the correct id to the backend while preserving the display function? I tried sending the expenseCategory through displayWith as a parameter, but it's not in the for block, therefore I don't have access to the option object. One thing to note, these are dynamically created inputs as part of a formArray.
Heres the autocomplete field:
<mat-form-field appearance="outline" class="full-width-input">
<mat-label>Expense Category</mat-label>
<input type="text" aria-label="Assignee" matInput formControlName = "expense_category_id" [matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayExpenseCategoryFn">
<mat-option *ngFor="let option of filteredExpenseCategories[i] | async" [value]="option">
{{option.category}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
Here's the displayWith function:
displayExpenseCategoryFn(expenseCategory?: ExpenseCategory): string | undefined {
return expenseCategory ? expenseCategory.category : undefined;
}
First, I'm sure this is a typo, but what is i, in:
let option of filteredExpenseCategories[i]
Secondly, you don't need a displayWith function if you also are defining a template, which you are with the binding {{option.category}}.
So you could just go back to [value]="option.id", which is the easiest way IMHO.
If you are submitting your form via code, then you can always massage the data before it goes off to the gods.
You could also make use of the (optionSelected) event of the MatAutoComplete (https://material.angular.io/components/autocomplete/api):
public selectOption(e: MatAutocompleteSelectedEvent): void {
const myRealObject = e.option.value as MyRealObject;
// do anything else you need here, such as tweak the data or clearout the typeahead control
}
<mat-autocomplete #myAutoComplete (optionSelected)="selectOption($event)">
<mat-option *ngFor="let option of options" [value]="option.id">
{{option.name}}
</mat-option>
</mat-autocomplete>
Hope this helps.
Ok. I figured it out from a previous project I did a couple years ago (thought I solved this before.)
So what I did was bind displayExpenseCategoryFn to the controller, so I could search through the expenseCategories array to extract the category name and display that in the input field.
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayExpenseCategoryFn.bind(this)">
<mat-option *ngFor="let option of filteredExpenseCategories[i] | async" [value]="option.id">
{{option.category}}
</mat-option>
</mat-autocomplete>
I then made the following changes to the displayWith function.
displayExpenseCategoryFn(expenseCategoryId?: number): string | undefined {
const index = this.expenseCategories.findIndex(expenseCategoryIndex => expenseCategoryIndex.id === expenseCategoryId);
return expenseCategoryId ? this.expenseCategories[index].category : undefined;
}

Is PrimeNG's p-pickList changing source and target?

I have an Angular app with PrimeNG. I am using the PickList component like this:
<p-pickList
[source]="source"
[target]="target"
>
<ng-template
let-item
pTemplate="item">
{{item | json}}
</ng-template>
</p-pickList>
<h2>source</h2>
<ul>
<li *ngFor="let s of source">{{s|json}}</li>
</ul>
<h2>target</h2>
<ul>
<li *ngFor="let t of target">{{t|json}}</li>
</ul>
The component itself is straightforward:
#Component({
selector: 'app-hello',
templateUrl: './hello.component.html',
styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
source: string[];
target: string[] = [];
constructor() {
this.source = [
"foo",
"bar",
"baz",
];
}
}
I do not use two-way-binding here, so how does PrimeNG updates source and target property?
On my main project source and target are #Input() properties and thus I don't want some sub component to fiddle with it.
How is it possible that PrimeNG changes those values?
You could replicate the values of source and target in your HelloComponent, then use these copies for the underlying PrimeNG PickList widget. This allows you to pass updates to the HelloComponent that will be communicated down to the PrimeNG widget, but changes to those arrays within the PrimeNG widget won't impact the original input array.
I believe in your original code, the original array that's being passed as an input to HelloComponent, then passed into the PrimeNG widget, is being passed by a "copy of a reference."
private _sourceOptions: string[];
#Input set sourceOptions(options: string[]) {
// Ensure we're not passing a reference to the array down because it may still
// be changed. Create a new array with the same items. This can also help with
// firing change detection in the PrimeNG widget.
this._sourceOptions = options.slice(0);
}
get sourceOptions(): string[] {
return this._sourceOptions;
}
<!-- Component template file -->
<p-pickList [source]="sourceOptions" [target]="target">
<ng-template let-item pTemplate="item">
{{item | json}}
</ng-template>
</p-pickList>
Odds are that within the component for primeNG there is an OnChange listener, and just generally speaking when something changes within one #Input it does trigger an onChange event.
As the Angular doc says (https://angular.io/api/core/OnChanges) anytime a data bound property changes it fires onChange. In this case here, target is a databound property.
Also, what do you mean by changing values? If you select foo it turns into foobar? Under the hood primeNG is not manipulating the data you passed it, it probably has its own internal store of how it displays the data for its picker list component.

Angular ng-select : selectedItems.map is not a function

When I'm using ng-select in reactive form angular I get this error:
ERROR TypeError: selectedItems.map is not a function
I have 3 select the first two work very well but in this third one I get the error ! to map item i'm using the function (the ng-select is inside *ngFor) :
//for mappinig item :
mapLabelValueBS(objet) {
return objet.map(data => {
return {
id: data,
text: data.name
}
})
}
//this is the one that is causing the problem
<ng-select
[allowClear]="true"
[items]="mapLabelValueBS(filieres)"
placeholder="Filière non sélectionné"
(selected)="selecteFiliere($event)"
formControlName="filiere">
</ng-select>
the result in my page (when I click on the field it doubles itself) :
Without the code is difficult to know, but today I had the same error. The reason was that I determined a default value in the FormControl that had no relation with the array that ng-select demands. When the FormGroup loaded, and this mistaken default was loaded into the ng-select, the error was selectedItems.map is not a function
This error came while I was passing [items]="value" where the value was not an array, so please check if you are not passing non array element to items binding.
You are trying to bind the items of object type. [items] attribute accepts an array. You can trying adding a pipe keyvalue
<ng-select
[allowClear]="true"
[items]="jsonData | keyvalue"
placeholder="Filière non sélectionné"
(selected)="selecteFiliere($event)"
formControlName="filiere">
</ng-select>
few days ago i came across this error if you are binding a list that is filled from backend server be sure to fill the list using concat method like this
this.userService.getLookup().subscribe((res: any) => {
this.apps = this.apps.concat(res.data);
});
I had same problem, because the list of items was undefined sometime in the middle of page preparing, so I added a silly condition to show those select only when the list of items is ready:
<ng-select
*ngIf="selectedItems.map"
[allowClear]="true"
[items]="jsonData | keyvalue"
placeholder="Filière non sélectionné"
(selected)="selecteFiliere($event)"
formControlName="filiere">
</ng-select>

Material.Angular.io mat-autocomplete [displayWith] function update scope variables

I'm running into an issue where I can access locally declared variables in the component controller instantiating the mat-autocomplete. The problem I'm facing is the local variables are stuck in this scope and I can't update them.
Any ideas or thoughts on updating the mat-autocomplete scope variables.
Ultimately what I'm doing is concatenating the display string and a variable bound to the input model. This is giving me an autocomplete input that adds helper text for the user, ideally the text is up to date with clearing the input. The text is currently continuously concatenating, creating unusable text pretty quickly
html
<input
[(ngModel)]="filter>
mat-autocomplete
#auto="matAutocomplete"
[displayWith]="displayFn">
<mat-option
*ngFor="let option of filteredOptions | async"
[value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
component.ts
displayFn(search): string | undefined {
if(!search) return; //check if the search isn't already populated
if(!search.match(/(=|\*)/)){
if(this.filter){
this.filter += ' ' + search + '==*term*';
}else{
this.filter = search +'==*term*';
}
return this.filter; //this isn't persisting across the lifecycle
}
}
You have two options, the first one is just calling [displayWith]="displayFn.bind(this)" which looks weird in the Angular world, but I can confirm that it works (although I got an Error on my WebStorm saying ng: Unknow Method bind)
And the second one is to use an arrow function in order to preserve the context.
Something like this:
displayFn(offer?: Offer): string | undefined {
return offer && offer.name == this.currentOffer.name ? offer.name : undefined;
}
displayFnWrapper() {
return (offer) => this.displayFn(offer);
}
And in the template:
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFnWrapper()" (optionSelected)='assign($event.option.value)'>
<mat-option *ngFor="let offer of filteredOffers$ | async" [value]="offer">{{ offer.name }}</mat-option>
</mat-autocomplete>
If I use an example, MyClass, where
#Input() modeCity = false;
in ngOnInit() I can access the modeCity and change it. It is reflected over other methods in the class.
in HTML,
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption
[displayWith]="itemDisplayFn" (optionSelected)="selected($event)">
then for method itemDisplayFn(item: ..) in the ts file, the modeCity is undefined.
I found that somehow the method itemDisplayFn() has static context. Therefore I created the property,
static staticModeCity = false;
staticModeCity can be set in the ngOnInit() like so,
MyClass.staticModeCity = true
and used in the Method itemDisplayFn() like so,
if(MyClass.staticModeCity) ....
I do not know why this is. Of course static can be conflicting, if the same component is used multiple times in the same parent component.

Angular Reactive Forms: Dynamic Select dropdown value not binding

I have two arrays of data: AssociatedPrincipals (previously saved data) and ReferencePrincipals (static data to populate in dropdown controls). I'm struggling to get the previous value from AssociatedPrincipals to be displayed/selected in a dynamic amount (most examples use a single dropdown) of dropdowns on page load.
I'm not certain how to set up the form (code behind and HTML), especially setting the Select's formControlName. Currently, the static values in each dropdown populate, but I cannot get the selected value to bind properly.
public ngOnInit() {
this.factsForm = this.formbuilder.group({
associatedPrincipals: this.formbuilder.array([]),
referencePrincipals: this.formbuilder.array([])
});
// Data for both of these methods comes from external source...
var responseData = // HTTP source...
// Push retrieved data into form
this.initPrincipals(responseData[0]);
// Push static data into form
this.initStaticData(responseData[1]);
}
public initPrincipals(principals?: IAssociatedPrincipal[]): FormArray {
principals.forEach((principal) => {
this.associatedPrincipals.push(this.createPrincipalFormGroup(principal));
});
}
public initStaticData(response: IReferencePrincipal[]) {
response.forEach((principal) => {
this.referencePrincipals.push(
this.formbuilder.control({
code: principal.code,
canHaveLead: principal.canHaveLead,
isDuplicate: false
}));
});
}
public createPrincipalFormGroup(principal: IAssociatedPrincipal) {
return this.formbuilder.group({
code: principal.code,
canHaveLead: false,
isDuplicate: false
});
}
public get associatedPrincipals(): FormArray {
return this.factsForm.get('associatedPrincipals') as FormArray;
}
public get referencePrincipals(): FormArray {
return this.factsForm.get("referencePrincipals") as FormArray;
}
HTML:
<form novalidate [formGroup]="factsForm">
<div formArrayName="associatedPrincipals">
<div *ngFor="let associatedPrincipal of associatedPrincipals.controls; let i=index;" [formGroupName]="i" >
<select class="form-control create-input"
formControlName="i">
<option value=null disabled selected hidden>--Select--</option>
<option *ngFor="let refPrincipal of referencePrincipals.controls" [ngValue]="refPrincipal">refPrincipal.value.code</option>
</select>
</div>
</div>
</form>
I appreciate any feedback!
EDIT: Added Plunker showing the issue: https://embed.plnkr.co/XMLvFUbuc32EStLylDGO/
Problems in your demo
Based on the demo you provided, There are several problems as listed below:
There is no formControlName assigned to select.
You are binding object to select's option.
For the first problem
Since you are looping through associatedPrincipals to show dropdownlist dynamically. And associatedPrincipals which is a formArray which can consider as below:
associatedPrincipals = {
"0": FormControl,
"1": FormControl
}
So you can simply assign i which is defined at *ngFor expression to formControlName.
<select formControlName="{{i}}" style="margin-top: 10px">
...
</select>
For the second problem
While binding object to option, Angular will compare default value and option's value by object instance by default.
You can set same instance(get from value of referencePrincipals's formControls) to formControl of associatedPrincipals(as #Fetra R.'s answer). But this is not the most convenient way since you have to take some logic to keep the same instance of an object.
Here I would give you another solution which is using compareWith directive designed specifically for your current situation, see docs.
Using compareWith directive, you just need to implement a compareFun to tell angular how to consider two objects(with different instances) as the same.Here yo can include comparing object instance and comparing object fields at the same time.
<select formControlName="{{i}}" style="margin-top: 10px" [compareWith]="compareFun">
<option value=null disabled selected hidden>--Select--</option>
<option *ngFor="let refPrincipal of referencePrincipals.controls"
[ngValue]="refPrincipal.value">{{ refPrincipal.value.code }}</option>
</select>
// tell angular how to compare two objects
compareFn(item1, item2): boolean {
return item1 && item2 ? item1.code === item2.code : item1 === item2;
}
Refer docs and fixed demo to learn detail about it.
You need to pass the exact same reference of the object which populate the select into the selected one to get the selected value.
Here you use a value of all FormControl in referencePrincipals to populate your selectbox, so to get it selected use this object:
public createPrincipalFormControl(principal) {
const selectedFormControl = this.referencePrincipals.controls.find(form => form.value.code === principal.code)
return this.formbuilder.control(selectedFormControl.value);
}
Working plunker. https://plnkr.co/edit/vw3WZ6?p=preview
There are at least 2 problems with your approach.
Your data source here is probably async. Which means you should not do this.initiPrincipals(responseData[0]) immediately after var responseData but instead in callback of whatever method gets you the data or in a subscription to http service, if you get data through Observable.
let subscription = myservice.getmedata.subscribe(data =>
{
//here you should do your initializations with data from server
};
If your data comes from #Input() then the right plase is ngOnChanges.
As Fetra pointed out, regardless of the fact that your previously selected option has exactly the same value as the ones you've prepopulated into select list, in order to set it as selected you need exact reference to the ones you've populated it with. so, something like:
this.formGroup.controls['yourSelectControl'].patchValue(this.yourInitialCollectionOfOptions.find(v => v.propertyByWhichYouWantToCompare == valueFromServer.propertyByWhichYouWantToCompare)

Categories

Resources