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

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.

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.

Cannot read property 'first_name' of undefined

I have this issue
Cannot read property 'first_name' of undefined
This is my HTML File checkout.html
<ion-content>
<ion-item>
<ion-label>First Name</ion-label>
<ion-input type="text" [(ngModel)]="newOrder.billing.first_name"></ion-input>
</ion-item>
....
</ion-content>
and this is my ts file checkout.ts
this.storage.get("userLoginInfo").then((userLoginInfo) => {
this.userInfo = userLoginInfo.user;
let email = userLoginInfo.user.email;
let id = userLoginInfo.user.id;
this.WooCommerce.getAsync("customers/email/"+email).then((data) => {
this.newOrder = JSON.parse(data.body).customer;
})
})
and this is an image of the error
It seems that you are loading the newOrder asynchronous.
As a result your page get rendered, but the async-task is not finished yet.
You wrote that you declared this.newOrder.billing = {} in the constructor.
Because of that, he now tries to read newOrder.billing.first_name in your html.
newOrder.billing is an empty object, so there is no first_name there.
Only when the async-task has finished, this data will be existent. But currently Angular throws an error before that could happen.
There are two very common ways to handle a situation like that.
You could tell the template that it should NOT render the part with the missing data
<ion-item *ngIf="newOrder.billing">
<ion-label>First Name</ion-label>
<ion-input type="text" [(ngModel)]="newOrder.billing.first_name"></ion-input>
</ion-item>
Then the *ngIf will see that the variable is undefined and that part of the template won´t be shown in the DOM => no error :-)
As soon as the async-task finishes, the variable is not undefined anymore and the template-part will be shown.
Another way (more common when you just want to show data) is using
{{ newOrder.billing.first_name | async }}
The async-pipe will also make sure that Angular could handle that one.
In combination with [ngModel] i think the async-pipe will not work. But it may be worth a try.
warm regards
Just add a quotemark, i think it will help.
<ion-input type="text" [(ngModel)]="newOrder?.billing.first_name"></ion-input>
You can use simply a quote mark with the object in your interpolation as said by
#Kraken.
But two solution provided by the #JanRecker:
if you use async pipe in your interpolation it won't work, because this works with Observables rxjs.
So, adding a quote mark really a helpful option.

Angular4 Template

I have written a simple code to demonstrate ngTemplate
<div> <ng-container [ngTemplateOutlet] ="template1"> </ng-container></div>
and here are the template
<ng-template #template1> This is 1st template </ng-template>
<ng-template #template2>This is 2nd template </ng-template>
It is working as expected, but my problem is I am not able to pass dynamic value to [ngTemplateOutlet]
I want to achieve something like this
<div> <ng-container [ngTemplateOutlet] ="selectTemplate"></ng-container> </div>
Which will either select template1 or template2.
Whenever I am passing some dynamic value which I have defined in my typescript file it gives is error
ERROR TypeError: templateRef.createEmbeddedView is not a function
My typescript file contains this code
if(some condition) {
this.selectTemplate = "template1";
} else {
this.selectTemplate = "template2";
}
In your first example, you're binding ngTemplateOutlet to the value of the expression template1, which is the first ngTemplate element. So far so good.
In your second example, you're binding ngTemplateOutlet to the value of the expression selectTemplate, which is not a template element - you've set it to be a string!
To fix this, you first need to add the template references to your component as fields using the ViewChild annotation:
#ViewChild('template1') template1;
#ViewChild('template2') template2;
Then set selectTemplate to refer to the element itself rather than its name:
if (some condition) {
this.selectTemplate = this.template1;
}
else {
this.selectTemplate = this.template2;
}
Note that the ViewChild variables will not be set until the component's view has been initialized - you can use the ngViewAfterInithook to wait for that to happen.
update Angular 5
ngOutletContext was renamed to ngTemplateOutletContext
See also https://github.com/angular/angular/blob/master/CHANGELOG.md#500-beta5-2017-08-29
original
You can pass a context
<ng-container [ngTemplateOutlet]="template1; context: {foo: 'foo', bar: 'bar', $implicit: 'implit' }"
In the template you can use it like
<ng-template #template1 let-foo1="foo" let-foo2="bar" let-item>
This is 1st template
<div>foo1: {{foo1}}</div>
<div>foo2: {{foo2}}</div>
<div>foo1: {{item}}</div>
</ng-template>
I hope the names I used are not too confusing. I tried to make the distinction between
the name as the value is passed to the context
the name as it is used inside the template
$implicit which doesn't require ="somename" in the template variable definintion (let-item)

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

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/

Categories

Resources